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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/set_vue_error_handler.js30
-rw-r--r--spec/frontend/access_tokens/components/tokens_app_spec.js4
-rw-r--r--spec/frontend/actioncable_connection_monitor_spec.js79
-rw-r--r--spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js17
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_category_spec.js43
-rw-r--r--spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js16
-rw-r--r--spec/frontend/admin/applications/components/delete_application_spec.js7
-rw-r--r--spec/frontend/admin/broadcast_messages/components/message_form_spec.js43
-rw-r--r--spec/frontend/admin/topics/components/remove_avatar_spec.js2
-rw-r--r--spec/frontend/admin/topics/components/topic_select_spec.js4
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js2
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js31
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js13
-rw-r--r--spec/frontend/analytics/usage_trends/components/users_chart_spec.js47
-rw-r--r--spec/frontend/api/user_api_spec.js21
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js68
-rw-r--r--spec/frontend/batch_comments/components/submit_dropdown_spec.js17
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js55
-rw-r--r--spec/frontend/behaviors/gl_emoji_spec.js196
-rw-r--r--spec/frontend/behaviors/markdown/render_gfm_spec.js26
-rw-r--r--spec/frontend/behaviors/markdown/render_metrics_spec.js49
-rw-r--r--spec/frontend/blob/line_highlighter_spec.js71
-rw-r--r--spec/frontend/blob_edit/edit_blob_spec.js1
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js2
-rw-r--r--spec/frontend/boards/components/board_app_spec.js11
-rw-r--r--spec/frontend/boards/components/board_content_spec.js11
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js23
-rw-r--r--spec/frontend/boards/components/board_new_issue_spec.js65
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js4
-rw-r--r--spec/frontend/boards/mock_data.js41
-rw-r--r--spec/frontend/boards/project_select_spec.js111
-rw-r--r--spec/frontend/boards/stores/actions_spec.js8
-rw-r--r--spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap5
-rw-r--r--spec/frontend/branches/components/delete_merged_branches_spec.js2
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js65
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js43
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js5
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js2
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js33
-rw-r--r--spec/frontend/ci/ci_variable_list/mocks.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js21
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js18
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js306
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js37
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js13
-rw-r--r--spec/frontend/ci/pipeline_schedules/mock_data.js34
-rw-r--r--spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap26
-rw-r--r--spec/frontend/ci/reports/components/grouped_issues_list_spec.js83
-rw-r--r--spec/frontend/ci/reports/components/summary_row_spec.js63
-rw-r--r--spec/frontend/ci/reports/mock_data/mock_data.js54
-rw-r--r--spec/frontend/ci/reports/utils_spec.js30
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js82
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js2
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js94
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js75
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_token_spec.js23
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_action_spec.js223
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_button_spec.js222
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_disclosure_dropdown_item_spec.js68
-rw-r--r--spec/frontend/ci/runner/components/runner_delete_modal_spec.js34
-rw-r--r--spec/frontend/ci/runner/components/runner_detail_spec.js88
-rw-r--r--spec/frontend/ci/runner/components/runner_edit_button_spec.js30
-rw-r--r--spec/frontend/ci/runner/components/runner_edit_disclosure_dropdown_item_spec.js42
-rw-r--r--spec/frontend/ci/runner/components/runner_header_actions_spec.js147
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js159
-rw-r--r--spec/frontend/ci/runner/components/runner_pause_action_spec.js180
-rw-r--r--spec/frontend/ci/runner/components/runner_pause_button_spec.js282
-rw-r--r--spec/frontend/ci/runner/components/runner_pause_disclosure_dropdown_item_spec.js71
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js67
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js29
-rw-r--r--spec/frontend/clusters/agents/components/create_token_modal_spec.js13
-rw-r--r--spec/frontend/clusters/agents/components/revoke_token_button_spec.js8
-rw-r--r--spec/frontend/clusters_list/components/clusters_actions_spec.js38
-rw-r--r--spec/frontend/clusters_list/components/delete_agent_button_spec.js10
-rw-r--r--spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap2
-rw-r--r--spec/frontend/commit/commit_pipeline_status_spec.js (renamed from spec/frontend/commit/commit_pipeline_status_component_spec.js)2
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js2
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js2
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js71
-rw-r--r--spec/frontend/content_editor/components/bubble_menus/reference_bubble_menu_spec.js2
-rw-r--r--spec/frontend/content_editor/components/content_editor_spec.js20
-rw-r--r--spec/frontend/content_editor/components/formatting_toolbar_spec.js104
-rw-r--r--spec/frontend/content_editor/components/suggestions_dropdown_spec.js9
-rw-r--r--spec/frontend/content_editor/components/wrappers/code_block_spec.js239
-rw-r--r--spec/frontend/content_editor/components/wrappers/image_spec.js100
-rw-r--r--spec/frontend/content_editor/components/wrappers/reference_spec.js18
-rw-r--r--spec/frontend/content_editor/extensions/code_suggestion_spec.js128
-rw-r--r--spec/frontend/content_editor/extensions/comment_spec.js30
-rw-r--r--spec/frontend/content_editor/extensions/copy_paste_spec.js (renamed from spec/frontend/content_editor/extensions/paste_markdown_spec.js)110
-rw-r--r--spec/frontend/content_editor/extensions/hard_break_spec.js20
-rw-r--r--spec/frontend/content_editor/extensions/html_nodes_spec.js6
-rw-r--r--spec/frontend/content_editor/extensions/image_spec.js2
-rw-r--r--spec/frontend/content_editor/extensions/paragraph_spec.js29
-rw-r--r--spec/frontend/content_editor/remark_markdown_processing_spec.js13
-rw-r--r--spec/frontend/content_editor/services/code_suggestion_utils_spec.js53
-rw-r--r--spec/frontend/content_editor/services/create_content_editor_spec.js8
-rw-r--r--spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js13
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js27
-rw-r--r--spec/frontend/content_editor/test_utils.js19
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js34
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js79
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js30
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js30
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js30
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_merged_spec.js31
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_private_spec.js33
-rw-r--r--spec/frontend/contribution_events/components/contribution_event/contribution_event_pushed_spec.js141
-rw-r--r--spec/frontend/contribution_events/components/contribution_events_spec.js37
-rw-r--r--spec/frontend/contribution_events/components/resource_parent_link_spec.js46
-rw-r--r--spec/frontend/contribution_events/components/target_link_spec.js43
-rw-r--r--spec/frontend/contribution_events/utils.js52
-rw-r--r--spec/frontend/deploy_keys/components/app_spec.js82
-rw-r--r--spec/frontend/design_management/components/design_description/description_form_spec.js35
-rw-r--r--spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap86
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js12
-rw-r--r--spec/frontend/design_management/components/design_notes/design_note_spec.js268
-rw-r--r--spec/frontend/design_management/components/design_presentation_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js4
-rw-r--r--spec/frontend/design_management/mock_data/apollo_mock.js29
-rw-r--r--spec/frontend/design_management/mock_data/discussion.js12
-rw-r--r--spec/frontend/design_management/mock_data/notes.js3
-rw-r--r--spec/frontend/diffs/components/app_spec.js12
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_code_quality_item_spec.js37
-rw-r--r--spec/frontend/diffs/components/diff_code_quality_spec.js55
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js72
-rw-r--r--spec/frontend/diffs/components/diff_discussions_spec.js16
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js26
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js181
-rw-r--r--spec/frontend/diffs/components/diff_inline_findings_spec.js33
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js40
-rw-r--r--spec/frontend/diffs/components/diff_line_spec.js21
-rw-r--r--spec/frontend/diffs/components/diff_row_spec.js14
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js36
-rw-r--r--spec/frontend/diffs/mock_data/diff_code_quality.js87
-rw-r--r--spec/frontend/diffs/store/actions_spec.js65
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js2
-rw-r--r--spec/frontend/diffs/store/utils_spec.js6
-rw-r--r--spec/frontend/drawio/drawio_editor_spec.js1
-rw-r--r--spec/frontend/dropzone_input_spec.js2
-rw-r--r--spec/frontend/editor/source_editor_extension_base_spec.js1
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js7
-rw-r--r--spec/frontend/editor/source_editor_yaml_ext_spec.js4
-rw-r--r--spec/frontend/emoji/index_spec.js95
-rw-r--r--spec/frontend/environments/edit_environment_spec.js158
-rw-r--r--spec/frontend/environments/environment_form_spec.js247
-rw-r--r--spec/frontend/environments/graphql/mock_data.js5
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js46
-rw-r--r--spec/frontend/environments/new_environment_item_spec.js60
-rw-r--r--spec/frontend/environments/new_environment_spec.js108
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js11
-rw-r--r--spec/frontend/fixtures/groups.rb33
-rw-r--r--spec/frontend/fixtures/issues.rb4
-rw-r--r--spec/frontend/fixtures/metrics_dashboard.rb42
-rw-r--r--spec/frontend/fixtures/milestones.rb43
-rw-r--r--spec/frontend/fixtures/pipeline_schedules.rb6
-rw-r--r--spec/frontend/fixtures/static/line_highlighter.html85
-rw-r--r--spec/frontend/fixtures/static/textarea.html27
-rw-r--r--spec/frontend/fixtures/timezones.rb2
-rw-r--r--spec/frontend/fixtures/users.rb9
-rw-r--r--spec/frontend/frequent_items/mock_data.js2
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js2
-rw-r--r--spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js33
-rw-r--r--spec/frontend/groups/components/app_spec.js8
-rw-r--r--spec/frontend/groups/components/overview_tabs_spec.js67
-rw-r--r--spec/frontend/groups/service/archived_projects_service_spec.js90
-rw-r--r--spec/frontend/groups/service/groups_service_spec.js19
-rw-r--r--spec/frontend/header_search/init_spec.js2
-rw-r--r--spec/frontend/ide/components/ide_status_bar_spec.js44
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js1
-rw-r--r--spec/frontend/ide/mock_data.js2
-rw-r--r--spec/frontend/invite_members/components/group_select_spec.js174
-rw-r--r--spec/frontend/invite_members/components/invite_groups_modal_spec.js31
-rw-r--r--spec/frontend/invite_members/components/members_token_select_spec.js13
-rw-r--r--spec/frontend/issuable/components/related_issuable_item_spec.js6
-rw-r--r--spec/frontend/issuable/components/status_box_spec.js2
-rw-r--r--spec/frontend/issuable/issuable_form_spec.js35
-rw-r--r--spec/frontend/issuable/popover/components/issue_popover_spec.js2
-rw-r--r--spec/frontend/issuable/popover/components/mr_popover_spec.js2
-rw-r--r--spec/frontend/issuable/popover/index_spec.js68
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js51
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js109
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js3
-rw-r--r--spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js1
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js1
-rw-r--r--spec/frontend/issues/show/components/delete_issue_modal_spec.js9
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js103
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js36
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js10
-rw-r--r--spec/frontend/issues/show/components/task_list_item_actions_spec.js6
-rw-r--r--spec/frontend/issues/show/issue_spec.js2
-rw-r--r--spec/frontend/jira_connect/branches/components/project_dropdown_spec.js1
-rw-r--r--spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js1
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/app_spec.js8
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/feedback_banner_spec.js45
-rw-r--r--spec/frontend/jobs/components/job/job_app_spec.js2
-rw-r--r--spec/frontend/jobs/components/job/job_container_item_spec.js16
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js34
-rw-r--r--spec/frontend/lib/utils/datetime/date_format_utility_spec.js15
-rw-r--r--spec/frontend/lib/utils/downloader_spec.js4
-rw-r--r--spec/frontend/lib/utils/forms_spec.js111
-rw-r--r--spec/frontend/lib/utils/ref_validator_spec.js23
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js2
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js10
-rw-r--r--spec/frontend/merge_requests/generated_content_spec.js310
-rw-r--r--spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js39
-rw-r--r--spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js12
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap155
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap55
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap160
-rw-r--r--spec/frontend/monitoring/components/charts/annotations_spec.js95
-rw-r--r--spec/frontend/monitoring/components/charts/anomaly_spec.js304
-rw-r--r--spec/frontend/monitoring/components/charts/bar_spec.js53
-rw-r--r--spec/frontend/monitoring/components/charts/column_spec.js118
-rw-r--r--spec/frontend/monitoring/components/charts/empty_chart_spec.js21
-rw-r--r--spec/frontend/monitoring/components/charts/gauge_spec.js210
-rw-r--r--spec/frontend/monitoring/components/charts/heatmap_spec.js93
-rw-r--r--spec/frontend/monitoring/components/charts/options_spec.js327
-rw-r--r--spec/frontend/monitoring/components/charts/single_stat_spec.js94
-rw-r--r--spec/frontend/monitoring/components/charts/stacked_column_spec.js193
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js748
-rw-r--r--spec/frontend/monitoring/components/create_dashboard_modal_spec.js44
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js421
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js395
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_builder_spec.js226
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js582
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js784
-rw-r--r--spec/frontend/monitoring/components/dashboard_template_spec.js41
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js159
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js170
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js166
-rw-r--r--spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js110
-rw-r--r--spec/frontend/monitoring/components/embeds/embed_group_spec.js157
-rw-r--r--spec/frontend/monitoring/components/embeds/metric_embed_spec.js100
-rw-r--r--spec/frontend/monitoring/components/embeds/mock_data.js86
-rw-r--r--spec/frontend/monitoring/components/empty_state_spec.js55
-rw-r--r--spec/frontend/monitoring/components/graph_group_spec.js144
-rw-r--r--spec/frontend/monitoring/components/group_empty_state_spec.js47
-rw-r--r--spec/frontend/monitoring/components/links_section_spec.js64
-rw-r--r--spec/frontend/monitoring/components/refresh_button_spec.js139
-rw-r--r--spec/frontend/monitoring/components/variables/dropdown_field_spec.js62
-rw-r--r--spec/frontend/monitoring/components/variables/text_field_spec.js55
-rw-r--r--spec/frontend/monitoring/components/variables_section_spec.js125
-rw-r--r--spec/frontend/monitoring/csv_export_spec.js126
-rw-r--r--spec/frontend/monitoring/fixture_data.js49
-rw-r--r--spec/frontend/monitoring/graph_data.js274
-rw-r--r--spec/frontend/monitoring/mock_data.js574
-rw-r--r--spec/frontend/monitoring/pages/dashboard_page_spec.js60
-rw-r--r--spec/frontend/monitoring/pages/panel_new_page_spec.js93
-rw-r--r--spec/frontend/monitoring/requests/index_spec.js157
-rw-r--r--spec/frontend/monitoring/router_spec.js106
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js1167
-rw-r--r--spec/frontend/monitoring/store/embed_group/actions_spec.js16
-rw-r--r--spec/frontend/monitoring/store/embed_group/getters_spec.js19
-rw-r--r--spec/frontend/monitoring/store/embed_group/mutations_spec.js16
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js457
-rw-r--r--spec/frontend/monitoring/store/index_spec.js23
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js586
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js893
-rw-r--r--spec/frontend/monitoring/store/variable_mapping_spec.js209
-rw-r--r--spec/frontend/monitoring/store_utils.js80
-rw-r--r--spec/frontend/monitoring/stubs/modal_stub.js11
-rw-r--r--spec/frontend/monitoring/utils_spec.js464
-rw-r--r--spec/frontend/monitoring/validators_spec.js80
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js51
-rw-r--r--spec/frontend/notes/components/comment_type_dropdown_spec.js40
-rw-r--r--spec/frontend/notes/components/diff_discussion_header_spec.js105
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js25
-rw-r--r--spec/frontend/notes/components/mr_discussion_filter_spec.js28
-rw-r--r--spec/frontend/notes/components/note_form_spec.js35
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js30
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js26
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js3
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js1
-rw-r--r--spec/frontend/notes/mock_data.js2
-rw-r--r--spec/frontend/notes/utils_spec.js31
-rw-r--r--spec/frontend/notifications/components/notification_email_listbox_input_spec.js2
-rw-r--r--spec/frontend/observability/client_spec.js66
-rw-r--r--spec/frontend/observability/observability_app_spec.js15
-rw-r--r--spec/frontend/observability/observability_container_spec.js134
-rw-r--r--spec/frontend/observability/skeleton_spec.js44
-rw-r--r--spec/frontend/organizations/groups_and_projects/components/app_spec.js99
-rw-r--r--spec/frontend/organizations/groups_and_projects/components/mock_data.js98
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js36
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js94
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js24
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/components/manifests_empty_state_spec.js81
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js143
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js40
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap21
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js82
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js26
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js16
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/list_spec.js6
-rw-r--r--spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js6
-rw-r--r--spec/frontend/pages/groups/new/components/app_spec.js5
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js13
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js45
-rw-r--r--spec/frontend/pipeline_wizard/components/commit_spec.js8
-rw-r--r--spec/frontend/pipeline_wizard/components/editor_spec.js46
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js252
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js236
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js54
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js114
-rw-r--r--spec/frontend/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row_spec.js140
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js56
-rw-r--r--spec/frontend/pipelines/graph/job_name_component_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js6
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js9
-rw-r--r--spec/frontend/pipelines/graph_shared/links_inner_spec.js1
-rw-r--r--spec/frontend/pipelines/header_component_spec.js246
-rw-r--r--spec/frontend/pipelines/mock_data.js8
-rw-r--r--spec/frontend/pipelines/pipeline_details_header_spec.js44
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js23
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js26
-rw-r--r--spec/frontend/pipelines/time_ago_spec.js13
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js7
-rw-r--r--spec/frontend/profile/components/follow_spec.js49
-rw-r--r--spec/frontend/profile/components/followers_tab_spec.js2
-rw-r--r--spec/frontend/profile/components/following_tab_spec.js108
-rw-r--r--spec/frontend/profile/components/profile_tabs_spec.js19
-rw-r--r--spec/frontend/profile/components/snippets/snippets_tab_spec.js43
-rw-r--r--spec/frontend/profile/mock_data.js1
-rw-r--r--spec/frontend/profile/preferences/components/profile_preferences_spec.js8
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js110
-rw-r--r--spec/frontend/projects/compare/components/app_spec.js104
-rw-r--r--spec/frontend/projects/new/components/__snapshots__/app_spec.js.snap30
-rw-r--r--spec/frontend/projects/new/components/app_spec.js6
-rw-r--r--spec/frontend/projects/settings/access_dropdown_spec.js25
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/index_spec.js1
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js10
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js24
-rw-r--r--spec/frontend/projects/terraform_notification/terraform_notification_spec.js3
-rw-r--r--spec/frontend/related_issues/components/related_issuable_input_spec.js98
-rw-r--r--spec/frontend/releases/components/releases_pagination_spec.js15
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap4
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js7
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js43
-rw-r--r--spec/frontend/repository/mixins/highlight_mixin_spec.js8
-rw-r--r--spec/frontend/scripts/frontend/po_to_json_spec.js8
-rw-r--r--spec/frontend/search/mock_data.js21
-rw-r--r--spec/frontend/search/sidebar/components/label_filter_spec.js30
-rw-r--r--spec/frontend/search/sidebar/components/scope_legacy_navigation_spec.js6
-rw-r--r--spec/frontend/search/sidebar/components/scope_sidebar_navigation_spec.js4
-rw-r--r--spec/frontend/search/topbar/components/app_spec.js16
-rw-r--r--spec/frontend/search/topbar/components/group_filter_spec.js5
-rw-r--r--spec/frontend/search/topbar/components/project_filter_spec.js5
-rw-r--r--spec/frontend/service_desk/components/info_banner_spec.js81
-rw-r--r--spec/frontend/service_desk/components/service_desk_list_app_spec.js151
-rw-r--r--spec/frontend/service_desk/mock_data.js118
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js96
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_title_spec.js21
-rw-r--r--spec/frontend/sidebar/components/assignees/assignees_spec.js5
-rw-r--r--spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js2
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js22
-rw-r--r--spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js8
-rw-r--r--spec/frontend/sidebar/components/participants/participants_spec.js13
-rw-r--r--spec/frontend/sidebar/components/reviewers/reviewer_avatar_link_spec.js66
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js11
-rw-r--r--spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js50
-rw-r--r--spec/frontend/sidebar/components/time_tracking/mock_data.js13
-rw-r--r--spec/frontend/sidebar/components/time_tracking/report_spec.js57
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap1
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js1
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js2
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap9
-rw-r--r--spec/frontend/snippets/components/snippet_visibility_edit_spec.js11
-rw-r--r--spec/frontend/super_sidebar/components/create_menu_spec.js15
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js93
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js43
-rw-r--r--spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js13
-rw-r--r--spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js175
-rw-r--r--spec/frontend/super_sidebar/components/global_search/mock_data.js14
-rw-r--r--spec/frontend/super_sidebar/components/help_center_spec.js6
-rw-r--r--spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js12
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js15
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js15
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js8
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js13
-rw-r--r--spec/frontend/super_sidebar/components/user_name_group_spec.js2
-rw-r--r--spec/frontend/super_sidebar/mock_data.js1
-rw-r--r--spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js32
-rw-r--r--spec/frontend/tags/components/delete_tag_modal_spec.js19
-rw-r--r--spec/frontend/token_access/outbound_token_access_spec.js125
-rw-r--r--spec/frontend/tracing/components/tracing_empty_state_spec.js44
-rw-r--r--spec/frontend/tracing/components/tracing_list_spec.js131
-rw-r--r--spec/frontend/tracing/components/tracing_table_list_spec.js63
-rw-r--r--spec/frontend/tracing/list_index_spec.js37
-rw-r--r--spec/frontend/tracking/internal_events_spec.js100
-rw-r--r--spec/frontend/tracking/tracking_spec.js1
-rw-r--r--spec/frontend/tracking/utils_spec.js37
-rw-r--r--spec/frontend/usage_quotas/storage/components/usage_graph_spec.js9
-rw-r--r--spec/frontend/usage_quotas/storage/mock_data.js12
-rw-r--r--spec/frontend/users/profile/actions/components/user_actions_app_spec.js38
-rw-r--r--spec/frontend/vue_compat_test_setup.js74
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_commit_message_dropdown_spec.js25
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_failed_to_merge_spec.js19
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js51
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap32
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js39
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js46
-rw-r--r--spec/frontend/vue_merge_request_widget/mock_data.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js208
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js65
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_status_spec.js120
-rw-r--r--spec/frontend/vue_shared/components/actions_button_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/awards_list_spec.js13
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/ci_icon_spec.js63
-rw-r--r--spec/frontend/vue_shared/components/code_block_highlighted_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/entity_select/entity_select_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/entity_select/group_select_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/entity_select/project_select_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js47
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js28
-rw-r--r--spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js100
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/markdown/header_spec.js112
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/markdown/non_gfm_markdown_spec.js157
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js73
-rw-r--r--spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js45
-rw-r--r--spec/frontend/vue_shared/components/projects_list/projects_list_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/registry/list_item_spec.js46
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap144
-rw-r--r--spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js104
-rw-r--r--spec/frontend/vue_shared/components/security_reports/help_icon_spec.js63
-rw-r--r--spec/frontend/vue_shared/components/security_reports/security_summary_spec.js33
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js11
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/plugins/wrap_lines_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/user_popover/user_popover_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js27
-rw-r--r--spec/frontend/vue_shared/issuable/list/mock_data.js4
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js25
-rw-r--r--spec/frontend/vue_shared/new_namespace/components/welcome_spec.js12
-rw-r--r--spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js9
-rw-r--r--spec/frontend/vue_shared/security_reports/mock_data.js136
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js267
-rw-r--r--spec/frontend/vue_shared/security_reports/store/getters_spec.js182
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js197
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js84
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js198
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js84
-rw-r--r--spec/frontend/vue_shared/security_reports/store/utils_spec.js63
-rw-r--r--spec/frontend/vue_shared/security_reports/utils_spec.js48
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js63
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js147
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js30
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js8
-rw-r--r--spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js107
-rw-r--r--spec/frontend/work_items/components/work_item_award_emoji_spec.js231
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js13
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js292
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js14
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js5
-rw-r--r--spec/frontend/work_items/components/work_item_todos_spec.js111
-rw-r--r--spec/frontend/work_items/mock_data.js258
-rw-r--r--spec/frontend/work_items/notes/award_utils_spec.js109
-rw-r--r--spec/frontend/work_items/router_spec.js39
-rw-r--r--spec/frontend/work_items/utils_spec.js21
478 files changed, 12380 insertions, 19566 deletions
diff --git a/spec/frontend/__helpers__/set_vue_error_handler.js b/spec/frontend/__helpers__/set_vue_error_handler.js
new file mode 100644
index 00000000000..d254630d1e4
--- /dev/null
+++ b/spec/frontend/__helpers__/set_vue_error_handler.js
@@ -0,0 +1,30 @@
+import Vue from 'vue';
+
+const modifiedInstances = [];
+
+export function setVueErrorHandler({ instance, handler }) {
+ if (Vue.version.startsWith('2')) {
+ // only global handlers are supported
+ const { config } = Vue;
+ config.errorHandler = handler;
+ return;
+ }
+
+ // eslint-disable-next-line no-param-reassign
+ instance.$.appContext.config.errorHandler = handler;
+ modifiedInstances.push(instance);
+}
+
+export function resetVueErrorHandler() {
+ if (Vue.version.startsWith('2')) {
+ const { config } = Vue;
+ config.errorHandler = null;
+ return;
+ }
+
+ modifiedInstances.forEach((instance) => {
+ // eslint-disable-next-line no-param-reassign
+ instance.$.appContext.config.errorHandler = null;
+ });
+ modifiedInstances.length = 0;
+}
diff --git a/spec/frontend/access_tokens/components/tokens_app_spec.js b/spec/frontend/access_tokens/components/tokens_app_spec.js
index 6e7dee6a2cc..59cc8e25414 100644
--- a/spec/frontend/access_tokens/components/tokens_app_spec.js
+++ b/spec/frontend/access_tokens/components/tokens_app_spec.js
@@ -43,8 +43,8 @@ describe('TokensApp', () => {
}) => {
const container = extendedWrapper(wrapper.findByTestId(testId));
- expect(container.findByText(expectedLabel, { selector: 'h4' }).exists()).toBe(true);
- expect(container.findByText(expectedDescription).exists()).toBe(true);
+ expect(container.findByText(expectedLabel).exists()).toBe(true);
+ expect(container.findByText(expectedDescription, { exact: false }).exists()).toBe(true);
expect(container.findByText(expectedInputDescription, { exact: false }).exists()).toBe(true);
expect(container.findByText('reset this token').attributes()).toMatchObject({
'data-confirm': expectedResetConfirmMessage,
diff --git a/spec/frontend/actioncable_connection_monitor_spec.js b/spec/frontend/actioncable_connection_monitor_spec.js
deleted file mode 100644
index c68eb53acde..00000000000
--- a/spec/frontend/actioncable_connection_monitor_spec.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import ConnectionMonitor from '~/actioncable_connection_monitor';
-
-describe('ConnectionMonitor', () => {
- let monitor;
-
- beforeEach(() => {
- monitor = new ConnectionMonitor({});
- });
-
- describe('#getPollInterval', () => {
- beforeEach(() => {
- Math.originalRandom = Math.random;
- });
- afterEach(() => {
- Math.random = Math.originalRandom;
- });
-
- const { staleThreshold, reconnectionBackoffRate } = ConnectionMonitor;
- const backoffFactor = 1 + reconnectionBackoffRate;
- const ms = 1000;
-
- it('uses exponential backoff', () => {
- Math.random = () => 0;
-
- monitor.reconnectAttempts = 0;
- expect(monitor.getPollInterval()).toEqual(staleThreshold * ms);
-
- monitor.reconnectAttempts = 1;
- expect(monitor.getPollInterval()).toEqual(staleThreshold * backoffFactor * ms);
-
- monitor.reconnectAttempts = 2;
- expect(monitor.getPollInterval()).toEqual(
- staleThreshold * backoffFactor * backoffFactor * ms,
- );
- });
-
- it('caps exponential backoff after some number of reconnection attempts', () => {
- Math.random = () => 0;
- monitor.reconnectAttempts = 42;
- const cappedPollInterval = monitor.getPollInterval();
-
- monitor.reconnectAttempts = 9001;
- expect(monitor.getPollInterval()).toEqual(cappedPollInterval);
- });
-
- it('uses 100% jitter when 0 reconnection attempts', () => {
- Math.random = () => 0;
- expect(monitor.getPollInterval()).toEqual(staleThreshold * ms);
-
- Math.random = () => 0.5;
- expect(monitor.getPollInterval()).toEqual(staleThreshold * 1.5 * ms);
- });
-
- it('uses reconnectionBackoffRate for jitter when >0 reconnection attempts', () => {
- monitor.reconnectAttempts = 1;
-
- Math.random = () => 0.25;
- expect(monitor.getPollInterval()).toEqual(
- staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.25) * ms,
- );
-
- Math.random = () => 0.5;
- expect(monitor.getPollInterval()).toEqual(
- staleThreshold * backoffFactor * (1 + reconnectionBackoffRate * 0.5) * ms,
- );
- });
-
- it('applies jitter after capped exponential backoff', () => {
- monitor.reconnectAttempts = 9001;
-
- Math.random = () => 0;
- const withoutJitter = monitor.getPollInterval();
- Math.random = () => 0.5;
- const withJitter = monitor.getPollInterval();
-
- expect(withJitter).toBeGreaterThan(withoutJitter);
- });
- });
-});
diff --git a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
index 27fe010c354..fa051f7a43a 100644
--- a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
+++ b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
@@ -45,14 +45,13 @@ describe('AddContextCommitsModal', () => {
...props,
},
});
- return wrapper;
};
const findModal = () => wrapper.findComponent(GlModal);
const findSearch = () => wrapper.findComponent(GlFilteredSearch);
beforeEach(() => {
- wrapper = createWrapper();
+ createWrapper();
});
it('renders modal with 2 tabs', () => {
@@ -98,7 +97,7 @@ describe('AddContextCommitsModal', () => {
});
it('enabled ok button when atleast one row is selected', async () => {
- wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
+ store.state.selectedCommits = [{ ...commit, isSelected: true }];
await nextTick();
expect(findModal().attributes('ok-disabled')).toBe(undefined);
});
@@ -106,14 +105,14 @@ describe('AddContextCommitsModal', () => {
describe('when in second tab, renders a modal with', () => {
beforeEach(() => {
- wrapper.vm.$store.state.tabIndex = 1;
+ store.state.tabIndex = 1;
});
it('a disabled ok button when no row is selected', () => {
expect(findModal().attributes('ok-disabled')).toBe('true');
});
it('an enabled ok button when atleast one row is selected', async () => {
- wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
+ store.state.selectedCommits = [{ ...commit, isSelected: true }];
await nextTick();
expect(findModal().attributes('ok-disabled')).toBe(undefined);
});
@@ -126,7 +125,7 @@ describe('AddContextCommitsModal', () => {
describe('has an ok button when clicked calls action', () => {
it('"createContextCommits" when only new commits to be added', async () => {
- wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
+ store.state.selectedCommits = [{ ...commit, isSelected: true }];
findModal().vm.$emit('ok');
await nextTick();
expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
@@ -135,14 +134,14 @@ describe('AddContextCommitsModal', () => {
});
});
it('"removeContextCommits" when only added commits are to be removed', async () => {
- wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
+ store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
await nextTick();
expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), true);
});
it('"createContextCommits" and "removeContextCommits" when new commits are to be added and old commits are to be removed', async () => {
- wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
- wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
+ store.state.selectedCommits = [{ ...commit, isSelected: true }];
+ store.state.toRemoveCommits = [commit.short_id];
findModal().vm.$emit('ok');
await nextTick();
expect(createContextCommits).toHaveBeenCalledWith(expect.anything(), {
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_category_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_category_spec.js
new file mode 100644
index 00000000000..456df3b1857
--- /dev/null
+++ b/spec/frontend/admin/abuse_reports/components/abuse_category_spec.js
@@ -0,0 +1,43 @@
+import { GlLabel } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import AbuseCategory from '~/admin/abuse_reports/components/abuse_category.vue';
+import { ABUSE_CATEGORIES } from '~/admin/abuse_reports/constants';
+import { mockAbuseReports } from '../mock_data';
+
+describe('AbuseCategory', () => {
+ let wrapper;
+
+ const mockAbuseReport = mockAbuseReports[0];
+ const category = ABUSE_CATEGORIES[mockAbuseReport.category];
+
+ const findLabel = () => wrapper.findComponent(GlLabel);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(AbuseCategory, {
+ propsData: {
+ category: mockAbuseReport.category,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders a label', () => {
+ expect(findLabel().exists()).toBe(true);
+ });
+
+ it('renders the label with the right background color for the category', () => {
+ expect(findLabel().props()).toMatchObject({
+ backgroundColor: category.backgroundColor,
+ title: category.title,
+ target: null,
+ });
+ });
+
+ it('renders the label with the right text color for the category', () => {
+ expect(findLabel().attributes('class')).toBe(`gl-text-${category.color}`);
+ });
+});
diff --git a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
index f3cced81478..03bf510f3ad 100644
--- a/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
+++ b/spec/frontend/admin/abuse_reports/components/abuse_report_row_spec.js
@@ -1,6 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import AbuseReportRow from '~/admin/abuse_reports/components/abuse_report_row.vue';
+import AbuseCategory from '~/admin/abuse_reports/components/abuse_category.vue';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
import { SORT_UPDATED_AT } from '~/admin/abuse_reports/constants';
@@ -11,7 +12,8 @@ describe('AbuseReportRow', () => {
const mockAbuseReport = mockAbuseReports[0];
const findListItem = () => wrapper.findComponent(ListItem);
- const findTitle = () => wrapper.findByTestId('title');
+ const findAbuseCategory = () => wrapper.findComponent(AbuseCategory);
+ const findAbuseReportTitle = () => wrapper.findByTestId('abuse-report-title');
const findDisplayedDate = () => wrapper.findByTestId('abuse-report-date');
const createComponent = (props = {}) => {
@@ -35,13 +37,13 @@ describe('AbuseReportRow', () => {
const { reporter, reportedUser, category, reportPath } = mockAbuseReport;
it('displays correctly formatted title', () => {
- expect(findTitle().text()).toMatchInterpolatedText(
+ expect(findAbuseReportTitle().text()).toMatchInterpolatedText(
`${reportedUser.name} reported for ${category} by ${reporter.name}`,
);
});
it('links to the details page', () => {
- expect(findTitle().attributes('href')).toEqual(reportPath);
+ expect(findAbuseReportTitle().attributes('href')).toEqual(reportPath);
});
describe('when the reportedUser is missing', () => {
@@ -50,7 +52,7 @@ describe('AbuseReportRow', () => {
});
it('displays correctly formatted title', () => {
- expect(findTitle().text()).toMatchInterpolatedText(
+ expect(findAbuseReportTitle().text()).toMatchInterpolatedText(
`Deleted user reported for ${category} by ${reporter.name}`,
);
});
@@ -62,7 +64,7 @@ describe('AbuseReportRow', () => {
});
it('displays correctly formatted title', () => {
- expect(findTitle().text()).toMatchInterpolatedText(
+ expect(findAbuseReportTitle().text()).toMatchInterpolatedText(
`${reportedUser.name} reported for ${category} by Deleted user`,
);
});
@@ -88,4 +90,8 @@ describe('AbuseReportRow', () => {
});
});
});
+
+ it('renders abuse category', () => {
+ expect(findAbuseCategory().exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/admin/applications/components/delete_application_spec.js b/spec/frontend/admin/applications/components/delete_application_spec.js
index 315c38a2bbc..e0282b8c149 100644
--- a/spec/frontend/admin/applications/components/delete_application_spec.js
+++ b/spec/frontend/admin/applications/components/delete_application_spec.js
@@ -1,6 +1,7 @@
import { GlModal, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { stubComponent } from 'helpers/stub_component';
import DeleteApplication from '~/admin/applications/components/delete_application.vue';
const path = 'application/path/1';
@@ -14,6 +15,11 @@ describe('DeleteApplication', () => {
const createComponent = () => {
wrapper = shallowMount(DeleteApplication, {
stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: jest.fn(),
+ },
+ }),
GlSprintf,
},
});
@@ -36,7 +42,6 @@ describe('DeleteApplication', () => {
describe('the modal component', () => {
beforeEach(() => {
- wrapper.vm.$refs.deleteModal.show = jest.fn();
document.querySelector('.js-application-delete-button').click();
});
diff --git a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
index dca77e67cac..b937a58a742 100644
--- a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
+++ b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
@@ -5,7 +5,12 @@ import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status';
import MessageForm from '~/admin/broadcast_messages/components/message_form.vue';
-import { TYPE_BANNER, TYPE_NOTIFICATION, THEMES } from '~/admin/broadcast_messages/constants';
+import {
+ TYPE_BANNER,
+ TYPE_NOTIFICATION,
+ THEMES,
+ TARGET_OPTIONS,
+} from '~/admin/broadcast_messages/constants';
import waitForPromises from 'helpers/wait_for_promises';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { MOCK_TARGET_ACCESS_LEVELS } from '../mock_data';
@@ -37,6 +42,8 @@ describe('MessageForm', () => {
const findCancelButton = () => wrapper.findComponent('[data-testid=cancel-button]');
const findForm = () => wrapper.findComponent(GlForm);
const findShowInCli = () => wrapper.findComponent('[data-testid=show-in-cli-checkbox]');
+ const findTargetSelect = () => wrapper.findComponent('[data-testid=target-select]');
+ const findTargetPath = () => wrapper.findComponent('[data-testid=target-path-input]');
function createComponent({ broadcastMessage = {} } = {}) {
wrapper = mount(MessageForm, {
@@ -112,10 +119,38 @@ describe('MessageForm', () => {
});
});
- describe('target roles checkboxes', () => {
- it('renders target roles', () => {
+ describe('target select', () => {
+ it('renders the first option and hide target path and target roles when creating message', () => {
createComponent();
- expect(findTargetRoles().exists()).toBe(true);
+ expect(findTargetSelect().element.value).toBe(TARGET_OPTIONS[0].value);
+ expect(findTargetRoles().isVisible()).toBe(false);
+ expect(findTargetPath().isVisible()).toBe(false);
+ });
+
+ it('triggers displaying target path and target roles when selecting different options', async () => {
+ createComponent();
+ const options = findTargetSelect().findAll('option');
+ await options.at(1).setSelected();
+ expect(findTargetPath().isVisible()).toBe(true);
+ expect(findTargetRoles().isVisible()).toBe(false);
+
+ await options.at(2).setSelected();
+ expect(findTargetPath().isVisible()).toBe(true);
+ expect(findTargetRoles().isVisible()).toBe(true);
+ });
+
+ it('renders the second option and hide target roles when editing message with path specified', () => {
+ createComponent({ broadcastMessage: { targetPath: '/welcome' } });
+ expect(findTargetSelect().element.value).toBe(TARGET_OPTIONS[1].value);
+ expect(findTargetRoles().isVisible()).toBe(false);
+ expect(findTargetPath().isVisible()).toBe(true);
+ });
+
+ it('renders the third option when editing message with path and roles specified', () => {
+ createComponent({ broadcastMessage: { targetPath: '/welcome', targetAccessLevels: [20] } });
+ expect(findTargetSelect().element.value).toBe(TARGET_OPTIONS[2].value);
+ expect(findTargetRoles().isVisible()).toBe(true);
+ expect(findTargetPath().isVisible()).toBe(true);
});
});
diff --git a/spec/frontend/admin/topics/components/remove_avatar_spec.js b/spec/frontend/admin/topics/components/remove_avatar_spec.js
index c069203d046..705066c3ef0 100644
--- a/spec/frontend/admin/topics/components/remove_avatar_spec.js
+++ b/spec/frontend/admin/topics/components/remove_avatar_spec.js
@@ -73,7 +73,7 @@ describe('RemoveAvatar', () => {
let formSubmitSpy;
beforeEach(() => {
- formSubmitSpy = jest.spyOn(wrapper.vm.$refs.deleteForm, 'submit');
+ formSubmitSpy = jest.spyOn(findForm().element, 'submit');
findModal().vm.$emit('primary');
});
diff --git a/spec/frontend/admin/topics/components/topic_select_spec.js b/spec/frontend/admin/topics/components/topic_select_spec.js
index 113a0e3d404..5b7e6365606 100644
--- a/spec/frontend/admin/topics/components/topic_select_spec.js
+++ b/spec/frontend/admin/topics/components/topic_select_spec.js
@@ -58,10 +58,6 @@ describe('TopicSelect', () => {
});
}
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it('mounts', () => {
createComponent();
diff --git a/spec/frontend/alert_management/components/alert_management_table_spec.js b/spec/frontend/alert_management/components/alert_management_table_spec.js
index afd88e1a6ac..9980843defb 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -186,7 +186,7 @@ describe('AlertManagementTable', () => {
expect(findSeverityFields().at(0).text()).toBe('Critical');
});
- it('renders Unassigned when no assignee(s) present', () => {
+ it('renders Unassigned when no assignees present', () => {
mountComponent({
data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
loading: false,
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index 4a0c7f65493..e6b38a1e824 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -68,8 +68,11 @@ describe('AlertsSettingsForm', () => {
await options.at(index).setSelected();
};
- const enableIntegration = (index, value) => {
- findFormFields().at(index).setValue(value);
+ const enableIntegration = (index, value = '') => {
+ if (value !== '') {
+ findFormFields().at(index).setValue(value);
+ }
+
findFormToggle().vm.$emit('change', true);
};
@@ -100,7 +103,8 @@ describe('AlertsSettingsForm', () => {
it('hides the name input when the selected value is prometheus', async () => {
createComponent();
await selectOptionAtIndex(2);
- expect(findFormFields().at(0).attributes('id')).not.toBe('name-integration');
+
+ expect(findFormFields()).toHaveLength(0);
});
it('verify pricing link url', () => {
@@ -203,8 +207,8 @@ describe('AlertsSettingsForm', () => {
it('create', async () => {
createComponent();
await selectOptionAtIndex(2);
- const apiUrl = 'https://test.com';
- enableIntegration(0, apiUrl);
+ enableIntegration(0);
+
const submitBtn = findSubmitButton();
expect(submitBtn.exists()).toBe(true);
expect(submitBtn.text()).toBe('Save integration');
@@ -213,14 +217,14 @@ describe('AlertsSettingsForm', () => {
expect(wrapper.emitted('create-new-integration')[0][0]).toMatchObject({
type: typeSet.prometheus,
- variables: { apiUrl, active: true },
+ variables: { active: true },
});
});
it('update', () => {
createComponent({
data: {
- integrationForm: { id: '1', apiUrl: 'https://test-pre.com', type: typeSet.prometheus },
+ integrationForm: { id: '1', type: typeSet.prometheus },
currentIntegration: { id: '1' },
},
props: {
@@ -228,8 +232,7 @@ describe('AlertsSettingsForm', () => {
},
});
- const apiUrl = 'https://test-post.com';
- enableIntegration(0, apiUrl);
+ enableIntegration(0);
const submitBtn = findSubmitButton();
expect(submitBtn.exists()).toBe(true);
@@ -239,7 +242,7 @@ describe('AlertsSettingsForm', () => {
expect(wrapper.emitted('update-integration')[0][0]).toMatchObject({
type: typeSet.prometheus,
- variables: { apiUrl, active: true },
+ variables: { active: true },
});
});
});
@@ -442,16 +445,8 @@ describe('AlertsSettingsForm', () => {
expect(findSubmitButton().attributes('disabled')).toBe(undefined);
});
- it('should not be able to submit when Prometheus integration form is invalid', async () => {
- await selectOptionAtIndex(2);
- await findFormFields().at(0).vm.$emit('input', '');
-
- expect(findSubmitButton().attributes('disabled')).toBeDefined();
- });
-
it('should be able to submit when Prometheus integration form is valid', async () => {
await selectOptionAtIndex(2);
- await findFormFields().at(0).vm.$emit('input', 'http://valid.url');
expect(findSubmitButton().attributes('disabled')).toBe(undefined);
});
diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
index 4e0b546b3d2..802da47d6cd 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -57,7 +57,6 @@ describe('ProjectsDropdownFilter component', () => {
});
};
- const findClearAllButton = () => wrapper.findByTestId('listbox-reset-button');
const findSelectedProjectsLabel = () => wrapper.findComponent(GlTruncate);
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
@@ -143,10 +142,6 @@ describe('ProjectsDropdownFilter component', () => {
expect(findSelectedProjectsLabel().text()).toBe('Select projects');
});
-
- it('does not render the clear all button', () => {
- expect(findClearAllButton().exists()).toBe(false);
- });
});
describe('with a selected project', () => {
@@ -169,12 +164,6 @@ describe('ProjectsDropdownFilter component', () => {
expect(findSelectedProjectsLabel().text()).toBe(projects[0].name);
});
- it('renders the clear all button', async () => {
- await selectDropdownItemAtIndex([0], false);
-
- expect(findClearAllButton().exists()).toBe(true);
- });
-
it('clears all selected items when the clear all button is clicked', async () => {
createComponent({
mountFn: mountExtended,
@@ -186,7 +175,7 @@ describe('ProjectsDropdownFilter component', () => {
expect(findSelectedProjectsLabel().text()).toBe('2 projects selected');
- await findClearAllButton().vm.$emit('click');
+ await findDropdown().vm.$emit('reset');
expect(findSelectedProjectsLabel().text()).toBe('Select projects');
});
diff --git a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
index 20836d7cc70..8638d82ae3c 100644
--- a/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
+++ b/spec/frontend/analytics/usage_trends/components/users_chart_spec.js
@@ -22,23 +22,19 @@ describe('UsersChart', () => {
let queryHandler;
const createComponent = ({
- loadingError = false,
- loading = false,
users = [],
additionalData = [],
+ handler = mockQueryResponse({ key: 'users', data: users, additionalData }),
} = {}) => {
- queryHandler = mockQueryResponse({ key: 'users', data: users, loading, additionalData });
+ queryHandler = handler;
- return shallowMount(UsersChart, {
+ wrapper = shallowMount(UsersChart, {
+ apolloProvider: createMockApollo([[usersQuery, queryHandler]]),
props: {
startDate: new Date(2020, 9, 26),
endDate: new Date(2020, 10, 1),
totalDataPoints: mockCountsData2.length,
},
- apolloProvider: createMockApollo([[usersQuery, queryHandler]]),
- data() {
- return { loadingError };
- },
});
};
@@ -48,7 +44,7 @@ describe('UsersChart', () => {
describe('while loading', () => {
beforeEach(() => {
- wrapper = createComponent({ loading: true });
+ createComponent({ loading: true });
});
it('displays the skeleton loader', () => {
@@ -62,7 +58,7 @@ describe('UsersChart', () => {
describe('without data', () => {
beforeEach(async () => {
- wrapper = createComponent({ users: [] });
+ createComponent({ users: [] });
await nextTick();
});
@@ -81,7 +77,7 @@ describe('UsersChart', () => {
describe('with data', () => {
beforeEach(async () => {
- wrapper = createComponent({ users: mockCountsData2 });
+ createComponent({ users: mockCountsData2 });
await waitForPromises();
});
@@ -102,11 +98,17 @@ describe('UsersChart', () => {
describe('with errors', () => {
beforeEach(async () => {
- wrapper = createComponent({ loadingError: true });
+ createComponent();
await nextTick();
});
- it('renders an error message', () => {
+ it('renders an error message', async () => {
+ createComponent({
+ handler: jest.fn().mockRejectedValue({}),
+ });
+
+ await waitForPromises();
+
expect(findAlert().text()).toBe(
'Could not load the user chart. Please refresh the page to try again.',
);
@@ -124,42 +126,37 @@ describe('UsersChart', () => {
describe('when fetching more data', () => {
describe('when the fetchMore query returns data', () => {
beforeEach(async () => {
- wrapper = createComponent({
+ createComponent({
users: mockCountsData2,
additionalData: mockCountsData1,
});
- jest.spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore');
await nextTick();
});
it('requests data twice', () => {
expect(queryHandler).toHaveBeenCalledTimes(2);
});
-
- it('calls fetchMore', () => {
- expect(wrapper.vm.$apollo.queries.users.fetchMore).toHaveBeenCalledTimes(1);
- });
});
describe('when the fetchMore query throws an error', () => {
beforeEach(async () => {
- wrapper = createComponent({
+ createComponent({
users: mockCountsData2,
additionalData: mockCountsData1,
});
- jest
- .spyOn(wrapper.vm.$apollo.queries.users, 'fetchMore')
- .mockImplementation(jest.fn().mockRejectedValue());
await waitForPromises();
});
it('calls fetchMore', () => {
- expect(wrapper.vm.$apollo.queries.users.fetchMore).toHaveBeenCalledTimes(1);
+ expect(queryHandler).toHaveBeenCalledTimes(2);
});
- it('renders an error message', () => {
+ it('renders an error message', async () => {
+ createComponent({ handler: jest.fn().mockRejectedValue({}) });
+ await waitForPromises();
+
expect(findAlert().text()).toBe(
'Could not load the user chart. Please refresh the page to try again.',
);
diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js
index b2ecfeb8394..a6e08e1cf4b 100644
--- a/spec/frontend/api/user_api_spec.js
+++ b/spec/frontend/api/user_api_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import projects from 'test_fixtures/api/users/projects/get.json';
import followers from 'test_fixtures/api/users/followers/get.json';
+import following from 'test_fixtures/api/users/following/get.json';
import {
followUser,
unfollowUser,
@@ -9,6 +10,7 @@ import {
updateUserStatus,
getUserProjects,
getUserFollowers,
+ getUserFollowing,
} from '~/api/user_api';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -131,4 +133,23 @@ describe('~/api/user_api', () => {
expect(axiosMock.history.get[0].params).toEqual({ ...params, per_page: DEFAULT_PER_PAGE });
});
});
+
+ describe('getUserFollowing', () => {
+ it('calls correct URL and returns expected response', async () => {
+ const MOCK_USER_ID = 1;
+ const MOCK_PAGE = 2;
+
+ const expectedUrl = `/api/v4/users/${MOCK_USER_ID}/following`;
+ const expectedResponse = { data: following };
+ const params = { page: MOCK_PAGE };
+
+ axiosMock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse);
+
+ await expect(getUserFollowing(MOCK_USER_ID, params)).resolves.toEqual(
+ expect.objectContaining({ data: expectedResponse }),
+ );
+ expect(axiosMock.history.get[0].url).toBe(expectedUrl);
+ expect(axiosMock.history.get[0].params).toEqual({ ...params, per_page: DEFAULT_PER_PAGE });
+ });
+ });
});
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index 159e36c1364..b6042b4aa81 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -41,9 +41,11 @@ describe('Batch comments draft note component', () => {
},
});
- jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation();
+ jest.spyOn(store, 'dispatch').mockImplementation();
};
+ const findNoteableNote = () => wrapper.findComponent(NoteableNote);
+
beforeEach(() => {
store = createStore();
draft = createDraft();
@@ -53,32 +55,28 @@ describe('Batch comments draft note component', () => {
createComponent();
expect(wrapper.findComponent(GlBadge).exists()).toBe(true);
- const note = wrapper.findComponent(NoteableNote);
-
- expect(note.exists()).toBe(true);
- expect(note.props().note).toEqual(draft);
+ expect(findNoteableNote().exists()).toBe(true);
+ expect(findNoteableNote().props('note')).toEqual(draft);
});
describe('update', () => {
it('dispatches updateDraft', async () => {
createComponent();
- const note = wrapper.findComponent(NoteableNote);
-
- note.vm.$emit('handleEdit');
+ findNoteableNote().vm.$emit('handleEdit');
await nextTick();
const formData = {
note: draft,
noteText: 'a',
resolveDiscussion: false,
+ callback: jest.fn(),
+ parentElement: wrapper.vm.$el,
+ errorCallback: jest.fn(),
};
- note.vm.$emit('handleUpdateNote', formData);
+ findNoteableNote().vm.$emit('handleUpdateNote', formData);
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith(
- 'batchComments/updateDraft',
- formData,
- );
+ expect(store.dispatch).toHaveBeenCalledWith('batchComments/updateDraft', formData);
});
});
@@ -87,18 +85,15 @@ describe('Batch comments draft note component', () => {
createComponent();
jest.spyOn(window, 'confirm').mockImplementation(() => true);
- const note = wrapper.findComponent(NoteableNote);
-
- note.vm.$emit('handleDeleteNote', draft);
+ findNoteableNote().vm.$emit('handleDeleteNote', draft);
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('batchComments/deleteDraft', draft);
+ expect(store.dispatch).toHaveBeenCalledWith('batchComments/deleteDraft', draft);
});
});
describe('quick actions', () => {
it('renders referenced commands', async () => {
- createComponent();
- wrapper.setProps({
+ createComponent({
draft: {
...draft,
references: {
@@ -116,22 +111,27 @@ describe('Batch comments draft note component', () => {
});
describe('multiline comments', () => {
- describe.each`
- desc | props | event | expectedCalls
- ${'with `draft.position`'} | ${draftWithLineRange} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]}
- ${'with `draft.position`'} | ${draftWithLineRange} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]}
- ${'without `draft.position`'} | ${{}} | ${'mouseenter'} | ${[]}
- ${'without `draft.position`'} | ${{}} | ${'mouseleave'} | ${[]}
- `('$desc', ({ props, event, expectedCalls }) => {
- beforeEach(() => {
- createComponent({ draft: { ...draft, ...props } });
- jest.spyOn(store, 'dispatch');
- });
+ it(`calls store with draft.position with mouseenter`, () => {
+ createComponent({ draft: { ...draft, ...draftWithLineRange } });
+ findNoteableNote().trigger('mouseenter');
- it(`calls store ${expectedCalls.length} times on ${event}`, () => {
- wrapper.element.dispatchEvent(new MouseEvent(event, { bubbles: true }));
- expect(store.dispatch.mock.calls).toEqual(expectedCalls);
- });
+ expect(store.dispatch).toHaveBeenCalledWith('setSelectedCommentPositionHover', LINE_RANGE);
+ });
+
+ it(`calls store with draft.position and mouseleave`, () => {
+ createComponent({ draft: { ...draft, ...draftWithLineRange } });
+ findNoteableNote().trigger('mouseleave');
+
+ expect(store.dispatch).toHaveBeenCalledWith('setSelectedCommentPositionHover');
+ });
+
+ it(`does not call store without draft position`, () => {
+ createComponent({ draft });
+
+ findNoteableNote().trigger('mouseenter');
+ findNoteableNote().trigger('mouseleave');
+
+ expect(store.dispatch).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
index 5c33df882bf..7e2ff7f786f 100644
--- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import SubmitDropdown from '~/batch_comments/components/submit_dropdown.vue';
+import { mockTracking } from 'helpers/tracking_helper';
jest.mock('~/autosave');
@@ -10,9 +11,11 @@ Vue.use(Vuex);
let wrapper;
let publishReview;
+let trackingSpy;
function factory({ canApprove = true, shouldAnimateReviewButton = false } = {}) {
publishReview = jest.fn();
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
const store = new Vuex.Store({
getters: {
@@ -69,6 +72,20 @@ describe('Batch comments submit dropdown', () => {
});
});
+ it('tracks submit action', () => {
+ factory();
+
+ findCommentTextarea().setValue('Hello world');
+
+ findForm().vm.$emit('submit', { preventDefault: jest.fn() });
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context: 'MergeRequest_review',
+ editorType: 'editor_type_plain_text_editor',
+ label: 'editor_tracking',
+ });
+ });
+
it('switches to the overview tab after submit', async () => {
window.mrTabs = { tabShown: jest.fn() };
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
index 521bbf06b02..824b2a296c6 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
@@ -1,10 +1,15 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
+import { sprintf } from '~/locale';
+import { createAlert } from '~/alert';
import service from '~/batch_comments/services/drafts_service';
import * as actions from '~/batch_comments/stores/modules/batch_comments/actions';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { UPDATE_COMMENT_FORM } from '~/notes/i18n';
+
+jest.mock('~/alert');
describe('Batch comments store actions', () => {
let res = {};
@@ -44,15 +49,15 @@ describe('Batch comments store actions', () => {
});
it('does not commit ADD_NEW_DRAFT if errors returned', () => {
+ const commit = jest.fn();
+
mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- return testAction(
- actions.addDraftToDiscussion,
- { endpoint: TEST_HOST, data: 'test' },
- null,
- [],
- [],
- );
+ return actions
+ .addDraftToDiscussion({ commit }, { endpoint: TEST_HOST, data: 'test' })
+ .catch(() => {
+ expect(commit).not.toHaveBeenCalledWith('ADD_NEW_DRAFT', expect.anything());
+ });
});
});
@@ -84,15 +89,13 @@ describe('Batch comments store actions', () => {
});
it('does not commit ADD_NEW_DRAFT if errors returned', () => {
+ const commit = jest.fn();
+
mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- return testAction(
- actions.createNewDraft,
- { endpoint: TEST_HOST, data: 'test' },
- null,
- [],
- [],
- );
+ return actions.createNewDraft({ commit }, { endpoint: TEST_HOST, data: 'test' }).catch(() => {
+ expect(commit).not.toHaveBeenCalledWith('ADD_NEW_DRAFT', expect.anything());
+ });
});
});
@@ -239,8 +242,6 @@ describe('Batch comments store actions', () => {
params = { note: { id: 1 }, noteText: 'test' };
});
- afterEach(() => jest.clearAllMocks());
-
it('commits RECEIVE_DRAFT_UPDATE_SUCCESS with returned data', () => {
return actions.updateDraft(context, { ...params, callback() {} }).then(() => {
expect(commit).toHaveBeenCalledWith('RECEIVE_DRAFT_UPDATE_SUCCESS', { id: 1 });
@@ -267,6 +268,28 @@ describe('Batch comments store actions', () => {
expect(service.update.mock.calls[0][1].position).toBe(expectation);
});
});
+
+ describe('when updating a draft returns an error', () => {
+ const errorCallback = jest.fn();
+ const flashContainer = null;
+ const error = 'server error';
+
+ beforeEach(async () => {
+ service.update.mockRejectedValue({ response: { data: { errors: error } } });
+ await actions.updateDraft(context, { ...params, flashContainer, errorCallback });
+ });
+
+ it('renders an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: sprintf(UPDATE_COMMENT_FORM.error, { reason: error }),
+ parent: flashContainer,
+ });
+ });
+
+ it('calls errorCallback', () => {
+ expect(errorCallback).toHaveBeenCalledTimes(1);
+ });
+ });
});
describe('expandAllDiscussions', () => {
diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js
index 995e4219ae3..c7f4fce0e4c 100644
--- a/spec/frontend/behaviors/gl_emoji_spec.js
+++ b/spec/frontend/behaviors/gl_emoji_spec.js
@@ -1,11 +1,18 @@
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import waitForPromises from 'helpers/wait_for_promises';
+import { createMockClient } from 'helpers/mock_apollo_helper';
import installGlEmojiElement from '~/behaviors/gl_emoji';
import { EMOJI_VERSION } from '~/emoji';
+import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql';
import * as EmojiUnicodeSupport from '~/emoji/support';
+let mockClient;
+
jest.mock('~/emoji/support');
+jest.mock('~/lib/graphql', () => {
+ return () => mockClient;
+});
describe('gl_emoji', () => {
const emojiData = {
@@ -36,101 +43,144 @@ describe('gl_emoji', () => {
return div.firstElementChild;
}
- beforeEach(async () => {
- await initEmojiMock(emojiData);
- });
-
afterEach(() => {
clearEmojiMock();
document.body.innerHTML = '';
});
- describe.each([
- [
- 'bomb emoji just with name attribute',
- '<gl-emoji data-name="bomb"></gl-emoji>',
- '<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
- `<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
- ],
- [
- 'bomb emoji with name attribute and unicode version',
- '<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
- '<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
- `<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
- ],
- [
- 'bomb emoji with sprite fallback',
- '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
- '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
- '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb" class="emoji-icon emoji-bomb">💣</gl-emoji>',
- ],
- [
- 'bomb emoji with image fallback',
- '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb"></gl-emoji>',
- '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
- '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/bomb.png" width="16" height="16" align="absmiddle"></gl-emoji>',
- ],
- [
- 'invalid emoji',
- '<gl-emoji data-name="invalid_emoji"></gl-emoji>',
- '<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament">❔</gl-emoji>',
- `<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" width="16" height="16" align="absmiddle"></gl-emoji>`,
- ],
- [
- 'custom emoji with image fallback',
- '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"></gl-emoji>',
- '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="16" height="16" align="absmiddle"></gl-emoji>',
- '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" width="16" height="16" align="absmiddle"></gl-emoji>',
- ],
- ])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => {
- it(`renders correctly with emoji support`, async () => {
- jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true);
- const glEmojiElement = markupToDomElement(markup);
+ describe('standard emoji', () => {
+ beforeEach(async () => {
+ await initEmojiMock(emojiData);
+ });
+
+ describe.each([
+ [
+ 'bomb emoji just with name attribute',
+ '<gl-emoji data-name="bomb"></gl-emoji>',
+ '<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
+ `<gl-emoji data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" align="absmiddle"></gl-emoji>`,
+ ],
+ [
+ 'bomb emoji with name attribute and unicode version',
+ '<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
+ '<gl-emoji data-name="bomb" data-unicode-version="6.0">💣</gl-emoji>',
+ `<gl-emoji data-name="bomb" data-unicode-version="6.0"><img class="emoji" title=":bomb:" alt=":bomb:" src="/-/emojis/${EMOJI_VERSION}/bomb.png" align="absmiddle"></gl-emoji>`,
+ ],
+ [
+ 'bomb emoji with sprite fallback',
+ '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
+ '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
+ '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb" data-unicode-version="6.0" title="bomb" class="emoji-icon emoji-bomb">💣</gl-emoji>',
+ ],
+ [
+ 'bomb emoji with image fallback',
+ '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb"></gl-emoji>',
+ '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb">💣</gl-emoji>',
+ '<gl-emoji data-fallback-src="/bomb.png" data-name="bomb" data-unicode-version="6.0" title="bomb"><img class="emoji" title=":bomb:" alt=":bomb:" src="/bomb.png" align="absmiddle"></gl-emoji>',
+ ],
+ [
+ 'invalid emoji',
+ '<gl-emoji data-name="invalid_emoji"></gl-emoji>',
+ '<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament">❔</gl-emoji>',
+ `<gl-emoji data-name="grey_question" data-unicode-version="6.0" title="white question mark ornament"><img class="emoji" title=":grey_question:" alt=":grey_question:" src="/-/emojis/${EMOJI_VERSION}/grey_question.png" align="absmiddle"></gl-emoji>`,
+ ],
+ [
+ 'custom emoji with image fallback',
+ '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"></gl-emoji>',
+ '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" align="absmiddle"></gl-emoji>',
+ '<gl-emoji data-fallback-src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" data-name="party-parrot" data-unicode-version="custom"><img class="emoji" title=":party-parrot:" alt=":party-parrot:" src="https://cultofthepartyparrot.com/parrots/hd/parrot.gif" align="absmiddle"></gl-emoji>',
+ ],
+ ])('%s', (name, markup, withEmojiSupport, withoutEmojiSupport) => {
+ it(`renders correctly with emoji support`, async () => {
+ jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(true);
+ const glEmojiElement = markupToDomElement(markup);
+
+ await waitForPromises();
+
+ expect(glEmojiElement.outerHTML).toBe(withEmojiSupport);
+ });
+
+ it(`renders correctly without emoji support`, async () => {
+ jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false);
+ const glEmojiElement = markupToDomElement(markup);
+
+ await waitForPromises();
+
+ expect(glEmojiElement.outerHTML).toBe(withoutEmojiSupport);
+ });
+ });
+
+ it('escapes gl-emoji name', async () => {
+ const glEmojiElement = markupToDomElement(
+ "<gl-emoji data-name='&#34;x=&#34y&#34 onload=&#34;alert(document.location.href)&#34;' data-unicode-version='x'>abc</gl-emoji>",
+ );
await waitForPromises();
- expect(glEmojiElement.outerHTML).toBe(withEmojiSupport);
+ expect(glEmojiElement.outerHTML).toBe(
+ '<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/2/grey_question.png" align="absmiddle"></gl-emoji>',
+ );
});
- it(`renders correctly without emoji support`, async () => {
+ it('Adds sprite CSS if emojis are not supported', async () => {
+ const testPath = '/test-path.css';
jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false);
- const glEmojiElement = markupToDomElement(markup);
+ window.gon.emoji_sprites_css_path = testPath;
+ expect(document.head.querySelector(`link[href="${testPath}"]`)).toBe(null);
+ expect(window.gon.emoji_sprites_css_added).toBe(undefined);
+
+ markupToDomElement(
+ '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
+ );
await waitForPromises();
- expect(glEmojiElement.outerHTML).toBe(withoutEmojiSupport);
+ expect(document.head.querySelector(`link[href="${testPath}"]`).outerHTML).toBe(
+ '<link rel="stylesheet" href="/test-path.css">',
+ );
+ expect(window.gon.emoji_sprites_css_added).toBe(true);
});
});
- it('escapes gl-emoji name', async () => {
- const glEmojiElement = markupToDomElement(
- "<gl-emoji data-name='&#34;x=&#34y&#34 onload=&#34;alert(document.location.href)&#34;' data-unicode-version='x'>abc</gl-emoji>",
- );
-
- await waitForPromises();
+ describe('custom emoji', () => {
+ beforeEach(async () => {
+ mockClient = createMockClient([
+ [
+ customEmojiQuery,
+ jest.fn().mockResolvedValue({
+ data: {
+ group: {
+ id: 1,
+ customEmoji: {
+ nodes: [{ id: 1, name: 'parrot', url: 'parrot.gif' }],
+ },
+ },
+ },
+ }),
+ ],
+ ]);
+
+ window.gon = { features: { customEmoji: true } };
+ document.body.dataset.groupFullPath = 'test-group';
+
+ await initEmojiMock(emojiData);
+ });
- expect(glEmojiElement.outerHTML).toBe(
- '<gl-emoji data-name="&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;" data-unicode-version="x"><img class="emoji" title=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" alt=":&quot;x=&quot;y&quot; onload=&quot;alert(document.location.href)&quot;:" src="/-/emojis/2/grey_question.png" width="16" height="16" align="absmiddle"></gl-emoji>',
- );
- });
+ afterEach(() => {
+ window.gon = {};
+ delete document.body.dataset.groupFullPath;
+ });
- it('Adds sprite CSS if emojis are not supported', async () => {
- const testPath = '/test-path.css';
- jest.spyOn(EmojiUnicodeSupport, 'default').mockReturnValue(false);
- window.gon.emoji_sprites_css_path = testPath;
+ it('renders custom emoji', async () => {
+ const glEmojiElement = markupToDomElement('<gl-emoji data-name="parrot"></gl-emoji>');
- expect(document.head.querySelector(`link[href="${testPath}"]`)).toBe(null);
- expect(window.gon.emoji_sprites_css_added).toBe(undefined);
+ await waitForPromises();
- markupToDomElement(
- '<gl-emoji data-fallback-sprite-class="emoji-bomb" data-name="bomb"></gl-emoji>',
- );
- await waitForPromises();
+ const img = glEmojiElement.querySelector('img');
- expect(document.head.querySelector(`link[href="${testPath}"]`).outerHTML).toBe(
- '<link rel="stylesheet" href="/test-path.css">',
- );
- expect(window.gon.emoji_sprites_css_added).toBe(true);
+ expect(glEmojiElement.dataset.unicodeVersion).toBe('custom');
+ expect(img.getAttribute('src')).toBe('parrot.gif');
+ });
});
});
diff --git a/spec/frontend/behaviors/markdown/render_gfm_spec.js b/spec/frontend/behaviors/markdown/render_gfm_spec.js
index 220ad874b47..0bbb92282e5 100644
--- a/spec/frontend/behaviors/markdown/render_gfm_spec.js
+++ b/spec/frontend/behaviors/markdown/render_gfm_spec.js
@@ -1,7 +1,4 @@
import { renderGFM } from '~/behaviors/markdown/render_gfm';
-import renderMetrics from '~/behaviors/markdown/render_metrics';
-
-jest.mock('~/behaviors/markdown/render_metrics');
describe('renderGFM', () => {
it('handles a missing element', () => {
@@ -9,27 +6,4 @@ describe('renderGFM', () => {
renderGFM();
}).not.toThrow();
});
-
- describe('remove_monitor_metrics flag', () => {
- let metricsElement;
-
- beforeEach(() => {
- window.gon = { features: { removeMonitorMetrics: true } };
- metricsElement = document.createElement('div');
- metricsElement.setAttribute('class', '.js-render-metrics');
- });
-
- it('renders metrics when the flag is disabled', () => {
- window.gon.features = { features: { removeMonitorMetrics: false } };
- renderGFM(metricsElement);
-
- expect(renderMetrics).toHaveBeenCalled();
- });
-
- it('does not render metrics when the flag is enabled', () => {
- renderGFM(metricsElement);
-
- expect(renderMetrics).not.toHaveBeenCalled();
- });
- });
});
diff --git a/spec/frontend/behaviors/markdown/render_metrics_spec.js b/spec/frontend/behaviors/markdown/render_metrics_spec.js
deleted file mode 100644
index ab81ed6b8f0..00000000000
--- a/spec/frontend/behaviors/markdown/render_metrics_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import { TEST_HOST } from 'helpers/test_constants';
-import renderMetrics from '~/behaviors/markdown/render_metrics';
-
-const mockEmbedGroup = jest.fn();
-
-jest.mock('vue', () => ({ extend: () => mockEmbedGroup }));
-jest.mock('~/monitoring/components/embeds/embed_group.vue', () => jest.fn());
-jest.mock('~/monitoring/stores/embed_group/', () => ({ createStore: jest.fn() }));
-
-const getElements = () => Array.from(document.getElementsByClassName('js-render-metrics'));
-
-describe('Render metrics for Gitlab Flavoured Markdown', () => {
- it('does nothing when no elements are found', () => {
- return renderMetrics([]).then(() => {
- expect(mockEmbedGroup).not.toHaveBeenCalled();
- });
- });
-
- it('renders a vue component when elements are found', () => {
- document.body.innerHTML = `<div class="js-render-metrics" data-dashboard-url="${TEST_HOST}"></div>`;
-
- return renderMetrics(getElements()).then(() => {
- expect(mockEmbedGroup).toHaveBeenCalledTimes(1);
- expect(mockEmbedGroup).toHaveBeenCalledWith(
- expect.objectContaining({ propsData: { urls: [`${TEST_HOST}`] } }),
- );
- });
- });
-
- it('takes sibling metrics and groups them under a shared parent', () => {
- document.body.innerHTML = `
- <p><span>Hello</span></p>
- <div class="js-render-metrics" data-dashboard-url="${TEST_HOST}/1"></div>
- <div class="js-render-metrics" data-dashboard-url="${TEST_HOST}/2"></div>
- <p><span>Hello</span></p>
- <div class="js-render-metrics" data-dashboard-url="${TEST_HOST}/3"></div>
- `;
-
- return renderMetrics(getElements()).then(() => {
- expect(mockEmbedGroup).toHaveBeenCalledTimes(2);
- expect(mockEmbedGroup).toHaveBeenCalledWith(
- expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/1`, `${TEST_HOST}/2`] } }),
- );
- expect(mockEmbedGroup).toHaveBeenCalledWith(
- expect.objectContaining({ propsData: { urls: [`${TEST_HOST}/3`] } }),
- );
- });
- });
-});
diff --git a/spec/frontend/blob/line_highlighter_spec.js b/spec/frontend/blob/line_highlighter_spec.js
index b2e1a29b84f..de39a8f688a 100644
--- a/spec/frontend/blob/line_highlighter_spec.js
+++ b/spec/frontend/blob/line_highlighter_spec.js
@@ -1,5 +1,4 @@
/* eslint-disable no-return-assign, no-new, no-underscore-dangle */
-import $ from 'jquery';
import htmlStaticLineHighlighter from 'test_fixtures_static/line_highlighter.html';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import LineHighlighter from '~/blob/line_highlighter';
@@ -9,11 +8,15 @@ describe('LineHighlighter', () => {
const testContext = {};
const clickLine = (number, eventData = {}) => {
- if ($.isEmptyObject(eventData)) {
- return $(`#L${number}`).click();
+ if (Object.keys(eventData).length === 0) {
+ return document.querySelector(`#L${number}`).click();
}
- const e = $.Event('click', eventData);
- return $(`#L${number}`).trigger(e);
+ const e = new MouseEvent('click', {
+ bubbles: true,
+ cancelable: true,
+ ...eventData,
+ });
+ return document.querySelector(`#L${number}`).dispatchEvent(e);
};
beforeEach(() => {
@@ -35,32 +38,30 @@ describe('LineHighlighter', () => {
it('highlights one line given in the URL hash', () => {
new LineHighlighter({ hash: '#L13' });
- expect($('#LC13')).toHaveClass(testContext.css);
+ expect(document.querySelector('#LC13').classList).toContain(testContext.css);
});
it('highlights one line given in the URL hash with given CSS class name', () => {
const hiliter = new LineHighlighter({ hash: '#L13', highlightLineClass: 'hilite' });
expect(hiliter.highlightLineClass).toBe('hilite');
- expect($('#LC13')).toHaveClass('hilite');
- expect($('#LC13')).not.toHaveClass('hll');
+ expect(document.querySelector('#LC13').classList).toContain('hilite');
+ expect(document.querySelector('#LC13').classList).not.toContain('hll');
});
it('highlights a range of lines given in the URL hash', () => {
new LineHighlighter({ hash: '#L5-25' });
- expect($(`.${testContext.css}`).length).toBe(21);
for (let line = 5; line <= 25; line += 1) {
- expect($(`#LC${line}`)).toHaveClass(testContext.css);
+ expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css);
}
});
it('highlights a range of lines given in the URL hash using GitHub format', () => {
new LineHighlighter({ hash: '#L5-L25' });
- expect($(`.${testContext.css}`).length).toBe(21);
for (let line = 5; line <= 25; line += 1) {
- expect($(`#LC${line}`)).toHaveClass(testContext.css);
+ expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css);
}
});
@@ -74,11 +75,13 @@ describe('LineHighlighter', () => {
it('discards click events', () => {
const clickSpy = jest.fn();
- $('a[data-line-number]').click(clickSpy);
+ document.querySelectorAll('a[data-line-number]').forEach((el) => {
+ el.addEventListener('click', clickSpy);
+ });
clickLine(13);
- expect(clickSpy.mock.calls[0][0].isDefaultPrevented()).toEqual(true);
+ expect(clickSpy.mock.calls[0][0].defaultPrevented).toEqual(true);
});
it('handles garbage input from the hash', () => {
@@ -101,27 +104,19 @@ describe('LineHighlighter', () => {
});
describe('clickHandler', () => {
- it('handles clicking on a child icon element', () => {
- const spy = jest.spyOn(testContext.class, 'setHash');
- $('#L13 [data-testid="link-icon"]').mousedown().click();
-
- expect(spy).toHaveBeenCalledWith(13);
- expect($('#LC13')).toHaveClass(testContext.css);
- });
-
describe('without shiftKey', () => {
it('highlights one line when clicked', () => {
clickLine(13);
- expect($('#LC13')).toHaveClass(testContext.css);
+ expect(document.querySelector('#LC13').classList).toContain(testContext.css);
});
it('unhighlights previously highlighted lines', () => {
clickLine(13);
clickLine(20);
- expect($('#LC13')).not.toHaveClass(testContext.css);
- expect($('#LC20')).toHaveClass(testContext.css);
+ expect(document.querySelector('#LC13').classList).not.toContain(testContext.css);
+ expect(document.querySelector('#LC20').classList).toContain(testContext.css);
});
it('sets the hash', () => {
@@ -138,6 +133,8 @@ describe('LineHighlighter', () => {
clickLine(13);
clickLine(20, {
shiftKey: true,
+ bubbles: true,
+ cancelable: true,
});
expect(spy).toHaveBeenCalledWith(13);
@@ -150,8 +147,8 @@ describe('LineHighlighter', () => {
shiftKey: true,
});
- expect($('#LC13')).toHaveClass(testContext.css);
- expect($(`.${testContext.css}`).length).toBe(1);
+ expect(document.querySelector('#LC13').classList).toContain(testContext.css);
+ expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(1);
});
it('sets the hash', () => {
@@ -171,9 +168,9 @@ describe('LineHighlighter', () => {
shiftKey: true,
});
- expect($(`.${testContext.css}`).length).toBe(6);
+ expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(6);
for (let line = 15; line <= 20; line += 1) {
- expect($(`#LC${line}`)).toHaveClass(testContext.css);
+ expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css);
}
});
@@ -183,9 +180,9 @@ describe('LineHighlighter', () => {
shiftKey: true,
});
- expect($(`.${testContext.css}`).length).toBe(6);
+ expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(6);
for (let line = 5; line <= 10; line += 1) {
- expect($(`#LC${line}`)).toHaveClass(testContext.css);
+ expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css);
}
});
});
@@ -205,9 +202,9 @@ describe('LineHighlighter', () => {
shiftKey: true,
});
- expect($(`.${testContext.css}`).length).toBe(6);
+ expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(6);
for (let line = 5; line <= 10; line += 1) {
- expect($(`#LC${line}`)).toHaveClass(testContext.css);
+ expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css);
}
});
@@ -216,9 +213,9 @@ describe('LineHighlighter', () => {
shiftKey: true,
});
- expect($(`.${testContext.css}`).length).toBe(6);
+ expect(document.querySelectorAll(`.${testContext.css}`).length).toBe(6);
for (let line = 10; line <= 15; line += 1) {
- expect($(`#LC${line}`)).toHaveClass(testContext.css);
+ expect(document.querySelector(`#LC${line}`).classList).toContain(testContext.css);
}
});
});
@@ -251,13 +248,13 @@ describe('LineHighlighter', () => {
it('highlights the specified line', () => {
testContext.subject(13);
- expect($('#LC13')).toHaveClass(testContext.css);
+ expect(document.querySelector('#LC13').classList).toContain(testContext.css);
});
it('accepts a String-based number', () => {
testContext.subject('13');
- expect($('#LC13')).toHaveClass(testContext.css);
+ expect(document.querySelector('#LC13').classList).toContain(testContext.css);
});
});
diff --git a/spec/frontend/blob_edit/edit_blob_spec.js b/spec/frontend/blob_edit/edit_blob_spec.js
index 9ab20fc2cd7..1bdc54723ce 100644
--- a/spec/frontend/blob_edit/edit_blob_spec.js
+++ b/spec/frontend/blob_edit/edit_blob_spec.js
@@ -61,7 +61,6 @@ describe('Blob Editing', () => {
});
afterEach(() => {
mock.restore();
- jest.clearAllMocks();
unuseMock.mockClear();
useMock.mockClear();
resetHTMLFixture();
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index a925f752f5e..36556ba00af 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -92,6 +92,7 @@ describe('Board card component', () => {
isEpicBoard,
issuableType: TYPE_ISSUE,
isGroupBoard,
+ isApolloBoard: false,
},
});
};
@@ -111,7 +112,6 @@ describe('Board card component', () => {
afterEach(() => {
store = null;
- jest.clearAllMocks();
});
it('renders issue title', () => {
diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js
index 3d6e4c18f51..e7624437ac5 100644
--- a/spec/frontend/boards/components/board_app_spec.js
+++ b/spec/frontend/boards/components/board_app_spec.js
@@ -4,7 +4,9 @@ import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import BoardApp from '~/boards/components/board_app.vue';
+import eventHub from '~/boards/eventhub';
import activeBoardItemQuery from 'ee_else_ce/boards/graphql/client/active_board_item.query.graphql';
import boardListsQuery from 'ee_else_ce/boards/graphql/board_lists.query.graphql';
import { rawIssue, boardListsQueryResponse } from '../mock_data';
@@ -93,5 +95,14 @@ describe('BoardApp', () => {
expect(wrapper.classes()).not.toContain('is-compact');
});
+
+ it('refetches lists when updateBoard event is received', async () => {
+ jest.spyOn(eventHub, '$on').mockImplementation(() => {});
+
+ createComponent({ isApolloBoard: true });
+ await waitForPromises();
+
+ expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists);
+ });
});
});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 9260718a94b..0a2a78479fb 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import Vue from 'vue';
import Draggable from 'vuedraggable';
import Vuex from 'vuex';
-import eventHub from '~/boards/eventhub';
+
import createMockApollo from 'helpers/mock_apollo_helper';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
@@ -182,15 +182,6 @@ describe('BoardContent', () => {
expect(wrapper.findComponent(BoardContentSidebar).exists()).toBe(true);
});
- it('refetches lists when updateBoard event is received', async () => {
- jest.spyOn(eventHub, '$on').mockImplementation(() => {});
-
- createComponent({ isApolloBoard: true });
- await waitForPromises();
-
- expect(eventHub.$on).toHaveBeenCalledWith('updateBoard', wrapper.vm.refetchLists);
- });
-
it('reorders lists', async () => {
const movableListsOrder = [mockLists[0].id, mockLists[1].id];
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index ad2674f9d3b..0c9e1b4646e 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -1,4 +1,4 @@
-import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { GlButtonGroup } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
@@ -93,18 +93,17 @@ describe('Board List Header Component', () => {
...injectedProps,
},
stubs: {
- GlDisclosureDropdown,
- GlDisclosureDropdownItem,
+ GlButtonGroup,
},
});
};
- const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findButtonGroup = () => wrapper.findComponent(GlButtonGroup);
const isCollapsed = () => wrapper.vm.list.collapsed;
const findTitle = () => wrapper.find('.board-title');
const findCaret = () => wrapper.findByTestId('board-title-caret');
- const findNewIssueButton = () => wrapper.findByTestId('newIssueBtn');
- const findSettingsButton = () => wrapper.findByTestId('settingsBtn');
+ const findNewIssueButton = () => wrapper.findByTestId('new-issue-btn');
+ const findSettingsButton = () => wrapper.findByTestId('settings-btn');
const findBoardListHeader = () => wrapper.findByTestId('board-list-header');
it('renders border when label color is present', () => {
@@ -131,13 +130,13 @@ describe('Board List Header Component', () => {
it.each(hasNoAddButton)('does not render dropdown when List Type is `%s`', (listType) => {
createComponent({ listType });
- expect(findDropdown().exists()).toBe(false);
+ expect(findButtonGroup().exists()).toBe(false);
});
it.each(hasAddButton)('does render when List Type is `%s`', (listType) => {
createComponent({ listType });
- expect(findDropdown().exists()).toBe(true);
+ expect(findButtonGroup().exists()).toBe(true);
expect(findNewIssueButton().exists()).toBe(true);
});
@@ -146,7 +145,7 @@ describe('Board List Header Component', () => {
currentUserId: null,
});
- expect(findDropdown().exists()).toBe(false);
+ expect(findButtonGroup().exists()).toBe(false);
});
});
@@ -156,20 +155,20 @@ describe('Board List Header Component', () => {
it.each(hasSettings)('does render for List Type `%s`', (listType) => {
createComponent({ listType });
- expect(findDropdown().exists()).toBe(true);
+ expect(findButtonGroup().exists()).toBe(true);
expect(findSettingsButton().exists()).toBe(true);
});
it('does not render dropdown when ListType `closed`', () => {
createComponent({ listType: ListType.closed });
- expect(findDropdown().exists()).toBe(false);
+ expect(findButtonGroup().exists()).toBe(false);
});
it('renders dropdown but not the Settings button when ListType `backlog`', () => {
createComponent({ listType: ListType.backlog });
- expect(findDropdown().exists()).toBe(true);
+ expect(findButtonGroup().exists()).toBe(true);
expect(findSettingsButton().exists()).toBe(false);
});
});
diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js
index 651d1daee52..a1088f1e8f7 100644
--- a/spec/frontend/boards/components/board_new_issue_spec.js
+++ b/spec/frontend/boards/components/board_new_issue_spec.js
@@ -1,25 +1,49 @@
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import BoardNewIssue from '~/boards/components/board_new_issue.vue';
import BoardNewItem from '~/boards/components/board_new_item.vue';
import ProjectSelect from '~/boards/components/project_select.vue';
import eventHub from '~/boards/eventhub';
-
-import { mockList, mockGroupProjects, mockIssue, mockIssue2 } from '../mock_data';
+import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
+import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
+import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
+
+import {
+ mockList,
+ mockGroupProjects,
+ mockIssue,
+ mockIssue2,
+ mockProjectBoardResponse,
+ mockGroupBoardResponse,
+} from '../mock_data';
Vue.use(Vuex);
+Vue.use(VueApollo);
const addListNewIssuesSpy = jest.fn().mockResolvedValue();
const mockActions = { addListNewIssue: addListNewIssuesSpy };
+const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
+const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
+
+const mockApollo = createMockApollo([
+ [projectBoardQuery, projectBoardQueryHandlerSuccess],
+ [groupBoardQuery, groupBoardQueryHandlerSuccess],
+]);
+
const createComponent = ({
- state = { selectedProject: mockGroupProjects[0] },
+ state = {},
actions = mockActions,
getters = { getBoardItemsByList: () => () => [] },
isGroupBoard = true,
+ data = { selectedProject: mockGroupProjects[0] },
+ provide = {},
} = {}) =>
shallowMount(BoardNewIssue, {
+ apolloProvider: mockApollo,
store: new Vuex.Store({
state,
actions,
@@ -27,13 +51,19 @@ const createComponent = ({
}),
propsData: {
list: mockList,
+ boardId: 'gid://gitlab/Board/1',
},
+ data: () => data,
provide: {
groupId: 1,
fullPath: mockGroupProjects[0].fullPath,
weightFeatureAvailable: false,
boardWeight: null,
isGroupBoard,
+ boardType: 'group',
+ isEpicBoard: false,
+ isApolloBoard: false,
+ ...provide,
},
stubs: {
BoardNewItem,
@@ -137,4 +167,33 @@ describe('Issue boards new issue form', () => {
expect(projectSelect.exists()).toBe(false);
});
});
+
+ describe('Apollo boards', () => {
+ it.each`
+ boardType | queryHandler | notCalledHandler
+ ${WORKSPACE_GROUP} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
+ ${WORKSPACE_PROJECT} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
+ `(
+ 'fetches $boardType board and emits addNewIssue event',
+ async ({ boardType, queryHandler, notCalledHandler }) => {
+ wrapper = createComponent({
+ provide: {
+ boardType,
+ isProjectBoard: boardType === WORKSPACE_PROJECT,
+ isGroupBoard: boardType === WORKSPACE_GROUP,
+ isApolloBoard: true,
+ },
+ });
+
+ await nextTick();
+ findBoardNewItem().vm.$emit('form-submit', { title: 'Foo' });
+
+ await nextTick();
+
+ expect(queryHandler).toHaveBeenCalled();
+ expect(notCalledHandler).not.toHaveBeenCalled();
+ expect(wrapper.emitted('addNewIssue')[0][0]).toMatchObject({ title: 'Foo' });
+ },
+ );
+ });
});
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
index b1e14be8ceb..affe1260c66 100644
--- a/spec/frontend/boards/components/board_settings_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -90,10 +90,6 @@ describe('BoardSettingsSidebar', () => {
const findModal = () => wrapper.findComponent(GlModal);
const findRemoveButton = () => wrapper.findComponent(GlButton);
- afterEach(() => {
- jest.restoreAllMocks();
- });
-
it('finds a MountingPortal component', () => {
createComponent();
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 447aacd9cea..8235c3e4194 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -836,6 +836,7 @@ export const mockTokens = (fetchLabels, fetchUsers, fetchMilestones, isSignedIn)
type: TOKEN_TYPE_ASSIGNEE,
operators: OPERATORS_IS_NOT,
token: UserToken,
+ dataType: 'user',
unique: true,
fetchUsers,
preloadedUsers: [],
@@ -847,6 +848,7 @@ export const mockTokens = (fetchLabels, fetchUsers, fetchMilestones, isSignedIn)
operators: OPERATORS_IS_NOT,
symbol: '@',
token: UserToken,
+ dataType: 'user',
unique: true,
fetchUsers,
preloadedUsers: [],
@@ -1040,4 +1042,43 @@ export const destroyBoardListMutationResponse = {
},
};
+export const mockProjects = [
+ {
+ id: 'gid://gitlab/Project/1',
+ name: 'Gitlab Shell',
+ nameWithNamespace: 'Gitlab Org / Gitlab Shell',
+ fullPath: 'gitlab-org/gitlab-shell',
+ archived: false,
+ __typename: 'Project',
+ },
+ {
+ id: 'gid://gitlab/Project/2',
+ name: 'Gitlab Test',
+ nameWithNamespace: 'Gitlab Org / Gitlab Test',
+ fullPath: 'gitlab-org/gitlab-test',
+ archived: true,
+ __typename: 'Project',
+ },
+];
+
+export const mockGroupProjectsResponse = (projects = mockProjects) => ({
+ data: {
+ group: {
+ id: 'gid://gitlab/Group/1',
+ projects: {
+ nodes: projects,
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'abc',
+ endCursor: 'bcd',
+ __typename: 'PageInfo',
+ },
+ __typename: 'ProjectConnection',
+ },
+ __typename: 'Group',
+ },
+ },
+});
+
export const DEFAULT_COLOR = '#1068bf';
diff --git a/spec/frontend/boards/project_select_spec.js b/spec/frontend/boards/project_select_spec.js
index b4308b38947..f1daccfadda 100644
--- a/spec/frontend/boards/project_select_spec.js
+++ b/spec/frontend/boards/project_select_spec.js
@@ -1,17 +1,19 @@
import { GlCollapsibleListbox, GlListboxItem, GlLoadingIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import groupProjectsQuery from '~/boards/graphql/group_projects.query.graphql';
import ProjectSelect from '~/boards/components/project_select.vue';
-import defaultState from '~/boards/stores/state';
-import { mockActiveGroupProjects, mockList } from './mock_data';
+import { mockList, mockGroupProjectsResponse, mockProjects } from './mock_data';
-const mockProjectsList1 = mockActiveGroupProjects.slice(0, 1);
+Vue.use(VueApollo);
describe('ProjectSelect component', () => {
let wrapper;
- let store;
+ let mockApollo;
const findLabel = () => wrapper.find("[data-testid='header-label']");
const findGlCollapsibleListBox = () => wrapper.findComponent(GlCollapsibleListbox);
@@ -26,77 +28,54 @@ describe('ProjectSelect component', () => {
const findInMenuLoadingIcon = () => wrapper.find("[data-testid='listbox-search-loader']");
const findEmptySearchMessage = () => wrapper.find("[data-testid='listbox-no-results-text']");
- const createStore = ({ state, activeGroupProjects }) => {
- Vue.use(Vuex);
-
- store = new Vuex.Store({
- state: {
- defaultState,
- groupProjectsFlags: {
- isLoading: false,
- pageInfo: {
- hasNextPage: false,
- },
- },
- ...state,
- },
- actions: {
- fetchGroupProjects: jest.fn(),
- setSelectedProject: jest.fn(),
- },
- getters: {
- activeGroupProjects: () => activeGroupProjects,
- },
- });
- };
-
- const createWrapper = ({ state = {}, activeGroupProjects = [] } = {}) => {
- createStore({
- state,
- activeGroupProjects,
- });
+ const projectsQueryHandler = jest.fn().mockResolvedValue(mockGroupProjectsResponse());
+ const emptyProjectsQueryHandler = jest.fn().mockResolvedValue(mockGroupProjectsResponse([]));
- wrapper = mount(ProjectSelect, {
+ const createWrapper = ({ queryHandler = projectsQueryHandler, selectedProject = {} } = {}) => {
+ mockApollo = createMockApollo([[groupProjectsQuery, queryHandler]]);
+ wrapper = mountExtended(ProjectSelect, {
+ apolloProvider: mockApollo,
propsData: {
list: mockList,
+ selectedProject,
},
- store,
provide: {
groupId: 1,
+ fullPath: 'gitlab-org',
},
attachTo: document.body,
});
};
- it('displays a header title', () => {
- createWrapper();
-
- expect(findLabel().text()).toBe('Projects');
- });
-
- it('renders a default dropdown text', () => {
- createWrapper();
-
- expect(findGlCollapsibleListBox().exists()).toBe(true);
- expect(findGlCollapsibleListBox().text()).toContain('Select a project');
- });
-
describe('when mounted', () => {
- it('displays a loading icon while projects are being fetched', async () => {
+ beforeEach(() => {
createWrapper();
+ });
+ it('displays a loading icon while projects are being fetched', async () => {
expect(findGlDropdownLoadingIcon().exists()).toBe(true);
- await nextTick();
+ await waitForPromises();
expect(findGlDropdownLoadingIcon().exists()).toBe(false);
+ expect(projectsQueryHandler).toHaveBeenCalled();
+ });
+
+ it('displays a header title', () => {
+ expect(findLabel().text()).toBe('Projects');
+ });
+
+ it('renders a default dropdown text', () => {
+ expect(findGlCollapsibleListBox().exists()).toBe(true);
+ expect(findGlCollapsibleListBox().text()).toContain('Select a project');
});
});
describe('when dropdown menu is open', () => {
describe('by default', () => {
- beforeEach(() => {
- createWrapper({ activeGroupProjects: mockActiveGroupProjects });
+ beforeEach(async () => {
+ createWrapper();
+ await waitForPromises();
});
it('shows GlListboxSearchInput with placeholder text', () => {
@@ -106,7 +85,7 @@ describe('ProjectSelect component', () => {
it("displays the fetched project's name", () => {
expect(findFirstGlDropdownItem().exists()).toBe(true);
- expect(findFirstGlDropdownItem().text()).toContain(mockProjectsList1[0].name);
+ expect(findFirstGlDropdownItem().text()).toContain(mockProjects[0].name);
});
it("doesn't render loading icon in the menu", () => {
@@ -119,33 +98,31 @@ describe('ProjectSelect component', () => {
});
describe('when no projects are being returned', () => {
- it('renders empty search result message', () => {
- createWrapper();
+ it('renders empty search result message', async () => {
+ createWrapper({ queryHandler: emptyProjectsQueryHandler });
+ await waitForPromises();
expect(findEmptySearchMessage().exists()).toBe(true);
});
});
describe('when a project is selected', () => {
- beforeEach(() => {
- createWrapper({ activeGroupProjects: mockProjectsList1 });
-
- findFirstGlDropdownItem().find('li').trigger('click');
+ beforeEach(async () => {
+ createWrapper({ selectedProject: mockProjects[0] });
+ await waitForPromises();
});
it('renders the name of the selected project', () => {
expect(findGlCollapsibleListBox().find('.gl-new-dropdown-button-text').text()).toBe(
- mockProjectsList1[0].name,
+ mockProjects[0].name,
);
});
});
describe('when projects are loading', () => {
- beforeEach(() => {
- createWrapper({ state: { groupProjectsFlags: { isLoading: true } } });
- });
-
- it('displays and hides gl-loading-icon while and after fetching data', () => {
+ it('displays and hides gl-loading-icon while and after fetching data', async () => {
+ createWrapper();
+ await nextTick();
expect(findInMenuLoadingIcon().isVisible()).toBe(true);
});
});
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index f3800ce8324..a2961fb1302 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1541,8 +1541,8 @@ describe('addListNewIssue', () => {
it('should add board scope to the issue being created', async () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
- createIssue: {
- issue: mockIssue,
+ createIssuable: {
+ issuable: mockIssue,
errors: [],
},
},
@@ -1600,8 +1600,8 @@ describe('addListNewIssue', () => {
it('dispatches a correct set of mutations', () => {
jest.spyOn(gqlClient, 'mutate').mockResolvedValue({
data: {
- createIssue: {
- issue: mockIssue,
+ createIssuable: {
+ issuable: mockIssue,
errors: [],
},
},
diff --git a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
index 9db6a523dec..4da56a865d5 100644
--- a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
+++ b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
@@ -5,7 +5,6 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
<gl-base-dropdown-stub
category="tertiary"
class="gl-disclosure-dropdown gl-display-none gl-md-display-block!"
- data-qa-selector="delete_merged_branches_dropdown_button"
icon="ellipsis_v"
nocaret="true"
offset="[object Object]"
@@ -34,7 +33,7 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
<b-button-stub
class="gl-display-block gl-md-display-none! gl-button btn-danger-secondary"
- data-qa-selector="delete_merged_branches_button"
+ data-testid="delete-merged-branches-button"
size="md"
tag="button"
type="button"
@@ -100,7 +99,6 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
aria-labelledby="input-label"
autocomplete="off"
class="gl-form-input gl-mt-2 gl-form-input-sm"
- data-qa-selector="delete_merged_branches_input"
debounce="0"
formatter="[Function]"
type="text"
@@ -146,7 +144,6 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
<b-button-stub
class="gl-button"
- data-qa-selector="delete_merged_branches_confirmation_button"
data-testid="delete-merged-branches-confirmation-button"
disabled="true"
size="md"
diff --git a/spec/frontend/branches/components/delete_merged_branches_spec.js b/spec/frontend/branches/components/delete_merged_branches_spec.js
index 3e47e76622d..3319ed13004 100644
--- a/spec/frontend/branches/components/delete_merged_branches_spec.js
+++ b/spec/frontend/branches/components/delete_merged_branches_spec.js
@@ -37,7 +37,7 @@ const createComponent = (mountFn = shallowMountExtended, stubs = {}) => {
};
const findDeleteButton = () =>
- wrapper.findComponent('[data-qa-selector="delete_merged_branches_button"]');
+ wrapper.findComponent('[data-testid="delete-merged-branches-button"]');
const findModal = () => wrapper.findComponent(GlModal);
const findConfirmationButton = () =>
wrapper.findByTestId('delete-merged-branches-confirmation-button');
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
index 1937e3b34b7..64227872af3 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
@@ -1,4 +1,10 @@
-import { GlListboxItem, GlCollapsibleListbox, GlDropdownItem, GlIcon } from '@gitlab/ui';
+import {
+ GlListboxItem,
+ GlCollapsibleListbox,
+ GlDropdownDivider,
+ GlDropdownItem,
+ GlIcon,
+} from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { allEnvironments, ENVIRONMENT_QUERY_LIMIT } from '~/ci/ci_variable_list/constants';
import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
@@ -10,6 +16,7 @@ describe('Ci environments dropdown', () => {
const defaultProps = {
areEnvironmentsLoading: false,
environments: envs,
+ hasEnvScopeQuery: false,
selectedEnvironmentScope: '',
};
@@ -19,19 +26,15 @@ describe('Ci environments dropdown', () => {
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListboxText = () => findListbox().props('toggleText');
const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem);
+ const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
const findMaxEnvNote = () => wrapper.findByTestId('max-envs-notice');
- const createComponent = ({ props = {}, searchTerm = '', enableFeatureFlag = false } = {}) => {
+ const createComponent = ({ props = {}, searchTerm = '' } = {}) => {
wrapper = mountExtended(CiEnvironmentsDropdown, {
propsData: {
...defaultProps,
...props,
},
- provide: {
- glFeatures: {
- ciLimitEnvironmentScope: enableFeatureFlag,
- },
- },
});
findListbox().vm.$emit('search', searchTerm);
@@ -42,6 +45,10 @@ describe('Ci environments dropdown', () => {
createComponent({ searchTerm: 'stable' });
});
+ it('renders dropdown divider', () => {
+ expect(findDropdownDivider().exists()).toBe(true);
+ });
+
it('renders create button with search term if environments do not contain search term', () => {
const button = findCreateWildcardButton();
expect(button.exists()).toBe(true);
@@ -51,14 +58,14 @@ describe('Ci environments dropdown', () => {
describe('Search term is empty', () => {
describe.each`
- featureFlag | flagStatus | defaultEnvStatus | firstItemValue | envIndices
- ${true} | ${'enabled'} | ${'prepends'} | ${'*'} | ${[1, 2, 3]}
- ${false} | ${'disabled'} | ${'does not prepend'} | ${envs[0]} | ${[0, 1, 2]}
+ hasEnvScopeQuery | status | defaultEnvStatus | firstItemValue | envIndices
+ ${true} | ${'exists'} | ${'prepends'} | ${'*'} | ${[1, 2, 3]}
+ ${false} | ${'does not exist'} | ${'does not prepend'} | ${envs[0]} | ${[0, 1, 2]}
`(
- 'when ciLimitEnvironmentScope feature flag is $flagStatus',
- ({ featureFlag, defaultEnvStatus, firstItemValue, envIndices }) => {
+ 'when query for fetching environment scope $status',
+ ({ defaultEnvStatus, firstItemValue, hasEnvScopeQuery, envIndices }) => {
beforeEach(() => {
- createComponent({ props: { environments: envs }, enableFeatureFlag: featureFlag });
+ createComponent({ props: { environments: envs, hasEnvScopeQuery } });
});
it(`${defaultEnvStatus} * in listbox`, () => {
@@ -91,7 +98,7 @@ describe('Ci environments dropdown', () => {
});
});
- describe('When ciLimitEnvironmentScope feature flag is disabled', () => {
+ describe('when environments are not fetched via graphql', () => {
const currentEnv = envs[2];
beforeEach(() => {
@@ -118,11 +125,15 @@ describe('Ci environments dropdown', () => {
});
});
- describe('When ciLimitEnvironmentScope feature flag is enabled', () => {
+ describe('when fetching environments via graphql', () => {
const currentEnv = envs[2];
beforeEach(() => {
- createComponent({ enableFeatureFlag: true });
+ createComponent({ props: { hasEnvScopeQuery: true } });
+ });
+
+ it('renders dropdown divider', () => {
+ expect(findDropdownDivider().exists()).toBe(true);
});
it('renders environments passed down to it', async () => {
@@ -131,6 +142,22 @@ describe('Ci environments dropdown', () => {
expect(findAllListboxItems()).toHaveLength(envs.length);
});
+ it('renders dropdown loading icon while fetch query is loading', () => {
+ createComponent({ props: { areEnvironmentsLoading: true, hasEnvScopeQuery: true } });
+
+ expect(findListbox().props('loading')).toBe(true);
+ expect(findListbox().props('searching')).toBe(false);
+ expect(findDropdownDivider().exists()).toBe(false);
+ });
+
+ it('renders search loading icon while search query is loading and dropdown is open', async () => {
+ createComponent({ props: { areEnvironmentsLoading: true, hasEnvScopeQuery: true } });
+ await findListbox().vm.$emit('shown');
+
+ expect(findListbox().props('loading')).toBe(false);
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
it('emits event when searching', async () => {
expect(wrapper.emitted('search-environment-scope')).toHaveLength(1);
@@ -140,12 +167,6 @@ describe('Ci environments dropdown', () => {
expect(wrapper.emitted('search-environment-scope')[1]).toEqual([currentEnv]);
});
- it('renders loading icon while search query is loading', () => {
- createComponent({ enableFeatureFlag: true, props: { areEnvironmentsLoading: true } });
-
- expect(findListbox().props('searching')).toBe(true);
- });
-
it('displays note about max environments shown', () => {
expect(findMaxEnvNote().exists()).toBe(true);
expect(findMaxEnvNote().text()).toContain(String(ENVIRONMENT_QUERY_LIMIT));
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
index 7436210fe70..b364f098a3a 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
@@ -9,15 +9,13 @@ import {
DELETE_MUTATION_ACTION,
UPDATE_MUTATION_ACTION,
} from '~/ci/ci_variable_list/constants';
+import getGroupEnvironments from '~/ci/ci_variable_list/graphql/queries/group_environments.query.graphql';
import getGroupVariables from '~/ci/ci_variable_list/graphql/queries/group_variables.query.graphql';
import addGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
import deleteGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
import updateGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
const mockProvide = {
- glFeatures: {
- groupScopedCiVariables: false,
- },
groupPath: '/group',
groupId: 12,
};
@@ -27,9 +25,16 @@ describe('Ci Group Variable wrapper', () => {
const findCiShared = () => wrapper.findComponent(ciVariableShared);
- const createComponent = ({ provide = {} } = {}) => {
+ const createComponent = ({ featureFlags } = {}) => {
wrapper = shallowMount(ciGroupVariables, {
- provide: { ...mockProvide, ...provide },
+ provide: {
+ ...mockProvide,
+ glFeatures: {
+ ciGroupEnvScopeGraphql: false,
+ groupScopedCiVariables: false,
+ ...featureFlags,
+ },
+ },
});
};
@@ -62,10 +67,10 @@ describe('Ci Group Variable wrapper', () => {
});
});
- describe('feature flag', () => {
+ describe('groupScopedCiVariables feature flag', () => {
describe('When enabled', () => {
beforeEach(() => {
- createComponent({ provide: { glFeatures: { groupScopedCiVariables: true } } });
+ createComponent({ featureFlags: { groupScopedCiVariables: true } });
});
it('Passes down `true` to variable shared component', () => {
@@ -75,7 +80,7 @@ describe('Ci Group Variable wrapper', () => {
describe('When disabled', () => {
beforeEach(() => {
- createComponent({ provide: { glFeatures: { groupScopedCiVariables: false } } });
+ createComponent();
});
it('Passes down `false` to variable shared component', () => {
@@ -83,4 +88,26 @@ describe('Ci Group Variable wrapper', () => {
});
});
});
+
+ describe('ciGroupEnvScopeGraphql feature flag', () => {
+ describe('When enabled', () => {
+ beforeEach(() => {
+ createComponent({ featureFlags: { ciGroupEnvScopeGraphql: true } });
+ });
+
+ it('Passes down environments query to variable shared component', () => {
+ expect(findCiShared().props('queryData').environments.query).toBe(getGroupEnvironments);
+ });
+ });
+
+ describe('When disabled', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('Does not pass down environments query to variable shared component', () => {
+ expect(findCiShared().props('queryData').environments).toBe(undefined);
+ });
+ });
+ });
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
index e9484cfce57..d843646df16 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
@@ -48,6 +48,7 @@ describe('Ci variable modal', () => {
areScopedVariablesAvailable: true,
environments: [],
hideEnvironmentScope: false,
+ hasEnvScopeQuery: false,
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
variables: [],
@@ -349,14 +350,14 @@ describe('Ci variable modal', () => {
expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
});
- describe('when feature flag is enabled', () => {
+ describe('when query for envioronment scope exists', () => {
beforeEach(() => {
createComponent({
props: {
environments: mockEnvs,
+ hasEnvScopeQuery: true,
variables: mockVariablesWithUniqueScopes(projectString),
},
- provide: { glFeatures: { ciLimitEnvironmentScope: true } },
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
index 12ca9a78369..d72cfc5fc14 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
@@ -21,6 +21,7 @@ describe('Ci variable table', () => {
environments: mapEnvironmentNames(mockEnvs),
hideEnvironmentScope: false,
isLoading: false,
+ hasEnvScopeQuery: false,
maxVariableLimit: 5,
pageInfo: { after: '' },
variables: mockVariablesWithScopes(projectString),
@@ -60,6 +61,7 @@ describe('Ci variable table', () => {
areEnvironmentsLoading: defaultProps.areEnvironmentsLoading,
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
environments: defaultProps.environments,
+ hasEnvScopeQuery: defaultProps.hasEnvScopeQuery,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
variables: defaultProps.variables,
mode: ADD_VARIABLE_ACTION,
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
index f7b90c3da30..6fa1915f3c1 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
@@ -52,6 +52,7 @@ const mockProvide = {
const defaultProps = {
areScopedVariablesAvailable: true,
+ hasEnvScopeQuery: false,
pageInfo: {},
hideEnvironmentScope: false,
refetchAfterMutation: false,
@@ -219,16 +220,12 @@ describe('Ci Variable Shared Component', () => {
expect(mockEnvironments).toHaveBeenCalled();
});
- describe('when Limit Environment Scope FF is enabled', () => {
+ // applies only to project-level CI variables
+ describe('when environment scope is limited', () => {
beforeEach(async () => {
await createComponentWithApollo({
props: { ...createProjectProps() },
- provide: {
- glFeatures: {
- ciLimitEnvironmentScope: true,
- ciVariablesPages: isVariablePagesEnabled,
- },
- },
+ provide: pagesFeatureFlagProvide,
});
});
@@ -251,26 +248,11 @@ describe('Ci Variable Shared Component', () => {
expect.objectContaining({ search: 'staging' }),
);
});
- });
-
- describe('when Limit Environment Scope FF is disabled', () => {
- beforeEach(async () => {
- await createComponentWithApollo({
- props: { ...createProjectProps() },
- provide: pagesFeatureFlagProvide,
- });
- });
- it('initial query is called with the correct variables', () => {
- expect(mockEnvironments).toHaveBeenCalledWith({ fullPath: '/namespace/project/' });
- });
+ it('does not show loading icon in table while searching for environments', () => {
+ findCiSettings().vm.$emit('search-environment-scope', 'staging');
- it(`does not refetch environments when search term is present`, async () => {
- expect(mockEnvironments).toHaveBeenCalledTimes(1);
-
- await findCiSettings().vm.$emit('search-environment-scope', 'staging');
-
- expect(mockEnvironments).toHaveBeenCalledTimes(1);
+ expect(findLoadingIcon().exists()).toBe(false);
});
});
});
@@ -532,6 +514,7 @@ describe('Ci Variable Shared Component', () => {
areEnvironmentsLoading: false,
areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
+ hasEnvScopeQuery: props.hasEnvScopeQuery,
pageInfo: defaultProps.pageInfo,
isLoading: false,
maxVariableLimit,
diff --git a/spec/frontend/ci/ci_variable_list/mocks.js b/spec/frontend/ci/ci_variable_list/mocks.js
index 9c9c99ad5ea..41dfc0ebfda 100644
--- a/spec/frontend/ci/ci_variable_list/mocks.js
+++ b/spec/frontend/ci/ci_variable_list/mocks.js
@@ -189,6 +189,7 @@ export const createProjectProps = () => {
componentName: 'ProjectVariable',
entity: 'project',
fullPath: '/namespace/project/',
+ hasEnvScopeQuery: true,
id: 'gid://gitlab/Project/20',
mutationData: {
[ADD_MUTATION_ACTION]: addProjectVariable,
@@ -213,6 +214,7 @@ export const createGroupProps = () => {
componentName: 'GroupVariable',
entity: 'group',
fullPath: '/my-group',
+ hasEnvScopeQuery: false,
id: 'gid://gitlab/Group/20',
mutationData: {
[ADD_MUTATION_ACTION]: addGroupVariable,
@@ -231,6 +233,7 @@ export const createGroupProps = () => {
export const createInstanceProps = () => {
return {
componentName: 'InstanceVariable',
+ hasEnvScopeQuery: false,
entity: '',
mutationData: {
[ADD_MUTATION_ACTION]: addAdminVariable,
diff --git a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
index 8834231aaef..7a9b4ffdce8 100644
--- a/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/commit/commit_section_spec.js
@@ -17,7 +17,8 @@ import {
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import commitCreate from '~/ci/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql';
import getCurrentBranch from '~/ci/pipeline_editor/graphql/queries/client/current_branch.query.graphql';
-import updatePipelineEtag from '~/ci/pipeline_editor/graphql/mutations/client/update_pipeline_etag.mutation.graphql';
+import getPipelineEtag from '~/ci/pipeline_editor/graphql/queries/client/pipeline_etag.query.graphql';
+
import {
mockCiConfigPath,
mockCiYml,
@@ -253,18 +254,20 @@ describe('Pipeline Editor | Commit section', () => {
describe('when the commit returns a different etag path', () => {
beforeEach(async () => {
createComponentWithApollo();
- jest.spyOn(wrapper.vm.$apollo, 'mutate');
+ jest.spyOn(mockApollo.clients.defaultClient.cache, 'writeQuery');
+
mockMutateCommitData.mockResolvedValue(mockCommitCreateResponseNewEtag);
await submitCommit();
});
- it('calls the client mutation to update the etag', () => {
- // 1:Commit submission, 2:etag update, 3:currentBranch update, 4:lastCommit update
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(4);
- expect(wrapper.vm.$apollo.mutate).toHaveBeenNthCalledWith(2, {
- mutation: updatePipelineEtag,
- variables: {
- pipelineEtag: mockCommitCreateResponseNewEtag.data.commitCreate.commitPipelinePath,
+ it('calls the client mutation to update the etag in the cache', () => {
+ expect(mockApollo.clients.defaultClient.cache.writeQuery).toHaveBeenCalledWith({
+ query: getPipelineEtag,
+ data: {
+ etags: {
+ __typename: 'EtagValues',
+ pipeline: mockCommitCreateResponseNewEtag.data.commitCreate.commitPipelinePath,
+ },
},
});
});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js
index edaa96a197a..d40499fae87 100644
--- a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/accordion_items/rules_item_spec.js
@@ -49,32 +49,36 @@ describe('Rules item', () => {
findRulesWhenSelect().vm.$emit('input', dummyRulesWhen);
- expect(wrapper.emitted('update-job')).toHaveLength(1);
+ expect(wrapper.emitted('update-job')).toHaveLength(2);
expect(wrapper.emitted('update-job')[0]).toEqual([
'rules[0].when',
JOB_RULES_WHEN.delayed.value,
]);
+ expect(wrapper.emitted('update-job')[1]).toEqual([
+ 'rules[0].start_in',
+ `1 ${JOB_RULES_START_IN.second.value}`,
+ ]);
findRulesStartInNumberInput().vm.$emit('input', dummyRulesStartInNumber);
- expect(wrapper.emitted('update-job')).toHaveLength(2);
- expect(wrapper.emitted('update-job')[1]).toEqual([
+ expect(wrapper.emitted('update-job')).toHaveLength(3);
+ expect(wrapper.emitted('update-job')[2]).toEqual([
'rules[0].start_in',
`2 ${JOB_RULES_START_IN.second.value}s`,
]);
findRulesStartInUnitSelect().vm.$emit('input', dummyRulesStartInUnit);
- expect(wrapper.emitted('update-job')).toHaveLength(3);
- expect(wrapper.emitted('update-job')[2]).toEqual([
+ expect(wrapper.emitted('update-job')).toHaveLength(4);
+ expect(wrapper.emitted('update-job')[3]).toEqual([
'rules[0].start_in',
`2 ${dummyRulesStartInUnit}s`,
]);
findRulesAllowFailureCheckBox().vm.$emit('input', dummyRulesAllowFailure);
- expect(wrapper.emitted('update-job')).toHaveLength(4);
- expect(wrapper.emitted('update-job')[3]).toEqual([
+ expect(wrapper.emitted('update-job')).toHaveLength(5);
+ expect(wrapper.emitted('update-job')[4]).toEqual([
'rules[0].allow_failure',
dummyRulesAllowFailure,
]);
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
index 639c2dbef4c..bb48d4dc38d 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_form_spec.js
@@ -1,14 +1,47 @@
import MockAdapter from 'axios-mock-adapter';
-import { GlForm } from '@gitlab/ui';
-import { nextTick } from 'vue';
+import { GlForm, GlLoadingIcon } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { createAlert } from '~/alert';
import PipelineSchedulesForm from '~/ci/pipeline_schedules/components/pipeline_schedules_form.vue';
import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
+import createPipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/create_pipeline_schedule.mutation.graphql';
+import updatePipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/update_pipeline_schedule.mutation.graphql';
+import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql';
import { timezoneDataFixture } from '../../../vue_shared/components/timezone_dropdown/helpers';
+import {
+ createScheduleMutationResponse,
+ updateScheduleMutationResponse,
+ mockSinglePipelineScheduleNode,
+} from '../mock_data';
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+ joinPaths: jest.fn().mockReturnValue(''),
+ queryToObject: jest.fn().mockReturnValue({ id: '1' }),
+}));
+
+const {
+ data: {
+ project: {
+ pipelineSchedules: { nodes },
+ },
+ },
+} = mockSinglePipelineScheduleNode;
+
+const schedule = nodes[0];
+const variables = schedule.variables.nodes;
describe('Pipeline schedules form', () => {
let wrapper;
@@ -17,22 +50,36 @@ describe('Pipeline schedules form', () => {
const cron = '';
const dailyLimit = '';
- const createComponent = (mountFn = shallowMountExtended, stubs = {}) => {
+ const querySuccessHandler = jest.fn().mockResolvedValue(mockSinglePipelineScheduleNode);
+ const queryFailedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+
+ const createMutationHandlerSuccess = jest.fn().mockResolvedValue(createScheduleMutationResponse);
+ const createMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ const updateMutationHandlerSuccess = jest.fn().mockResolvedValue(updateScheduleMutationResponse);
+ const updateMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+
+ const createMockApolloProvider = (
+ requestHandlers = [[createPipelineScheduleMutation, createMutationHandlerSuccess]],
+ ) => {
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (mountFn = shallowMountExtended, editing = false, requestHandlers) => {
wrapper = mountFn(PipelineSchedulesForm, {
propsData: {
timezoneData: timezoneDataFixture,
refParam: 'master',
+ editing,
},
provide: {
fullPath: 'gitlab-org/gitlab',
projectId,
defaultBranch,
- cron,
- cronTimezone: '',
dailyLimit,
settingsLink: '',
+ schedulesPath: '/root/ci-project/-/pipeline_schedules',
},
- stubs,
+ apolloProvider: createMockApolloProvider(requestHandlers),
});
};
@@ -43,17 +90,24 @@ describe('Pipeline schedules form', () => {
const findRefSelector = () => wrapper.findComponent(RefSelector);
const findSubmitButton = () => wrapper.findByTestId('schedule-submit-button');
const findCancelButton = () => wrapper.findByTestId('schedule-cancel-button');
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
// Variables
const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row');
const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key');
const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value');
const findRemoveIcons = () => wrapper.findAllByTestId('remove-ci-variable-row');
- beforeEach(() => {
- createComponent();
- });
+ const addVariableToForm = () => {
+ const input = findKeyInputs().at(0);
+ input.element.value = 'test_var_2';
+ input.trigger('change');
+ };
describe('Form elements', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
it('displays form', () => {
expect(findForm().exists()).toBe(true);
});
@@ -102,19 +156,16 @@ describe('Pipeline schedules form', () => {
it('displays the submit and cancel buttons', () => {
expect(findSubmitButton().exists()).toBe(true);
expect(findCancelButton().exists()).toBe(true);
+ expect(findCancelButton().attributes('href')).toBe('/root/ci-project/-/pipeline_schedules');
});
});
describe('CI variables', () => {
let mock;
- const addVariableToForm = () => {
- const input = findKeyInputs().at(0);
- input.element.value = 'test_var_2';
- input.trigger('change');
- };
-
beforeEach(() => {
+ // mock is needed when we fully mount
+ // downstream components request needs to be mocked
mock = new MockAdapter(axios);
createComponent(mountExtended);
});
@@ -157,4 +208,229 @@ describe('Pipeline schedules form', () => {
expect(findVariableRows()).toHaveLength(1);
});
});
+
+ describe('Button text', () => {
+ it.each`
+ editing | expectedText
+ ${true} | ${'Edit pipeline schedule'}
+ ${false} | ${'Create pipeline schedule'}
+ `(
+ 'button text is $expectedText when editing is $editing',
+ async ({ editing, expectedText }) => {
+ createComponent(shallowMountExtended, editing, [
+ [getPipelineSchedulesQuery, querySuccessHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(findSubmitButton().text()).toBe(expectedText);
+ },
+ );
+ });
+
+ describe('Schedule creation', () => {
+ it('when creating a schedule the query is not called', () => {
+ createComponent();
+
+ expect(querySuccessHandler).not.toHaveBeenCalled();
+ });
+
+ it('does not show loading state when creating new schedule', () => {
+ createComponent();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ describe('schedule creation success', () => {
+ let mock;
+
+ beforeEach(() => {
+ // mock is needed when we fully mount
+ // downstream components request needs to be mocked
+ mock = new MockAdapter(axios);
+ createComponent(mountExtended);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('creates pipeline schedule', async () => {
+ findDescription().element.value = 'My schedule';
+ findDescription().trigger('change');
+
+ findTimezoneDropdown().vm.$emit('input', {
+ formattedTimezone: '[UTC-4] Eastern Time (US & Canada)',
+ identifier: 'America/New_York',
+ });
+
+ findIntervalComponent().vm.$emit('cronValue', '0 16 * * *');
+
+ addVariableToForm();
+
+ findSubmitButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(createMutationHandlerSuccess).toHaveBeenCalledWith({
+ input: {
+ active: true,
+ cron: '0 16 * * *',
+ cronTimezone: 'America/New_York',
+ description: 'My schedule',
+ projectPath: 'gitlab-org/gitlab',
+ ref: 'main',
+ variables: [
+ {
+ key: 'test_var_2',
+ value: '',
+ variableType: 'ENV_VAR',
+ },
+ ],
+ },
+ });
+ expect(visitUrl).toHaveBeenCalledWith('/root/ci-project/-/pipeline_schedules');
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('schedule creation failure', () => {
+ beforeEach(() => {
+ createComponent(shallowMountExtended, false, [
+ [createPipelineScheduleMutation, createMutationHandlerFailed],
+ ]);
+ });
+
+ it('shows error for failed pipeline schedule creation', async () => {
+ findSubmitButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while creating the pipeline schedule.',
+ });
+ });
+ });
+ });
+
+ describe('Schedule editing', () => {
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('shows loading state when editing', async () => {
+ createComponent(shallowMountExtended, true, [
+ [getPipelineSchedulesQuery, querySuccessHandler],
+ ]);
+
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ describe('schedule fetch success', () => {
+ it('fetches schedule and sets form data correctly', async () => {
+ createComponent(mountExtended, true, [[getPipelineSchedulesQuery, querySuccessHandler]]);
+
+ expect(querySuccessHandler).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(findDescription().element.value).toBe(schedule.description);
+ expect(findIntervalComponent().props('initialCronInterval')).toBe(schedule.cron);
+ expect(findTimezoneDropdown().props('value')).toBe(schedule.cronTimezone);
+ expect(findRefSelector().props('value')).toBe(schedule.ref);
+ expect(findVariableRows()).toHaveLength(3);
+ expect(findKeyInputs().at(0).element.value).toBe(variables[0].key);
+ expect(findKeyInputs().at(1).element.value).toBe(variables[1].key);
+ expect(findValueInputs().at(0).element.value).toBe(variables[0].value);
+ expect(findValueInputs().at(1).element.value).toBe(variables[1].value);
+ });
+ });
+
+ it('schedule fetch failure', async () => {
+ createComponent(shallowMountExtended, true, [
+ [getPipelineSchedulesQuery, queryFailedHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while trying to fetch the pipeline schedule.',
+ });
+ });
+
+ it('edit schedule success', async () => {
+ createComponent(mountExtended, true, [
+ [getPipelineSchedulesQuery, querySuccessHandler],
+ [updatePipelineScheduleMutation, updateMutationHandlerSuccess],
+ ]);
+
+ await waitForPromises();
+
+ findDescription().element.value = 'Updated schedule';
+ findDescription().trigger('change');
+
+ findIntervalComponent().vm.$emit('cronValue', '0 22 16 * *');
+
+ // Ensures variable is sent with destroy property set true
+ findRemoveIcons().at(0).vm.$emit('click');
+
+ findSubmitButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(updateMutationHandlerSuccess).toHaveBeenCalledWith({
+ input: {
+ active: schedule.active,
+ cron: '0 22 16 * *',
+ cronTimezone: schedule.cronTimezone,
+ id: schedule.id,
+ ref: schedule.ref,
+ description: 'Updated schedule',
+ variables: [
+ {
+ destroy: true,
+ id: variables[0].id,
+ key: variables[0].key,
+ value: variables[0].value,
+ variableType: variables[0].variableType,
+ },
+ {
+ destroy: false,
+ id: variables[1].id,
+ key: variables[1].key,
+ value: variables[1].value,
+ variableType: variables[1].variableType,
+ },
+ ],
+ },
+ });
+ });
+
+ it('edit schedule failure', async () => {
+ createComponent(shallowMountExtended, true, [
+ [getPipelineSchedulesQuery, querySuccessHandler],
+ [updatePipelineScheduleMutation, updateMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+
+ findSubmitButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while updating the pipeline schedule.',
+ });
+ });
+ });
});
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
index 50008cedd9c..01a19711264 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -57,6 +57,7 @@ describe('Pipeline schedules app', () => {
wrapper = mountExtended(PipelineSchedules, {
provide: {
fullPath: 'gitlab-org/gitlab',
+ newSchedulePath: '/root/ci-project/-/pipeline_schedules/new',
},
mocks: {
$toast,
@@ -101,6 +102,10 @@ describe('Pipeline schedules app', () => {
expect(findLoadingIcon().exists()).toBe(false);
});
+
+ it('new schedule button links to new schedule path', () => {
+ expect(findNewButton().attributes('href')).toBe('/root/ci-project/-/pipeline_schedules/new');
+ });
});
describe('fetching pipeline schedules', () => {
@@ -146,15 +151,13 @@ describe('Pipeline schedules app', () => {
[deletePipelineScheduleMutation, deleteMutationHandlerSuccess],
]);
- jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch');
-
await waitForPromises();
const scheduleId = mockPipelineScheduleNodes[0].id;
findTable().vm.$emit('showDeleteModal', scheduleId);
- expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled();
+ expect(successHandler).toHaveBeenCalledTimes(1);
findDeleteModal().vm.$emit('deleteSchedule');
@@ -163,7 +166,7 @@ describe('Pipeline schedules app', () => {
expect(deleteMutationHandlerSuccess).toHaveBeenCalledWith({
id: scheduleId,
});
- expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled();
+ expect(successHandler).toHaveBeenCalledTimes(2);
expect($toast.show).toHaveBeenCalledWith('Pipeline schedule successfully deleted.');
});
@@ -252,15 +255,13 @@ describe('Pipeline schedules app', () => {
[takeOwnershipMutation, takeOwnershipMutationHandlerSuccess],
]);
- jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch');
-
await waitForPromises();
const scheduleId = mockPipelineScheduleNodes[1].id;
findTable().vm.$emit('showTakeOwnershipModal', scheduleId);
- expect(wrapper.vm.$apollo.queries.schedules.refetch).not.toHaveBeenCalled();
+ expect(successHandler).toHaveBeenCalledTimes(1);
findTakeOwnershipModal().vm.$emit('takeOwnership');
@@ -269,7 +270,7 @@ describe('Pipeline schedules app', () => {
expect(takeOwnershipMutationHandlerSuccess).toHaveBeenCalledWith({
id: scheduleId,
});
- expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalled();
+ expect(successHandler).toHaveBeenCalledTimes(2);
expect($toast.show).toHaveBeenCalledWith('Successfully taken ownership from Admin.');
});
@@ -297,7 +298,7 @@ describe('Pipeline schedules app', () => {
describe('pipeline schedule tabs', () => {
beforeEach(async () => {
- createComponent();
+ createComponent([[getPipelineSchedulesQuery, successHandler]]);
await waitForPromises();
});
@@ -315,13 +316,23 @@ describe('Pipeline schedules app', () => {
});
it('should refetch the schedules query on a tab click', async () => {
- jest.spyOn(wrapper.vm.$apollo.queries.schedules, 'refetch').mockImplementation(jest.fn());
-
- expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalledTimes(0);
+ expect(successHandler).toHaveBeenCalledTimes(1);
await findAllTab().trigger('click');
- expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalledTimes(1);
+ expect(successHandler).toHaveBeenCalledTimes(3);
+ });
+
+ it('all tab click should not send scope value with query', async () => {
+ findAllTab().trigger('click');
+
+ await nextTick();
+
+ expect(successHandler).toHaveBeenCalledWith({
+ ids: null,
+ projectPath: 'gitlab-org/gitlab',
+ status: null,
+ });
});
});
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
index be0052fc7cf..5eca355fcf4 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
@@ -1,6 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelineScheduleActions from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import {
mockPipelineScheduleNodes,
mockPipelineScheduleCurrentUser,
@@ -28,6 +29,7 @@ describe('Pipeline schedule actions', () => {
const findDeleteBtn = () => wrapper.findByTestId('delete-pipeline-schedule-btn');
const findTakeOwnershipBtn = () => wrapper.findByTestId('take-ownership-pipeline-schedule-btn');
const findPlayScheduleBtn = () => wrapper.findByTestId('play-pipeline-schedule-btn');
+ const findEditScheduleBtn = () => wrapper.findByTestId('edit-pipeline-schedule-btn');
it('displays buttons when user is the owner of schedule and has adminPipelineSchedule permissions', () => {
createComponent();
@@ -76,4 +78,15 @@ describe('Pipeline schedule actions', () => {
playPipelineSchedule: [[mockPipelineScheduleNodes[0].id]],
});
});
+
+ it('edit button links to edit schedule path', () => {
+ createComponent();
+
+ const { schedule } = defaultProps;
+ const id = getIdFromGraphQLId(schedule.id);
+
+ const expectedPath = `${schedule.editPath}?id=${id}`;
+
+ expect(findEditScheduleBtn().attributes('href')).toBe(expectedPath);
+ });
});
diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 1485f6beea4..0a4f233f199 100644
--- a/spec/frontend/ci/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -2,6 +2,7 @@
import mockGetPipelineSchedulesGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.json';
import mockGetPipelineSchedulesAsGuestGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.as_guest.json';
import mockGetPipelineSchedulesTakeOwnershipGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.take_ownership.json';
+import mockGetSinglePipelineScheduleGraphQLResponse from 'test_fixtures/graphql/pipeline_schedules/get_pipeline_schedules.query.graphql.single.json';
const {
data: {
@@ -30,15 +31,22 @@ const {
export const mockPipelineScheduleNodes = nodes;
export const mockPipelineScheduleCurrentUser = currentUser;
-
export const mockPipelineScheduleAsGuestNodes = guestNodes;
-
export const mockTakeOwnershipNodes = takeOwnershipNodes;
+export const mockSinglePipelineScheduleNode = mockGetSinglePipelineScheduleGraphQLResponse;
+
export const emptyPipelineSchedulesResponse = {
data: {
+ currentUser: {
+ id: 'gid://gitlab/User/1',
+ username: 'root',
+ },
project: {
id: 'gid://gitlab/Project/1',
- pipelineSchedules: { nodes: [], count: 0 },
+ pipelineSchedules: {
+ count: 0,
+ nodes: [],
+ },
},
},
};
@@ -79,4 +87,24 @@ export const takeOwnershipMutationResponse = {
},
};
+export const createScheduleMutationResponse = {
+ data: {
+ pipelineScheduleCreate: {
+ clientMutationId: null,
+ errors: [],
+ __typename: 'PipelineScheduleCreatePayload',
+ },
+ },
+};
+
+export const updateScheduleMutationResponse = {
+ data: {
+ pipelineScheduleUpdate: {
+ clientMutationId: null,
+ errors: [],
+ __typename: 'PipelineScheduleUpdatePayload',
+ },
+ },
+};
+
export { mockGetPipelineSchedulesGraphQLResponse };
diff --git a/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap b/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
deleted file mode 100644
index 311a67a3e31..00000000000
--- a/spec/frontend/ci/reports/components/__snapshots__/grouped_issues_list_spec.js.snap
+++ /dev/null
@@ -1,26 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Grouped Issues List renders a smart virtual list with the correct props 1`] = `
-Object {
- "length": 4,
- "remain": 20,
- "rtag": "div",
- "size": 32,
- "wclass": "report-block-list",
- "wtag": "ul",
-}
-`;
-
-exports[`Grouped Issues List with data renders a report item with the correct props 1`] = `
-Object {
- "component": "CodequalityIssueBody",
- "iconComponent": "IssueStatusIcon",
- "isNew": false,
- "issue": Object {
- "name": "foo",
- },
- "showReportSectionStatusIcon": false,
- "status": "none",
- "statusIconSize": 24,
-}
-`;
diff --git a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js b/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
deleted file mode 100644
index 8beec220802..00000000000
--- a/spec/frontend/ci/reports/components/grouped_issues_list_spec.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import GroupedIssuesList from '~/ci/reports/components/grouped_issues_list.vue';
-import ReportItem from '~/ci/reports/components/report_item.vue';
-import SmartVirtualList from '~/vue_shared/components/smart_virtual_list.vue';
-
-describe('Grouped Issues List', () => {
- let wrapper;
-
- const createComponent = ({ propsData = {}, stubs = {} } = {}) => {
- wrapper = shallowMount(GroupedIssuesList, {
- propsData,
- stubs,
- });
- };
-
- const findHeading = (groupName) => wrapper.find(`[data-testid="${groupName}Heading"`);
-
- it('renders a smart virtual list with the correct props', () => {
- createComponent({
- propsData: {
- resolvedIssues: [{ name: 'foo' }],
- unresolvedIssues: [{ name: 'bar' }],
- },
- stubs: {
- SmartVirtualList,
- },
- });
-
- expect(wrapper.findComponent(SmartVirtualList).props()).toMatchSnapshot();
- });
-
- describe('without data', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it.each(['unresolved', 'resolved'])('does not a render a header for %s issues', (issueName) => {
- expect(findHeading(issueName).exists()).toBe(false);
- });
-
- it.each(['resolved', 'unresolved'])('does not render report items for %s issues', () => {
- expect(wrapper.findComponent(ReportItem).exists()).toBe(false);
- });
- });
-
- describe('with data', () => {
- it.each`
- givenIssues | givenHeading | groupName
- ${[{ name: 'foo issue' }]} | ${'Foo Heading'} | ${'resolved'}
- ${[{ name: 'bar issue' }]} | ${'Bar Heading'} | ${'unresolved'}
- `('renders the heading for $groupName issues', ({ givenIssues, givenHeading, groupName }) => {
- createComponent({
- propsData: { [`${groupName}Issues`]: givenIssues, [`${groupName}Heading`]: givenHeading },
- });
-
- expect(findHeading(groupName).text()).toBe(givenHeading);
- });
-
- it.each(['resolved', 'unresolved'])('renders all %s issues', (issueName) => {
- const issues = [{ name: 'foo' }, { name: 'bar' }];
-
- createComponent({
- propsData: { [`${issueName}Issues`]: issues },
- });
-
- expect(wrapper.findAllComponents(ReportItem)).toHaveLength(issues.length);
- });
-
- it('renders a report item with the correct props', () => {
- createComponent({
- propsData: {
- resolvedIssues: [{ name: 'foo' }],
- component: 'CodequalityIssueBody',
- },
- stubs: {
- ReportItem,
- },
- });
-
- expect(wrapper.findComponent(ReportItem).props()).toMatchSnapshot();
- });
- });
-});
diff --git a/spec/frontend/ci/reports/components/summary_row_spec.js b/spec/frontend/ci/reports/components/summary_row_spec.js
deleted file mode 100644
index b1ae9e26b5b..00000000000
--- a/spec/frontend/ci/reports/components/summary_row_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import SummaryRow from '~/ci/reports/components/summary_row.vue';
-
-describe('Summary row', () => {
- let wrapper;
-
- const summary = 'SAST detected 1 new vulnerability and 1 fixed vulnerability';
- const popoverOptions = {
- title: 'Static Application Security Testing (SAST)',
- content: '<a>Learn more about SAST</a>',
- };
- const statusIcon = 'warning';
-
- const createComponent = ({ props = {}, slots = {} } = {}) => {
- wrapper = extendedWrapper(
- mount(SummaryRow, {
- propsData: {
- summary,
- popoverOptions,
- statusIcon,
- ...props,
- },
- slots,
- }),
- );
- };
-
- const findSummary = () => wrapper.findByTestId('summary-row-description');
- const findStatusIcon = () => wrapper.findByTestId('summary-row-icon');
- const findHelpPopover = () => wrapper.findComponent(HelpPopover);
-
- it('renders provided summary', () => {
- createComponent();
- expect(findSummary().text()).toContain(summary);
- });
-
- it('renders provided icon', () => {
- createComponent();
- expect(findStatusIcon().classes()).toContain('js-ci-status-icon-warning');
- });
-
- it('renders help popover if popoverOptions are provided', () => {
- createComponent();
- expect(findHelpPopover().props('options')).toEqual(popoverOptions);
- });
-
- it('does not render help popover if popoverOptions are not provided', () => {
- createComponent({ props: { popoverOptions: null } });
- expect(findHelpPopover().exists()).toBe(false);
- });
-
- describe('summary slot', () => {
- it('replaces the summary prop', () => {
- const summarySlotContent = 'Summary slot content';
- createComponent({ slots: { summary: summarySlotContent } });
-
- expect(wrapper.text()).not.toContain(summary);
- expect(findSummary().text()).toContain(summarySlotContent);
- });
- });
-});
diff --git a/spec/frontend/ci/reports/mock_data/mock_data.js b/spec/frontend/ci/reports/mock_data/mock_data.js
index 2599b0ac365..2983a9f1125 100644
--- a/spec/frontend/ci/reports/mock_data/mock_data.js
+++ b/spec/frontend/ci/reports/mock_data/mock_data.js
@@ -1,3 +1,6 @@
+import { SEVERITIES as SEVERITIES_CODE_QUALITY } from '~/ci/reports/codequality_report/constants';
+import { SEVERITIES as SEVERITIES_SAST } from '~/ci/reports/sast/constants';
+
export const failedIssue = {
result: 'failure',
name: 'Test#sum when a is 1 and b is 2 returns summary',
@@ -36,3 +39,54 @@ export const failedReport = {
},
],
};
+
+export const findingSastInfo = {
+ scale: 'sast',
+ severity: 'info',
+};
+
+export const findingSastInfoEnhanced = {
+ scale: 'sast',
+ severity: 'info',
+ class: SEVERITIES_SAST.info.class,
+ name: SEVERITIES_SAST.info.name,
+};
+
+export const findingsCodeQualityBlocker = {
+ scale: 'codeQuality',
+ severity: 'blocker',
+};
+
+export const findingCodeQualityBlockerEnhanced = {
+ scale: 'codeQuality',
+ severity: 'blocker',
+ class: SEVERITIES_CODE_QUALITY.blocker.class,
+ name: SEVERITIES_CODE_QUALITY.blocker.name,
+};
+
+export const findingCodeQualityInfo = {
+ scale: 'codeQuality',
+ severity: 'info',
+};
+
+export const findingCodeQualityInfoEnhanced = {
+ scale: 'codeQuality',
+ severity: 'info',
+ class: SEVERITIES_CODE_QUALITY.info.class,
+ name: SEVERITIES_CODE_QUALITY.info.name,
+};
+
+export const findingUnknownInfo = {
+ scale: 'codeQuality',
+ severity: 'info',
+};
+
+export const findingUnknownInfoEnhanced = {
+ scale: 'codeQuality',
+ severity: 'info',
+ class: SEVERITIES_CODE_QUALITY.info.class,
+ name: SEVERITIES_CODE_QUALITY.info.name,
+};
+
+export const findingsArray = [findingSastInfo, findingsCodeQualityBlocker];
+export const findingsArrayEnhanced = [findingSastInfoEnhanced, findingCodeQualityBlockerEnhanced];
diff --git a/spec/frontend/ci/reports/utils_spec.js b/spec/frontend/ci/reports/utils_spec.js
new file mode 100644
index 00000000000..e01aa903a97
--- /dev/null
+++ b/spec/frontend/ci/reports/utils_spec.js
@@ -0,0 +1,30 @@
+import { getSeverity } from '~/ci/reports/utils';
+
+import {
+ findingSastInfo,
+ findingSastInfoEnhanced,
+ findingCodeQualityInfo,
+ findingCodeQualityInfoEnhanced,
+ findingUnknownInfo,
+ findingUnknownInfoEnhanced,
+ findingsArray,
+ findingsArrayEnhanced,
+} from './mock_data/mock_data';
+
+describe('getSeverity utility function', () => {
+ it('should enhance finding with sast scale', () => {
+ expect(getSeverity(findingSastInfo)).toEqual(findingSastInfoEnhanced);
+ });
+
+ it('should enhance finding with codequality scale', () => {
+ expect(getSeverity(findingCodeQualityInfo)).toEqual(findingCodeQualityInfoEnhanced);
+ });
+
+ it('should use codeQuality scale when scale is unknown', () => {
+ expect(getSeverity(findingUnknownInfo)).toEqual(findingUnknownInfoEnhanced);
+ });
+
+ it('should correctly enhance an array of findings', () => {
+ expect(getSeverity(findingsArray)).toEqual(findingsArrayEnhanced);
+ });
+});
diff --git a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
index c4ed6d1bdb5..c9349c64bfb 100644
--- a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -9,10 +9,8 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerHeaderActions from '~/ci/runner/components/runner_header_actions.vue';
import RunnerDetails from '~/ci/runner/components/runner_details.vue';
-import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
-import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
-import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue';
import RunnersJobs from '~/ci/runner/components/runner_jobs.vue';
@@ -46,9 +44,7 @@ describe('AdminRunnerShowApp', () => {
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
- const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
- const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
- const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+ const findRunnerHeaderActions = () => wrapper.findComponent(RunnerHeaderActions);
const findRunnerDetailsTabs = () => wrapper.findComponent(RunnerDetailsTabs);
const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
@@ -94,9 +90,10 @@ describe('AdminRunnerShowApp', () => {
});
it('displays the runner edit and pause buttons', () => {
- expect(findRunnerEditButton().attributes('href')).toBe(mockRunner.editAdminUrl);
- expect(findRunnerPauseButton().exists()).toBe(true);
- expect(findRunnerDeleteButton().exists()).toBe(true);
+ expect(findRunnerHeaderActions().props()).toEqual({
+ runner: mockRunner,
+ editPath: mockRunner.editAdminUrl,
+ });
});
it('shows runner details', () => {
@@ -122,54 +119,6 @@ describe('AdminRunnerShowApp', () => {
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
});
- describe('when runner cannot be updated', () => {
- beforeEach(async () => {
- mockRunnerQueryResult({
- userPermissions: {
- ...mockRunner.userPermissions,
- updateRunner: false,
- },
- });
-
- await createComponent({
- mountFn: mountExtended,
- });
- });
-
- it('does not display the runner edit and pause buttons', () => {
- expect(findRunnerEditButton().exists()).toBe(false);
- expect(findRunnerPauseButton().exists()).toBe(false);
- });
-
- it('displays delete button', () => {
- expect(findRunnerDeleteButton().exists()).toBe(true);
- });
- });
-
- describe('when runner cannot be deleted', () => {
- beforeEach(async () => {
- mockRunnerQueryResult({
- userPermissions: {
- ...mockRunner.userPermissions,
- deleteRunner: false,
- },
- });
-
- await createComponent({
- mountFn: mountExtended,
- });
- });
-
- it('does not display the delete button', () => {
- expect(findRunnerDeleteButton().exists()).toBe(false);
- });
-
- it('displays edit and pause buttons', () => {
- expect(findRunnerEditButton().exists()).toBe(true);
- expect(findRunnerPauseButton().exists()).toBe(true);
- });
- });
-
describe('when runner is deleted', () => {
beforeEach(async () => {
await createComponent({
@@ -178,7 +127,7 @@ describe('AdminRunnerShowApp', () => {
});
it('redirects to the runner list page', () => {
- findRunnerDeleteButton().vm.$emit('deleted', { message: 'Runner deleted' });
+ findRunnerHeaderActions().vm.$emit('deleted', { message: 'Runner deleted' });
expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
message: 'Runner deleted',
@@ -187,23 +136,6 @@ describe('AdminRunnerShowApp', () => {
expect(visitUrl).toHaveBeenCalledWith(mockRunnersPath);
});
});
-
- describe('when runner does not have an edit url', () => {
- beforeEach(async () => {
- mockRunnerQueryResult({
- editAdminUrl: null,
- });
-
- await createComponent({
- mountFn: mountExtended,
- });
- });
-
- it('does not display the runner edit button', () => {
- expect(findRunnerEditButton().exists()).toBe(false);
- expect(findRunnerPauseButton().exists()).toBe(true);
- });
- });
});
describe('When loading', () => {
diff --git a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
index fc74e2947b6..1bbcb991619 100644
--- a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
@@ -156,7 +156,7 @@ describe('AdminRunnersApp', () => {
await createComponent({ mountFn: mountExtended });
});
- // https://gitlab.com/gitlab-org/gitlab/-/issues/414975
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/414975
// eslint-disable-next-line jest/no-disabled-tests
it.skip('fetches counts', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
index cda3876f9b2..ad20d7682ed 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
@@ -1,5 +1,6 @@
+import { GlSprintf } from '@gitlab/ui';
import { __, sprintf } from '~/locale';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -31,8 +32,8 @@ describe('RunnerTypeCell', () => {
wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon)
.wrappers[0];
- const createComponent = (runner, options) => {
- wrapper = mountExtended(RunnerSummaryCell, {
+ const createComponent = ({ runner, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(RunnerSummaryCell, {
propsData: {
runner: {
...mockRunner,
@@ -40,7 +41,7 @@ describe('RunnerTypeCell', () => {
},
},
stubs: {
- RunnerSummaryField,
+ GlSprintf,
},
...options,
});
@@ -51,6 +52,8 @@ describe('RunnerTypeCell', () => {
});
it('Displays the runner name as id and short token', () => {
+ createComponent({ mountFn: mountExtended });
+
expect(wrapper.text()).toContain(
`#${getIdFromGraphQLId(mockRunner.id)} (${mockRunner.shortSha})`,
);
@@ -58,13 +61,16 @@ describe('RunnerTypeCell', () => {
it('Displays no runner manager count', () => {
createComponent({
- managers: { count: 0 },
+ runner: { managers: { nodes: { count: 0 } } },
+ mountFn: mountExtended,
});
expect(findRunnerManagersBadge().html()).toBe('');
});
it('Displays runner manager count', () => {
+ createComponent({ mountFn: mountExtended });
+
expect(findRunnerManagersBadge().text()).toBe('2');
});
@@ -74,8 +80,8 @@ describe('RunnerTypeCell', () => {
it('Displays the locked icon for locked runners', () => {
createComponent({
- runnerType: PROJECT_TYPE,
- locked: true,
+ runner: { runnerType: PROJECT_TYPE, locked: true },
+ mountFn: mountExtended,
});
expect(findLockIcon().exists()).toBe(true);
@@ -83,8 +89,8 @@ describe('RunnerTypeCell', () => {
it('Displays the runner type', () => {
createComponent({
- runnerType: INSTANCE_TYPE,
- locked: true,
+ runner: { runnerType: INSTANCE_TYPE, locked: true },
+ mountFn: mountExtended,
});
expect(wrapper.text()).toContain(I18N_INSTANCE_TYPE);
@@ -101,7 +107,7 @@ describe('RunnerTypeCell', () => {
it('Displays "No description" for missing runner description', () => {
createComponent({
- description: null,
+ runner: { description: null },
});
expect(wrapper.findByText(I18N_NO_DESCRIPTION).classes()).toContain('gl-text-secondary');
@@ -109,7 +115,7 @@ describe('RunnerTypeCell', () => {
it('Displays last contact', () => {
createComponent({
- contactedAt: '2022-01-02',
+ runner: { contactedAt: '2022-01-02' },
});
expect(findRunnerSummaryField('clock').findComponent(TimeAgo).props('time')).toBe('2022-01-02');
@@ -124,20 +130,46 @@ describe('RunnerTypeCell', () => {
expect(findRunnerSummaryField('clock').text()).toContain(__('Never'));
});
- it('Displays ip address', () => {
- createComponent({
- ipAddress: '127.0.0.1',
+ describe('IP address', () => {
+ it('with no managers', () => {
+ createComponent({
+ runner: {
+ managers: { count: 0, nodes: [] },
+ },
+ });
+
+ expect(findRunnerSummaryField('disk')).toBeUndefined();
});
- expect(findRunnerSummaryField('disk').text()).toContain('127.0.0.1');
- });
+ it('with no ip', () => {
+ createComponent({
+ runner: {
+ managers: { count: 1, nodes: [{ ipAddress: null }] },
+ },
+ });
- it('Displays no ip address', () => {
- createComponent({
- ipAddress: null,
+ expect(findRunnerSummaryField('disk')).toBeUndefined();
});
- expect(findRunnerSummaryField('disk')).toBeUndefined();
+ it.each`
+ count | ipAddress | expected
+ ${1} | ${'127.0.0.1'} | ${'127.0.0.1'}
+ ${2} | ${'127.0.0.2'} | ${'127.0.0.2 (+1)'}
+ ${11} | ${'127.0.0.3'} | ${'127.0.0.3 (+10)'}
+ ${1001} | ${'127.0.0.4'} | ${'127.0.0.4 (+1,000)'}
+ `(
+ 'with $count managers, ip $ipAddress displays $expected',
+ ({ count, ipAddress, expected }) => {
+ createComponent({
+ runner: {
+ // `first: 1` is requested, `count` varies when there are more managers
+ managers: { count, nodes: [{ ipAddress }] },
+ },
+ });
+
+ expect(findRunnerSummaryField('disk').text()).toMatchInterpolatedText(expected);
+ },
+ );
});
it('Displays job count', () => {
@@ -146,7 +178,7 @@ describe('RunnerTypeCell', () => {
it('Formats large job counts', () => {
createComponent({
- jobCount: 1000,
+ runner: { jobCount: 1000 },
});
expect(findRunnerSummaryField('pipeline').text()).toContain('1,000');
@@ -154,7 +186,7 @@ describe('RunnerTypeCell', () => {
it('Formats large job counts with a plus symbol', () => {
createComponent({
- jobCount: 1001,
+ runner: { jobCount: 1001 },
});
expect(findRunnerSummaryField('pipeline').text()).toContain('1,000+');
@@ -165,7 +197,7 @@ describe('RunnerTypeCell', () => {
it('Displays created at ...', () => {
createComponent({
- createdBy: null,
+ runner: { createdBy: null },
});
expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
@@ -177,12 +209,15 @@ describe('RunnerTypeCell', () => {
});
it('Displays created at ... by ...', () => {
+ createComponent({ mountFn: mountExtended });
+
expect(findRunnerSummaryField('calendar').text()).toMatchInterpolatedText(
sprintf(I18N_CREATED_AT_BY_LABEL, {
timeAgo: findCreatedTime().text(),
avatar: mockRunner.createdBy.username,
}),
);
+
expect(findCreatedTime().props('time')).toBe(mockRunner.createdAt);
});
@@ -200,7 +235,7 @@ describe('RunnerTypeCell', () => {
it('Displays tag list', () => {
createComponent({
- tagList: ['shell', 'linux'],
+ runner: { tagList: ['shell', 'linux'] },
});
expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']);
@@ -209,14 +244,11 @@ describe('RunnerTypeCell', () => {
it('Displays a custom runner-name slot', () => {
const slotContent = 'My custom runner name';
- createComponent(
- {},
- {
- slots: {
- 'runner-name': slotContent,
- },
+ createComponent({
+ slots: {
+ 'runner-name': slotContent,
},
- );
+ });
expect(wrapper.text()).toContain(slotContent);
});
diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index e564cf49ca0..e4373d1c198 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -1,4 +1,10 @@
-import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm, GlIcon } from '@gitlab/ui';
+import {
+ GlModal,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlDropdownForm,
+ GlIcon,
+} from '@gitlab/ui';
import { createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -35,13 +41,16 @@ Vue.use(VueApollo);
describe('RegistrationDropdown', () => {
let wrapper;
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDropdownBtn = () => findDropdown().find('button');
- const findRegistrationInstructionsDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findRegistrationInstructionsDropdownItem = () =>
+ wrapper.findComponent(GlDisclosureDropdownItem);
const findTokenDropdownItem = () => wrapper.findComponent(GlDropdownForm);
const findRegistrationToken = () => wrapper.findComponent(RegistrationToken);
const findRegistrationTokenInput = () =>
- wrapper.findByLabelText(RegistrationToken.i18n.registrationToken);
+ wrapper.findByLabelText(
+ `${RegistrationToken.i18n.registrationToken} ${RegistrationDropdown.i18n.supportForRegistrationTokensDeprecated}`,
+ );
const findTokenResetDropdownItem = () =>
wrapper.findComponent(RegistrationTokenResetDropdownItem);
const findModal = () => wrapper.findComponent(GlModal);
@@ -52,9 +61,8 @@ describe('RegistrationDropdown', () => {
.replace(/[\n\t\s]+/g, ' ');
const openModal = async () => {
- await findRegistrationInstructionsDropdownItem().trigger('click');
+ await findRegistrationInstructionsDropdownItem().vm.$emit('action');
findModal().vm.$emit('shown');
-
await waitForPromises();
};
@@ -65,6 +73,9 @@ describe('RegistrationDropdown', () => {
type: INSTANCE_TYPE,
...props,
},
+ stubs: {
+ GlDisclosureDropdownItem,
+ },
...options,
});
};
@@ -107,12 +118,12 @@ describe('RegistrationDropdown', () => {
createComponent();
expect(findDropdown().props()).toMatchObject({
- category: 'primary',
- variant: 'confirm',
+ category: 'tertiary',
+ variant: 'default',
});
expect(findDropdown().attributes()).toMatchObject({
- toggleclass: '',
+ toggleclass: 'gl-px-3!',
});
});
@@ -186,6 +197,26 @@ describe('RegistrationDropdown', () => {
});
});
+ describe('Dropdown is expanded', () => {
+ beforeEach(() => {
+ createComponent({}, mountExtended);
+ findDropdownBtn().vm.$emit('click');
+ });
+
+ it('has aria-expanded set to true', () => {
+ expect(findDropdownBtn().attributes('aria-expanded')).toBe('true');
+ });
+
+ describe('when token is copied', () => {
+ it('should close dropdown', async () => {
+ findRegistrationToken().vm.$emit('copy');
+ await nextTick();
+
+ expect(findDropdownBtn().attributes('aria-expanded')).toBeUndefined();
+ });
+ });
+ });
+
describe('When token is reset', () => {
const newToken = 'mock1';
@@ -217,19 +248,15 @@ describe('RegistrationDropdown', () => {
});
});
- describe.each([
- { createRunnerWorkflowForAdmin: true },
- { createRunnerWorkflowForNamespace: true },
- ])('When showing a "deprecated" warning', (glFeatures) => {
+ describe('When showing a "deprecated" warning', () => {
it('passes deprecated variant props and attributes to dropdown', () => {
- createComponent({
- provide: { glFeatures },
- });
+ createComponent();
expect(findDropdown().props()).toMatchObject({
category: 'tertiary',
variant: 'default',
- text: '',
+ toggleText: I18N_REGISTER_INSTANCE_TYPE,
+ textSrOnly: true,
});
expect(findDropdown().attributes()).toMatchObject({
@@ -249,12 +276,7 @@ describe('RegistrationDropdown', () => {
});
it('shows warning text', () => {
- createComponent(
- {
- provide: { glFeatures },
- },
- mountExtended,
- );
+ createComponent({}, mountExtended);
const text = wrapper.findByText(s__('Runners|Support for registration tokens is deprecated'));
@@ -262,12 +284,7 @@ describe('RegistrationDropdown', () => {
});
it('button shows ellipsis icon', () => {
- createComponent(
- {
- provide: { glFeatures },
- },
- mountExtended,
- );
+ createComponent({}, mountExtended);
expect(findDropdownBtn().findComponent(GlIcon).props('name')).toBe('ellipsis_v');
expect(findDropdownBtn().findAllComponents(GlIcon)).toHaveLength(1);
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
index db54bf0c80e..d599bc1291c 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_reset_dropdown_item_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui';
+import { GlDisclosureDropdownItem, GlLoadingIcon, GlToast, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -27,7 +27,7 @@ describe('RegistrationTokenResetDropdownItem', () => {
let showToast;
const mockEvent = { preventDefault: jest.fn() };
- const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findModal = () => wrapper.findComponent(GlModal);
const clickSubmit = () => findModal().vm.$emit('primary', mockEvent);
diff --git a/spec/frontend/ci/runner/components/registration/registration_token_spec.js b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
index 869c032c0b5..fd3896d5500 100644
--- a/spec/frontend/ci/runner/components/registration/registration_token_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_token_spec.js
@@ -7,7 +7,7 @@ import { mockRegistrationToken } from '../../mock_data';
describe('RegistrationToken', () => {
let wrapper;
- let showToast;
+ const showToastMock = jest.fn();
Vue.use(GlToast);
@@ -21,9 +21,12 @@ describe('RegistrationToken', () => {
...props,
},
...options,
+ mocks: {
+ $toast: {
+ show: showToastMock,
+ },
+ },
});
-
- showToast = wrapper.vm.$toast ? jest.spyOn(wrapper.vm.$toast, 'show') : null;
};
it('Displays value and copy button', () => {
@@ -58,8 +61,14 @@ describe('RegistrationToken', () => {
it('shows a copied message', () => {
findInputCopyToggleVisibility().vm.$emit('copy');
- expect(showToast).toHaveBeenCalledTimes(1);
- expect(showToast).toHaveBeenCalledWith('Registration token copied!');
+ expect(showToastMock).toHaveBeenCalledTimes(1);
+ expect(showToastMock).toHaveBeenCalledWith('Registration token copied!');
+ });
+
+ it('emits a copy event', () => {
+ findInputCopyToggleVisibility().vm.$emit('copy');
+
+ expect(wrapper.emitted('copy')).toHaveLength(1);
});
});
@@ -76,9 +85,7 @@ describe('RegistrationToken', () => {
});
it('passes slots to the input component', () => {
- const slot = findInputCopyToggleVisibility().vm.$scopedSlots[slotName];
-
- expect(slot()[0].text).toBe(slotContent);
+ expect(findInputCopyToggleVisibility().text()).toBe(slotContent);
});
});
});
diff --git a/spec/frontend/ci/runner/components/runner_delete_action_spec.js b/spec/frontend/ci/runner/components/runner_delete_action_spec.js
new file mode 100644
index 00000000000..d6617e6e75c
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_delete_action_spec.js
@@ -0,0 +1,223 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql';
+import waitForPromises from 'helpers/wait_for_promises';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { createAlert } from '~/alert';
+
+import RunnerDeleteAction from '~/ci/runner/components/runner_delete_action.vue';
+import RunnerDeleteModal from '~/ci/runner/components/runner_delete_modal.vue';
+import { allRunnersData } from '../mock_data';
+
+const mockRunner = allRunnersData.data.runners.nodes[0];
+const mockRunnerId = getIdFromGraphQLId(mockRunner.id);
+const mockRunnerName = `#${mockRunnerId} (${mockRunner.shortSha})`;
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
+jest.mock('~/ci/runner/sentry_utils');
+
+describe('RunnerDeleteAction', () => {
+ let wrapper;
+ let apolloProvider;
+ let apolloCache;
+ let runnerDeleteHandler;
+ let mockModalShow;
+
+ const findBtn = () => wrapper.find('button');
+ const findModal = () => wrapper.findComponent(RunnerDeleteModal);
+
+ const createComponent = ({ props = {} } = {}) => {
+ const { runner, ...propsData } = props;
+
+ wrapper = shallowMountExtended(RunnerDeleteAction, {
+ propsData: {
+ runner: {
+ // We need typename so that cache.identify works
+ // eslint-disable-next-line no-underscore-dangle
+ __typename: mockRunner.__typename,
+ id: mockRunner.id,
+ shortSha: mockRunner.shortSha,
+ ...runner,
+ },
+ ...propsData,
+ },
+ apolloProvider,
+ stubs: {
+ RunnerDeleteModal: stubComponent(RunnerDeleteModal, {
+ methods: {
+ show: mockModalShow,
+ },
+ }),
+ },
+ scopedSlots: {
+ default: '<button :disabled="props.loading" @click="props.onClick"/>',
+ },
+ });
+ };
+
+ const clickOkAndWait = async () => {
+ findModal().vm.$emit('primary');
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ mockModalShow = jest.fn();
+
+ runnerDeleteHandler = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ runnerDelete: {
+ errors: [],
+ },
+ },
+ });
+ });
+ apolloProvider = createMockApollo([[runnerDeleteMutation, runnerDeleteHandler]]);
+ apolloCache = apolloProvider.defaultClient.cache;
+
+ jest.spyOn(apolloCache, 'evict');
+ jest.spyOn(apolloCache, 'gc');
+
+ createComponent();
+ });
+
+ it('Displays an action in the slot', () => {
+ expect(findBtn().exists()).toBe(true);
+ });
+
+ it('Displays a modal with the runner name', () => {
+ expect(findModal().props('runnerName')).toBe(mockRunnerName);
+ });
+
+ it('Displays a modal with the runner manager count', () => {
+ createComponent({
+ props: {
+ runner: { managers: { count: 2 } },
+ },
+ });
+
+ expect(findModal().props('managersCount')).toBe(2);
+ });
+
+ it('Displays a modal when action is triggered', async () => {
+ await findBtn().trigger('click');
+
+ expect(mockModalShow).toHaveBeenCalled();
+ });
+
+ describe('Before the delete button is clicked', () => {
+ it('The mutation has not been called', () => {
+ expect(runnerDeleteHandler).toHaveBeenCalledTimes(0);
+ });
+ });
+
+ describe('Immediately after the delete button is clicked', () => {
+ beforeEach(() => {
+ findModal().vm.$emit('primary');
+ });
+
+ it('The button has a loading state', () => {
+ expect(findBtn().attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('After clicking on the delete button', () => {
+ beforeEach(async () => {
+ await clickOkAndWait();
+ });
+
+ it('The mutation to delete is called', () => {
+ expect(runnerDeleteHandler).toHaveBeenCalledTimes(1);
+ expect(runnerDeleteHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockRunner.id,
+ },
+ });
+ });
+
+ it('The user can be notified with an event', () => {
+ const done = wrapper.emitted('done');
+
+ expect(done).toHaveLength(1);
+ expect(done[0][0].message).toMatch(`#${mockRunnerId}`);
+ expect(done[0][0].message).toMatch(`${mockRunner.shortSha}`);
+ });
+
+ it('evicts runner from apollo cache', () => {
+ expect(apolloCache.evict).toHaveBeenCalledWith({
+ id: apolloCache.identify(mockRunner),
+ });
+ expect(apolloCache.gc).toHaveBeenCalled();
+ });
+ });
+
+ describe('When update fails', () => {
+ describe('On a network error', () => {
+ const mockErrorMsg = 'Update error!';
+
+ beforeEach(async () => {
+ runnerDeleteHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
+
+ await clickOkAndWait();
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(mockErrorMsg),
+ component: 'RunnerDeleteAction',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ title: expect.stringContaining(mockRunnerName),
+ message: mockErrorMsg,
+ });
+ });
+ });
+
+ describe('On a validation error', () => {
+ const mockErrorMsg = 'Runner not found!';
+ const mockErrorMsg2 = 'User not allowed!';
+
+ beforeEach(async () => {
+ runnerDeleteHandler.mockResolvedValueOnce({
+ data: {
+ runnerDelete: {
+ errors: [mockErrorMsg, mockErrorMsg2],
+ },
+ },
+ });
+
+ await clickOkAndWait();
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
+ component: 'RunnerDeleteAction',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ title: expect.stringContaining(mockRunnerName),
+ message: `${mockErrorMsg} ${mockErrorMsg2}`,
+ });
+ });
+
+ it('does not evict runner from apollo cache', () => {
+ expect(apolloCache.evict).not.toHaveBeenCalled();
+ expect(apolloCache.gc).not.toHaveBeenCalled();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_delete_button_spec.js b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
index 3b3f3b1770d..87e857510de 100644
--- a/spec/frontend/ci/runner/components/runner_delete_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_delete_button_spec.js
@@ -1,110 +1,73 @@
-import Vue from 'vue';
import { GlButton } from '@gitlab/ui';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
-import runnerDeleteMutation from '~/ci/runner/graphql/shared/runner_delete.mutation.graphql';
-import waitForPromises from 'helpers/wait_for_promises';
-import { captureException } from '~/ci/runner/sentry_utils';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { createAlert } from '~/alert';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { I18N_DELETE_RUNNER } from '~/ci/runner/constants';
import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
-import RunnerDeleteModal from '~/ci/runner/components/runner_delete_modal.vue';
+import RunnerDeleteAction from '~/ci/runner/components/runner_delete_action.vue';
import { allRunnersData } from '../mock_data';
const mockRunner = allRunnersData.data.runners.nodes[0];
-const mockRunnerId = getIdFromGraphQLId(mockRunner.id);
-const mockRunnerName = `#${mockRunnerId} (${mockRunner.shortSha})`;
-
-Vue.use(VueApollo);
jest.mock('~/alert');
jest.mock('~/ci/runner/sentry_utils');
describe('RunnerDeleteButton', () => {
let wrapper;
- let apolloProvider;
- let apolloCache;
- let runnerDeleteHandler;
const findBtn = () => wrapper.findComponent(GlButton);
- const findModal = () => wrapper.findComponent(RunnerDeleteModal);
-
const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
- const getModal = () => getBinding(findBtn().element, 'gl-modal').value;
- const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
- const { runner, ...propsData } = props;
-
- wrapper = mountFn(RunnerDeleteButton, {
+ const createComponent = ({ props = {}, loading, onClick = jest.fn() } = {}) => {
+ wrapper = shallowMountExtended(RunnerDeleteButton, {
propsData: {
- runner: {
- // We need typename so that cache.identify works
- // eslint-disable-next-line no-underscore-dangle
- __typename: mockRunner.__typename,
- id: mockRunner.id,
- shortSha: mockRunner.shortSha,
- ...runner,
- },
- ...propsData,
+ runner: mockRunner,
+ ...props,
},
- apolloProvider,
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
- GlModal: createMockDirective('gl-modal'),
+ },
+ stubs: {
+ RunnerDeleteAction: stubComponent(RunnerDeleteAction, {
+ render() {
+ return this.$scopedSlots.default({
+ loading,
+ onClick,
+ });
+ },
+ }),
},
});
};
- const clickOkAndWait = async () => {
- findModal().vm.$emit('primary');
- await waitForPromises();
- };
-
beforeEach(() => {
- runnerDeleteHandler = jest.fn().mockImplementation(() => {
- return Promise.resolve({
- data: {
- runnerDelete: {
- errors: [],
- },
- },
- });
- });
- apolloProvider = createMockApollo([[runnerDeleteMutation, runnerDeleteHandler]]);
- apolloCache = apolloProvider.defaultClient.cache;
-
- jest.spyOn(apolloCache, 'evict');
- jest.spyOn(apolloCache, 'gc');
-
createComponent();
});
- it('Displays a delete button without an icon', () => {
+ it('Displays a delete button without a icon or tooltip', () => {
expect(findBtn().props()).toMatchObject({
loading: false,
icon: '',
});
expect(findBtn().classes('btn-icon')).toBe(false);
expect(findBtn().text()).toBe(I18N_DELETE_RUNNER);
- });
- it('Displays a modal with the runner name', () => {
- expect(findModal().props('runnerName')).toBe(mockRunnerName);
+ expect(getTooltip()).toBe('');
});
it('Does not have tabindex when button is enabled', () => {
expect(wrapper.attributes('tabindex')).toBeUndefined();
});
- it('Displays a modal when clicked', () => {
- const modalId = `delete-runner-modal-${mockRunnerId}`;
+ it('Triggers delete when clicked', () => {
+ const mockOnClick = jest.fn();
+
+ createComponent({ onClick: mockOnClick });
+ expect(mockOnClick).not.toHaveBeenCalled();
- expect(getModal()).toBe(modalId);
- expect(findModal().attributes('modal-id')).toBe(modalId);
+ findBtn().vm.$emit('click');
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
});
it('Does not display redundant text for screen readers', () => {
@@ -117,135 +80,41 @@ describe('RunnerDeleteButton', () => {
expect(findBtn().props('category')).toBe('secondary');
});
- describe(`Before the delete button is clicked`, () => {
- it('The mutation has not been called', () => {
- expect(runnerDeleteHandler).toHaveBeenCalledTimes(0);
- });
- });
-
- describe('Immediately after the delete button is clicked', () => {
+ describe('When loading result', () => {
beforeEach(() => {
- findModal().vm.$emit('primary');
+ createComponent({ loading: true });
});
it('The button has a loading state', () => {
expect(findBtn().props('loading')).toBe(true);
});
-
- it('The stale tooltip is removed', () => {
- expect(getTooltip()).toBe('');
- });
});
- describe('After clicking on the delete button', () => {
- beforeEach(async () => {
- await clickOkAndWait();
- });
-
- it('The mutation to delete is called', () => {
- expect(runnerDeleteHandler).toHaveBeenCalledTimes(1);
- expect(runnerDeleteHandler).toHaveBeenCalledWith({
- input: {
- id: mockRunner.id,
- },
- });
- });
-
- it('The user can be notified with an event', () => {
- const deleted = wrapper.emitted('deleted');
-
- expect(deleted).toHaveLength(1);
- expect(deleted[0][0].message).toMatch(`#${mockRunnerId}`);
- expect(deleted[0][0].message).toMatch(`${mockRunner.shortSha}`);
- });
-
- it('evicts runner from apollo cache', () => {
- expect(apolloCache.evict).toHaveBeenCalledWith({
- id: apolloCache.identify(mockRunner),
- });
- expect(apolloCache.gc).toHaveBeenCalled();
- });
- });
+ describe('When done after deleting', () => {
+ const doneEvent = { message: 'done!' };
- describe('When update fails', () => {
- describe('On a network error', () => {
- const mockErrorMsg = 'Update error!';
-
- beforeEach(async () => {
- runnerDeleteHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
-
- await clickOkAndWait();
- });
-
- it('error is reported to sentry', () => {
- expect(captureException).toHaveBeenCalledWith({
- error: new Error(mockErrorMsg),
- component: 'RunnerDeleteButton',
- });
- });
-
- it('error is shown to the user', () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- expect(createAlert).toHaveBeenCalledWith({
- title: expect.stringContaining(mockRunnerName),
- message: mockErrorMsg,
- });
- });
+ beforeEach(() => {
+ wrapper.findComponent(RunnerDeleteAction).vm.$emit('done', doneEvent);
});
- describe('On a validation error', () => {
- const mockErrorMsg = 'Runner not found!';
- const mockErrorMsg2 = 'User not allowed!';
-
- beforeEach(async () => {
- runnerDeleteHandler.mockResolvedValueOnce({
- data: {
- runnerDelete: {
- errors: [mockErrorMsg, mockErrorMsg2],
- },
- },
- });
-
- await clickOkAndWait();
- });
-
- it('error is reported to sentry', () => {
- expect(captureException).toHaveBeenCalledWith({
- error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
- component: 'RunnerDeleteButton',
- });
- });
-
- it('error is shown to the user', () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- expect(createAlert).toHaveBeenCalledWith({
- title: expect.stringContaining(mockRunnerName),
- message: `${mockErrorMsg} ${mockErrorMsg2}`,
- });
- });
-
- it('does not evict runner from apollo cache', () => {
- expect(apolloCache.evict).not.toHaveBeenCalled();
- expect(apolloCache.gc).not.toHaveBeenCalled();
- });
+ it('emits deleted event', () => {
+ expect(wrapper.emitted('deleted')).toEqual([[doneEvent]]);
});
});
- describe('When displaying a compact button for an active runner', () => {
+ describe('When displaying a compact button', () => {
beforeEach(() => {
createComponent({
- props: {
- runner: {
- paused: false,
- },
- compact: true,
- },
- mountFn: mountExtended,
+ props: { compact: true },
});
});
it('Displays no text', () => {
expect(findBtn().text()).toBe('');
+ });
+
+ it('Displays "x" icon', () => {
+ expect(findBtn().props('icon')).toBe('close');
expect(findBtn().classes('btn-icon')).toBe(true);
});
@@ -254,13 +123,12 @@ describe('RunnerDeleteButton', () => {
expect(getTooltip()).toBe(I18N_DELETE_RUNNER);
});
- describe('Immediately after the button is clicked', () => {
+ describe('When loading result', () => {
beforeEach(() => {
- findModal().vm.$emit('primary');
- });
-
- it('The button has a loading state', () => {
- expect(findBtn().props('loading')).toBe(true);
+ createComponent({
+ props: { compact: true },
+ loading: true,
+ });
});
it('The stale tooltip is removed', () => {
diff --git a/spec/frontend/ci/runner/components/runner_delete_disclosure_dropdown_item_spec.js b/spec/frontend/ci/runner/components/runner_delete_disclosure_dropdown_item_spec.js
new file mode 100644
index 00000000000..e311cb4d458
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_delete_disclosure_dropdown_item_spec.js
@@ -0,0 +1,68 @@
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { I18N_DELETE } from '~/ci/runner/constants';
+
+import RunnerDeleteDisclosureDropdownItem from '~/ci/runner/components/runner_delete_disclosure_dropdown_item.vue';
+import RunnerDeleteAction from '~/ci/runner/components/runner_delete_action.vue';
+import { allRunnersData } from '../mock_data';
+
+const mockRunner = allRunnersData.data.runners.nodes[0];
+
+jest.mock('~/alert');
+jest.mock('~/ci/runner/sentry_utils');
+
+describe('RunnerDeleteDisclosureDropdownItem', () => {
+ let wrapper;
+ let mockOnClick;
+
+ const findDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
+
+ const createComponent = () => {
+ mockOnClick = jest.fn();
+
+ wrapper = shallowMountExtended(RunnerDeleteDisclosureDropdownItem, {
+ propsData: {
+ runner: mockRunner,
+ },
+ stubs: {
+ RunnerDeleteAction: stubComponent(RunnerDeleteAction, {
+ render() {
+ return this.$scopedSlots.default({
+ onClick: mockOnClick,
+ });
+ },
+ }),
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('Displays a delete item', () => {
+ expect(findDisclosureDropdownItem().text()).toBe(I18N_DELETE);
+ });
+
+ it('Does not trigger on load', () => {
+ expect(mockOnClick).not.toHaveBeenCalled();
+ });
+
+ it('Triggers delete when clicked', () => {
+ findDisclosureDropdownItem().vm.$emit('action');
+ expect(mockOnClick).toHaveBeenCalledTimes(1);
+ });
+
+ describe('When done after deleting', () => {
+ const doneEvent = { message: 'done!' };
+
+ beforeEach(() => {
+ wrapper.findComponent(RunnerDeleteAction).vm.$emit('done', doneEvent);
+ });
+
+ it('emits deleted event', () => {
+ expect(wrapper.emitted('deleted')).toEqual([[doneEvent]]);
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_delete_modal_spec.js b/spec/frontend/ci/runner/components/runner_delete_modal_spec.js
index 606cc46c018..e486d708fec 100644
--- a/spec/frontend/ci/runner/components/runner_delete_modal_spec.js
+++ b/spec/frontend/ci/runner/components/runner_delete_modal_spec.js
@@ -1,5 +1,6 @@
import { GlModal } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
import RunnerDeleteModal from '~/ci/runner/components/runner_delete_modal.vue';
describe('RunnerDeleteModal', () => {
@@ -7,7 +8,7 @@ describe('RunnerDeleteModal', () => {
const findGlModal = () => wrapper.findComponent(GlModal);
- const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
+ const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMount) => {
wrapper = mountFn(RunnerDeleteModal, {
attachTo: document.body,
propsData: {
@@ -17,6 +18,7 @@ describe('RunnerDeleteModal', () => {
attrs: {
modalId: 'delete-runner-modal-99',
},
+ ...options,
});
};
@@ -66,15 +68,35 @@ describe('RunnerDeleteModal', () => {
});
});
- describe('When modal is confirmed by the user', () => {
+ describe('Modal API', () => {
let hideModalSpy;
+ let showModalSpy;
beforeEach(() => {
- createComponent({}, mount);
- hideModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide').mockImplementation(() => {});
+ hideModalSpy = jest.fn();
+ showModalSpy = jest.fn();
+
+ createComponent({
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ hide: hideModalSpy,
+ show: showModalSpy,
+ },
+ }),
+ },
+ });
+ });
+
+ it('When "show" method is called, modal is shown', () => {
+ expect(showModalSpy).toHaveBeenCalledTimes(0);
+
+ wrapper.vm.show();
+
+ expect(showModalSpy).toHaveBeenCalledTimes(1);
});
- it('Modal gets hidden', () => {
+ it('When confirmed, modal gets hidden', () => {
expect(hideModalSpy).toHaveBeenCalledTimes(0);
findGlModal().vm.$emit('primary');
diff --git a/spec/frontend/ci/runner/components/runner_detail_spec.js b/spec/frontend/ci/runner/components/runner_detail_spec.js
new file mode 100644
index 00000000000..b2d91af4e3b
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_detail_spec.js
@@ -0,0 +1,88 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerDetail from '~/ci/runner/components/runner_detail.vue';
+
+describe('RunnerDetail', () => {
+ let wrapper;
+ const createWrapper = ({ props, slots }) => {
+ wrapper = shallowMountExtended(RunnerDetail, {
+ propsData: props,
+ slots,
+ });
+ };
+ const findLabelText = () => wrapper.findByTestId('label-slot').text();
+ const findValueText = () => wrapper.findByTestId('value-slot').text();
+
+ it('renders the label slot when a label prop is provided', () => {
+ createWrapper({ props: { label: 'Field Name' } });
+
+ expect(findLabelText()).toBe('Field Name');
+ });
+
+ it('does not render the label slot when no label prop is provided', () => {
+ createWrapper({ props: {} });
+
+ expect(findLabelText()).toBe('');
+ });
+
+ it('renders the value slot when a value prop is provided', () => {
+ createWrapper({ props: { value: 'testValue' } });
+
+ expect(findValueText()).toBe('testValue');
+ });
+
+ it('renders the emptyValue when no value prop is provided', () => {
+ createWrapper({ props: {} });
+
+ expect(findValueText()).toBe('None');
+ });
+
+ it('renders both the label and value slots when both label and value props are provided', () => {
+ createWrapper({ props: { label: 'Field Name', value: 'testValue' } });
+
+ expect(findLabelText()).toBe('Field Name');
+ expect(findValueText()).toBe('testValue');
+ });
+
+ it('renders the label slot when a label slot is provided', () => {
+ createWrapper({
+ slots: {
+ label: 'Label Slot Test',
+ },
+ });
+
+ expect(findLabelText()).toBe('Label Slot Test');
+ });
+
+ it('does not render the label slot when no label slot is provided', () => {
+ createWrapper({
+ slots: {},
+ });
+
+ expect(findLabelText()).toBe('');
+ });
+
+ it('renders the value slot when a value slot is provided', () => {
+ createWrapper({
+ slots: {
+ value: 'Value Slot Test',
+ },
+ });
+
+ expect(findValueText()).toBe('Value Slot Test');
+ });
+
+ it('renders the emptyValue when no value slot is provided', () => {
+ createWrapper({
+ slots: {},
+ });
+
+ expect(findValueText()).toBe('None');
+ });
+
+ it('renders both the label and value slots when both label and value slots are provided', () => {
+ createWrapper({ slots: { label: 'Label Slot Test', value: 'Value Slot Test' } });
+
+ expect(findLabelText()).toBe('Label Slot Test');
+ expect(findValueText()).toBe('Value Slot Test');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_edit_button_spec.js b/spec/frontend/ci/runner/components/runner_edit_button_spec.js
index 5cc1ee049f4..5e36ff77146 100644
--- a/spec/frontend/ci/runner/components/runner_edit_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_edit_button_spec.js
@@ -1,18 +1,25 @@
-import { shallowMount, mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { I18N_EDIT } from '~/ci/runner/constants';
describe('RunnerEditButton', () => {
let wrapper;
+ const findButton = () => wrapper.findComponent(GlButton);
const getTooltipValue = () => getBinding(wrapper.element, 'gl-tooltip').value;
- const createComponent = ({ attrs = {}, mountFn = shallowMount } = {}) => {
+ const createComponent = ({ props = {}, mountFn = shallowMount, ...options } = {}) => {
wrapper = mountFn(RunnerEditButton, {
- attrs,
+ propsData: {
+ href: '/edit',
+ ...props,
+ },
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
+ ...options,
});
};
@@ -21,17 +28,24 @@ describe('RunnerEditButton', () => {
});
it('Displays Edit text', () => {
- expect(wrapper.attributes('aria-label')).toBe('Edit');
+ expect(wrapper.attributes('aria-label')).toBe(I18N_EDIT);
});
it('Displays Edit tooltip', () => {
- expect(getTooltipValue()).toBe('Edit');
+ expect(getTooltipValue()).toBe(I18N_EDIT);
});
it('Renders a link and adds an href attribute', () => {
- createComponent({ attrs: { href: '/edit' }, mountFn: mount });
+ expect(findButton().attributes('href')).toBe('/edit');
+ });
- expect(wrapper.element.tagName).toBe('A');
- expect(wrapper.attributes('href')).toBe('/edit');
+ describe('When no href is provided', () => {
+ beforeEach(() => {
+ createComponent({ props: { href: null } });
+ });
+
+ it('does not render', () => {
+ expect(wrapper.html()).toBe('');
+ });
});
});
diff --git a/spec/frontend/ci/runner/components/runner_edit_disclosure_dropdown_item_spec.js b/spec/frontend/ci/runner/components/runner_edit_disclosure_dropdown_item_spec.js
new file mode 100644
index 00000000000..4c6b4d2d52a
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_edit_disclosure_dropdown_item_spec.js
@@ -0,0 +1,42 @@
+import { shallowMount, mount } from '@vue/test-utils';
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+import RunnerEditDisclosureDropdownItem from '~/ci/runner/components/runner_edit_disclosure_dropdown_item.vue';
+import { I18N_EDIT } from '~/ci/runner/constants';
+
+describe('RunnerEditDisclosureDropdownItem', () => {
+ let wrapper;
+
+ const findItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
+
+ const createComponent = ({ props = {}, mountFn = shallowMount, ...options } = {}) => {
+ wrapper = mountFn(RunnerEditDisclosureDropdownItem, {
+ propsData: {
+ href: '/edit',
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ it('Displays Edit text', () => {
+ createComponent({ mountFn: mount });
+
+ expect(wrapper.text()).toBe(I18N_EDIT);
+ });
+
+ it('Renders a link and adds an href attribute', () => {
+ createComponent();
+
+ expect(findItem().props('item').href).toBe('/edit');
+ });
+
+ describe('When no href is provided', () => {
+ beforeEach(() => {
+ createComponent({ props: { href: null } });
+ });
+
+ it('does not render', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_header_actions_spec.js b/spec/frontend/ci/runner/components/runner_header_actions_spec.js
new file mode 100644
index 00000000000..243ada73435
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_header_actions_spec.js
@@ -0,0 +1,147 @@
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { s__ } from '~/locale';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import RunnerHeaderActions from '~/ci/runner/components/runner_header_actions.vue';
+
+import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
+import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
+import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+
+import RunnerEditDisclosureDropdownItem from '~/ci/runner/components/runner_edit_disclosure_dropdown_item.vue';
+import RunnerPauseDisclosureDropdownItem from '~/ci/runner/components/runner_pause_disclosure_dropdown_item.vue';
+import RunnerDeleteDisclosureDropdownItem from '~/ci/runner/components/runner_delete_disclosure_dropdown_item.vue';
+
+import { runnerData } from '../mock_data';
+
+const mockRunner = runnerData.data.runner;
+const mockRunnerEditPath = '/edit';
+
+describe('RunnerHeaderActions', () => {
+ let wrapper;
+
+ const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
+ const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+ const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
+
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findEditItem = () => findDropdown().findComponent(RunnerEditDisclosureDropdownItem);
+ const findPauseItem = () => findDropdown().findComponent(RunnerPauseDisclosureDropdownItem);
+ const findDeleteItem = () => findDropdown().findComponent(RunnerDeleteDisclosureDropdownItem);
+
+ const createComponent = ({ props = {}, options = {}, mountFn = shallowMountExtended } = {}) => {
+ const { runner, ...propsData } = props;
+
+ wrapper = mountFn(RunnerHeaderActions, {
+ propsData: {
+ runner: {
+ ...mockRunner,
+ ...runner,
+ },
+ editPath: mockRunnerEditPath,
+ ...propsData,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders all elements', () => {
+ // visible on md and up screens
+ expect(findRunnerEditButton().exists()).toBe(true);
+ expect(findRunnerPauseButton().exists()).toBe(true);
+ expect(findRunnerDeleteButton().exists()).toBe(true);
+
+ // visible on small screens
+ expect(findDropdown().exists()).toBe(true);
+ expect(findEditItem().exists()).toBe(true);
+ expect(findPauseItem().exists()).toBe(true);
+ expect(findDeleteItem().exists()).toBe(true);
+ });
+
+ it('renders disclosure dropdown with no caret and accesible text', () => {
+ expect(findDropdown().props()).toMatchObject({
+ icon: 'ellipsis_v',
+ toggleText: s__('Runner|Runner actions'),
+ textSrOnly: true,
+ category: 'tertiary',
+ noCaret: true,
+ });
+ });
+
+ it.each([findRunnerEditButton, findEditItem])('edit path is set (%p)', (find) => {
+ expect(find().props('href')).toEqual(mockRunnerEditPath);
+ });
+
+ it.each([findRunnerDeleteButton, findDeleteItem])('delete is emitted (%p)', (find) => {
+ const deleteEvent = { message: 'Deleted!' };
+
+ find().vm.$emit('deleted', deleteEvent);
+
+ expect(wrapper.emitted('deleted')).toEqual([[deleteEvent]]);
+ });
+
+ describe('when delete is disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: {
+ userPermissions: {
+ updateRunner: true,
+ deleteRunner: false,
+ },
+ },
+ },
+ });
+ });
+
+ it('does not render delete actions', () => {
+ expect(findRunnerDeleteButton().exists()).toBe(false);
+ expect(findDeleteItem().exists()).toBe(false);
+ });
+ });
+
+ describe('when update is disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: {
+ userPermissions: {
+ updateRunner: false,
+ deleteRunner: true,
+ },
+ },
+ },
+ });
+ });
+
+ it('does not render delete actions', () => {
+ expect(findRunnerEditButton().exists()).toBe(false);
+ expect(findRunnerPauseButton().exists()).toBe(false);
+ expect(findEditItem().exists()).toBe(false);
+ expect(findPauseItem().exists()).toBe(false);
+ });
+ });
+
+ describe('when no actions are enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: {
+ userPermissions: {
+ updateRunner: false,
+ deleteRunner: false,
+ },
+ },
+ },
+ });
+ });
+
+ it('does not render actions', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
index 22797433b58..511ed88f5ab 100644
--- a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
@@ -10,7 +10,6 @@ import {
I18N_CREATE_RUNNER_LINK,
I18N_STILL_USING_REGISTRATION_TOKENS,
I18N_CONTACT_ADMIN_TO_REGISTER,
- I18N_FOLLOW_REGISTRATION_INSTRUCTIONS,
I18N_NO_RESULTS,
I18N_EDIT_YOUR_SEARCH,
} from '~/ci/runner/constants';
@@ -59,136 +58,84 @@ describe('RunnerListEmptyState', () => {
});
describe('when search is not filtered', () => {
- describe.each([
- { createRunnerWorkflowForAdmin: true },
- { createRunnerWorkflowForNamespace: true },
- ])('when createRunnerWorkflow is enabled by %o', (currentGlFeatures) => {
- beforeEach(() => {
- glFeatures = currentGlFeatures;
- });
-
- describe.each`
- newRunnerPath | registrationToken | expectedMessages
- ${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]}
- ${mockNewRunnerPath} | ${null} | ${[I18N_CREATE_RUNNER_LINK]}
- ${null} | ${mockRegistrationToken} | ${[I18N_STILL_USING_REGISTRATION_TOKENS]}
- ${null} | ${null} | ${[I18N_CONTACT_ADMIN_TO_REGISTER]}
- `(
- 'when newRunnerPath is $newRunnerPath and registrationToken is $registrationToken',
- ({ newRunnerPath, registrationToken, expectedMessages }) => {
- beforeEach(() => {
- createComponent({
- props: {
- newRunnerPath,
- registrationToken,
- },
- });
- });
-
- it('shows title', () => {
- expectTitleToBe(I18N_GET_STARTED);
- });
-
- it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
- });
-
- it(`shows description: "${expectedMessages.join(' ')}"`, () => {
- expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, ...expectedMessages]);
- });
- },
- );
-
- describe('with newRunnerPath and registration token', () => {
+ describe.each`
+ newRunnerPath | registrationToken | expectedMessages
+ ${mockNewRunnerPath} | ${mockRegistrationToken} | ${[I18N_CREATE_RUNNER_LINK, I18N_STILL_USING_REGISTRATION_TOKENS]}
+ ${mockNewRunnerPath} | ${null} | ${[I18N_CREATE_RUNNER_LINK]}
+ ${null} | ${mockRegistrationToken} | ${[I18N_STILL_USING_REGISTRATION_TOKENS]}
+ ${null} | ${null} | ${[I18N_CONTACT_ADMIN_TO_REGISTER]}
+ `(
+ 'when newRunnerPath is $newRunnerPath and registrationToken is $registrationToken',
+ ({ newRunnerPath, registrationToken, expectedMessages }) => {
beforeEach(() => {
createComponent({
props: {
- registrationToken: mockRegistrationToken,
- newRunnerPath: mockNewRunnerPath,
+ newRunnerPath,
+ registrationToken,
},
});
});
- it('shows links to the new runner page and registration instructions', () => {
- expect(findLinks().at(0).attributes('href')).toBe(mockNewRunnerPath);
+ it('shows title', () => {
+ expectTitleToBe(I18N_GET_STARTED);
+ });
- const { value } = getBinding(findLinks().at(1).element, 'gl-modal');
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ it('renders an illustration', () => {
+ expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
});
- });
- describe('with newRunnerPath and no registration token', () => {
- beforeEach(() => {
- createComponent({
- props: {
- registrationToken: mockRegistrationToken,
- newRunnerPath: null,
- },
- });
+ it(`shows description: "${expectedMessages.join(' ')}"`, () => {
+ expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, ...expectedMessages]);
});
+ },
+ );
- it('opens a runner registration instructions modal with a link', () => {
- const { value } = getBinding(findLink().element, 'gl-modal');
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ describe('with newRunnerPath and registration token', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ registrationToken: mockRegistrationToken,
+ newRunnerPath: mockNewRunnerPath,
+ },
});
});
- describe('with no newRunnerPath nor registration token', () => {
- beforeEach(() => {
- createComponent({
- props: {
- registrationToken: null,
- newRunnerPath: null,
- },
- });
- });
+ it('shows links to the new runner page and registration instructions', () => {
+ expect(findLinks().at(0).attributes('href')).toBe(mockNewRunnerPath);
- it('has no link', () => {
- expect(findLink().exists()).toBe(false);
- });
+ const { value } = getBinding(findLinks().at(1).element, 'gl-modal');
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
});
});
- describe('when createRunnerWorkflow is disabled', () => {
- describe('when there is a registration token', () => {
- beforeEach(() => {
- createComponent({
- props: {
- registrationToken: mockRegistrationToken,
- },
- });
- });
-
- it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(EMPTY_STATE_SVG_URL);
- });
-
- it('opens a runner registration instructions modal with a link', () => {
- const { value } = getBinding(findLink().element, 'gl-modal');
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
- });
-
- it('displays text with registration instructions', () => {
- expectTitleToBe(I18N_GET_STARTED);
-
- expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_FOLLOW_REGISTRATION_INSTRUCTIONS]);
+ describe('with newRunnerPath and no registration token', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ registrationToken: mockRegistrationToken,
+ newRunnerPath: null,
+ },
});
});
- describe('when there is no registration token', () => {
- beforeEach(() => {
- createComponent({ props: { registrationToken: null } });
- });
-
- it('displays "contact admin" text', () => {
- expectTitleToBe(I18N_GET_STARTED);
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
+ });
- expectDescriptionToBe([I18N_RUNNERS_ARE_AGENTS, I18N_CONTACT_ADMIN_TO_REGISTER]);
+ describe('with no newRunnerPath nor registration token', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ registrationToken: null,
+ newRunnerPath: null,
+ },
});
+ });
- it('has no registration instructions link', () => {
- expect(findLink().exists()).toBe(false);
- });
+ it('has no link', () => {
+ expect(findLink().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/ci/runner/components/runner_pause_action_spec.js b/spec/frontend/ci/runner/components/runner_pause_action_spec.js
new file mode 100644
index 00000000000..b987eb1e310
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_pause_action_spec.js
@@ -0,0 +1,180 @@
+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 runnerTogglePausedMutation from '~/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql';
+import waitForPromises from 'helpers/wait_for_promises';
+import { captureException } from '~/ci/runner/sentry_utils';
+import { createAlert } from '~/alert';
+
+import RunnerPauseAction from '~/ci/runner/components/runner_pause_action.vue';
+import { allRunnersData } from '../mock_data';
+
+const mockRunner = allRunnersData.data.runners.nodes[0];
+
+Vue.use(VueApollo);
+
+jest.mock('~/alert');
+jest.mock('~/ci/runner/sentry_utils');
+
+describe('RunnerPauseAction', () => {
+ let wrapper;
+ let runnerTogglePausedHandler;
+
+ const findBtn = () => wrapper.find('button');
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
+ const { runner, ...propsData } = props;
+
+ wrapper = mountFn(RunnerPauseAction, {
+ propsData: {
+ runner: {
+ id: mockRunner.id,
+ paused: mockRunner.paused,
+ ...runner,
+ },
+ ...propsData,
+ },
+ apolloProvider: createMockApollo([[runnerTogglePausedMutation, runnerTogglePausedHandler]]),
+ scopedSlots: {
+ default: '<button :disabled="props.loading" @click="props.onClick"/>',
+ },
+ });
+ };
+
+ const clickAndWait = async () => {
+ findBtn().trigger('click');
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ runnerTogglePausedHandler = jest.fn().mockImplementation(({ input }) => {
+ return Promise.resolve({
+ data: {
+ runnerUpdate: {
+ runner: {
+ id: input.id,
+ paused: !input.paused,
+ },
+ errors: [],
+ },
+ },
+ });
+ });
+
+ createComponent();
+ });
+
+ describe('Pause/Resume action', () => {
+ describe.each`
+ runnerState | isPaused | newPausedValue
+ ${'paused'} | ${true} | ${false}
+ ${'active'} | ${false} | ${true}
+ `('When the runner is $runnerState', ({ isPaused, newPausedValue }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: {
+ paused: isPaused,
+ },
+ },
+ });
+ });
+
+ it('Displays slot contents', () => {
+ expect(findBtn().exists()).toBe(true);
+ });
+
+ it('The mutation has not been called', () => {
+ expect(runnerTogglePausedHandler).not.toHaveBeenCalled();
+ });
+
+ describe('Immediately after the action is triggered', () => {
+ it('The button has a loading state', async () => {
+ await findBtn().trigger('click');
+
+ expect(findBtn().attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('After the action is triggered', () => {
+ beforeEach(async () => {
+ await clickAndWait();
+ });
+
+ it(`The mutation to that sets "paused" to ${newPausedValue} is called`, () => {
+ expect(runnerTogglePausedHandler).toHaveBeenCalledTimes(1);
+ expect(runnerTogglePausedHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockRunner.id,
+ paused: newPausedValue,
+ },
+ });
+ });
+
+ it('The button does not have a loading state', () => {
+ expect(findBtn().attributes('disabled')).toBeUndefined();
+ });
+
+ it('The button emits "done"', () => {
+ expect(wrapper.emitted('done')).toHaveLength(1);
+ });
+ });
+
+ describe('When update fails', () => {
+ describe('On a network error', () => {
+ const mockErrorMsg = 'Update error!';
+
+ beforeEach(async () => {
+ runnerTogglePausedHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
+
+ await clickAndWait();
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(mockErrorMsg),
+ component: 'RunnerPauseAction',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('On a validation error', () => {
+ const mockErrorMsg = 'Runner not found!';
+ const mockErrorMsg2 = 'User not allowed!';
+
+ beforeEach(async () => {
+ runnerTogglePausedHandler.mockResolvedValueOnce({
+ data: {
+ runnerUpdate: {
+ runner: {
+ id: mockRunner.id,
+ paused: isPaused,
+ },
+ errors: [mockErrorMsg, mockErrorMsg2],
+ },
+ },
+ });
+
+ await clickAndWait();
+ });
+
+ it('error is reported to sentry', () => {
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
+ component: 'RunnerPauseAction',
+ });
+ });
+
+ it('error is shown to the user', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_pause_button_spec.js b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
index 1ea870e004a..f1ceecd4ae4 100644
--- a/spec/frontend/ci/runner/components/runner_pause_button_spec.js
+++ b/spec/frontend/ci/runner/components/runner_pause_button_spec.js
@@ -1,13 +1,7 @@
-import Vue, { nextTick } from 'vue';
import { GlButton } from '@gitlab/ui';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
-import runnerTogglePausedMutation from '~/ci/runner/graphql/shared/runner_toggle_paused.mutation.graphql';
-import waitForPromises from 'helpers/wait_for_promises';
-import { captureException } from '~/ci/runner/sentry_utils';
-import { createAlert } from '~/alert';
import {
I18N_PAUSE,
I18N_PAUSE_TOOLTIP,
@@ -16,244 +10,140 @@ import {
} from '~/ci/runner/constants';
import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
-import { allRunnersData } from '../mock_data';
-
-const mockRunner = allRunnersData.data.runners.nodes[0];
-
-Vue.use(VueApollo);
-
-jest.mock('~/alert');
-jest.mock('~/ci/runner/sentry_utils');
+import RunnerPauseAction from '~/ci/runner/components/runner_pause_action.vue';
describe('RunnerPauseButton', () => {
let wrapper;
- let runnerTogglePausedHandler;
const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
+ const findRunnerPauseAction = () => wrapper.findComponent(RunnerPauseAction);
const findBtn = () => wrapper.findComponent(GlButton);
- const createComponent = ({ props = {}, mountFn = shallowMountExtended } = {}) => {
- const { runner, ...propsData } = props;
-
+ const createComponent = ({
+ props = {},
+ loading,
+ onClick = jest.fn(),
+ mountFn = shallowMountExtended,
+ } = {}) => {
wrapper = mountFn(RunnerPauseButton, {
propsData: {
- runner: {
- id: mockRunner.id,
- paused: mockRunner.paused,
- ...runner,
- },
- ...propsData,
+ runner: {},
+ ...props,
},
- apolloProvider: createMockApollo([[runnerTogglePausedMutation, runnerTogglePausedHandler]]),
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
+ stubs: {
+ RunnerPauseAction: stubComponent(RunnerPauseAction, {
+ render() {
+ return this.$scopedSlots.default({
+ loading,
+ onClick,
+ });
+ },
+ }),
+ },
});
};
- const clickAndWait = async () => {
- findBtn().vm.$emit('click');
- await waitForPromises();
- };
-
beforeEach(() => {
- runnerTogglePausedHandler = jest.fn().mockImplementation(({ input }) => {
- return Promise.resolve({
- data: {
- runnerUpdate: {
- runner: {
- id: input.id,
- paused: !input.paused,
- },
- errors: [],
- },
- },
- });
- });
-
createComponent();
});
- describe('Pause/Resume action', () => {
+ describe('Pause/Resume button', () => {
describe.each`
- runnerState | icon | content | tooltip | isPaused | newPausedValue
- ${'paused'} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP} | ${true} | ${false}
- ${'active'} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP} | ${false} | ${true}
- `('When the runner is $runnerState', ({ icon, content, tooltip, isPaused, newPausedValue }) => {
- beforeEach(() => {
- createComponent({
- props: {
- runner: {
- paused: isPaused,
+ runnerState | paused | expectedIcon | expectedContent | expectedTooltip
+ ${'paused'} | ${true} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP}
+ ${'active'} | ${false} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP}
+ `(
+ 'When the runner is $runnerState',
+ ({ paused, expectedIcon, expectedContent, expectedTooltip }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: { paused },
},
- },
- });
- });
-
- it(`Displays a ${icon} button`, () => {
- expect(findBtn().props('loading')).toBe(false);
- expect(findBtn().props('icon')).toBe(icon);
- });
-
- it('Displays button content', () => {
- expect(findBtn().text()).toBe(content);
- expect(getTooltip()).toBe(tooltip);
- });
-
- it('Does not display redundant text for screen readers', () => {
- expect(findBtn().attributes('aria-label')).toBe(undefined);
- });
-
- describe(`Before the ${icon} button is clicked`, () => {
- it('The mutation has not been called', () => {
- expect(runnerTogglePausedHandler).not.toHaveBeenCalled();
+ });
});
- });
-
- describe(`Immediately after the ${icon} button is clicked`, () => {
- const setup = async () => {
- findBtn().vm.$emit('click');
- await nextTick();
- };
- it('The button has a loading state', async () => {
- await setup();
-
- expect(findBtn().props('loading')).toBe(true);
+ it(`Displays a ${expectedIcon} button`, () => {
+ expect(findBtn().props('loading')).toBe(false);
+ expect(findBtn().props('icon')).toBe(expectedIcon);
});
- it('The stale tooltip is removed', async () => {
- await setup();
-
- expect(getTooltip()).toBe('');
+ it('Displays button content', () => {
+ expect(findBtn().text()).toBe(expectedContent);
+ expect(getTooltip()).toBe(expectedTooltip);
});
- });
- describe(`After clicking on the ${icon} button`, () => {
- beforeEach(async () => {
- await clickAndWait();
+ it('Does not display redundant text for screen readers', () => {
+ expect(findBtn().attributes('aria-label')).toBe(undefined);
});
+ },
+ );
+ });
- it(`The mutation to that sets "paused" to ${newPausedValue} is called`, () => {
- expect(runnerTogglePausedHandler).toHaveBeenCalledTimes(1);
- expect(runnerTogglePausedHandler).toHaveBeenCalledWith({
- input: {
- id: mockRunner.id,
- paused: newPausedValue,
+ describe('Compact button', () => {
+ describe.each`
+ runnerState | paused | expectedIcon | expectedContent | expectedTooltip
+ ${'paused'} | ${true} | ${'play'} | ${I18N_RESUME} | ${I18N_RESUME_TOOLTIP}
+ ${'active'} | ${false} | ${'pause'} | ${I18N_PAUSE} | ${I18N_PAUSE_TOOLTIP}
+ `(
+ 'When the runner is $runnerState',
+ ({ paused, expectedIcon, expectedContent, expectedTooltip }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ runner: { paused },
+ compact: true,
},
+ mountFn: mountExtended,
});
});
- it('The button does not have a loading state', () => {
+ it(`Displays a ${expectedIcon} button`, () => {
expect(findBtn().props('loading')).toBe(false);
+ expect(findBtn().props('icon')).toBe(expectedIcon);
});
- it('The button emits toggledPaused', () => {
- expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
- });
- });
-
- describe('When update fails', () => {
- describe('On a network error', () => {
- const mockErrorMsg = 'Update error!';
-
- beforeEach(async () => {
- runnerTogglePausedHandler.mockRejectedValueOnce(new Error(mockErrorMsg));
-
- await clickAndWait();
- });
-
- it('error is reported to sentry', () => {
- expect(captureException).toHaveBeenCalledWith({
- error: new Error(mockErrorMsg),
- component: 'RunnerPauseButton',
- });
- });
+ it('Displays button content', () => {
+ expect(findBtn().text()).toBe('');
+ // Note: Use <template v-if> to ensure rendering a
+ // text-less button. Ensure we don't send even empty an
+ // content slot to prevent a distorted/rectangular button.
+ expect(wrapper.find('.gl-button-text').exists()).toBe(false);
- it('error is shown to the user', () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
+ expect(getTooltip()).toBe(expectedTooltip);
});
- describe('On a validation error', () => {
- const mockErrorMsg = 'Runner not found!';
- const mockErrorMsg2 = 'User not allowed!';
-
- beforeEach(async () => {
- runnerTogglePausedHandler.mockResolvedValueOnce({
- data: {
- runnerUpdate: {
- runner: {
- id: mockRunner.id,
- paused: isPaused,
- },
- errors: [mockErrorMsg, mockErrorMsg2],
- },
- },
- });
-
- await clickAndWait();
- });
-
- it('error is reported to sentry', () => {
- expect(captureException).toHaveBeenCalledWith({
- error: new Error(`${mockErrorMsg} ${mockErrorMsg2}`),
- component: 'RunnerPauseButton',
- });
- });
-
- it('error is shown to the user', () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
+ it('Does not display redundant text for screen readers', () => {
+ expect(findBtn().attributes('aria-label')).toBe(expectedContent);
});
- });
- });
+ },
+ );
});
- describe('When displaying a compact button for an active runner', () => {
- beforeEach(() => {
- createComponent({
- props: {
- runner: {
- paused: false,
- },
- compact: true,
- },
- mountFn: mountExtended,
- });
- });
-
- it('Displays no text', () => {
- expect(findBtn().text()).toBe('');
+ it('Shows loading state', () => {
+ createComponent({ loading: true });
- // Note: Use <template v-if> to ensure rendering a
- // text-less button. Ensure we don't send even empty an
- // content slot to prevent a distorted/rectangular button.
- expect(wrapper.find('.gl-button-text').exists()).toBe(false);
- });
+ expect(findBtn().props('loading')).toBe(true);
+ expect(getTooltip()).toBe('');
+ });
- it('Display correctly for screen readers', () => {
- expect(findBtn().attributes('aria-label')).toBe(I18N_PAUSE);
- expect(getTooltip()).toBe(I18N_PAUSE_TOOLTIP);
- });
+ it('Triggers action', () => {
+ const mockOnClick = jest.fn();
- describe('Immediately after the button is clicked', () => {
- const setup = async () => {
- findBtn().vm.$emit('click');
- await nextTick();
- };
+ createComponent({ onClick: mockOnClick });
+ findBtn().vm.$emit('click');
- it('The button has a loading state', async () => {
- await setup();
+ expect(mockOnClick).toHaveBeenCalled();
+ });
- expect(findBtn().props('loading')).toBe(true);
- });
+ it('Emits toggledPaused when done', () => {
+ createComponent();
- it('The stale tooltip is removed', async () => {
- await setup();
+ findRunnerPauseAction().vm.$emit('done');
- expect(getTooltip()).toBe('');
- });
- });
+ expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
});
});
diff --git a/spec/frontend/ci/runner/components/runner_pause_disclosure_dropdown_item_spec.js b/spec/frontend/ci/runner/components/runner_pause_disclosure_dropdown_item_spec.js
new file mode 100644
index 00000000000..5dc9a615b0e
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_pause_disclosure_dropdown_item_spec.js
@@ -0,0 +1,71 @@
+import { GlDisclosureDropdownItem } from '@gitlab/ui';
+import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import { I18N_PAUSE, I18N_RESUME } from '~/ci/runner/constants';
+
+import RunnerPauseDisclosureDropdownItem from '~/ci/runner/components/runner_pause_disclosure_dropdown_item.vue';
+import RunnerPauseAction from '~/ci/runner/components/runner_pause_action.vue';
+
+describe('RunnerPauseButton', () => {
+ let wrapper;
+
+ const findRunnerPauseAction = () => wrapper.findComponent(RunnerPauseAction);
+ const findDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
+
+ const createComponent = ({
+ props = {},
+ onClick = jest.fn(),
+ mountFn = shallowMountExtended,
+ } = {}) => {
+ wrapper = mountFn(RunnerPauseDisclosureDropdownItem, {
+ propsData: {
+ runner: {},
+ ...props,
+ },
+ stubs: {
+ RunnerPauseAction: stubComponent(RunnerPauseAction, {
+ render() {
+ return this.$scopedSlots.default({
+ onClick,
+ });
+ },
+ }),
+ },
+ });
+ };
+
+ it('Displays paused runner button content', () => {
+ createComponent({
+ props: { runner: { paused: true } },
+ mountFn: mountExtended,
+ });
+
+ expect(findDisclosureDropdownItem().text()).toBe(I18N_RESUME);
+ });
+
+ it('Displays active runner button content', () => {
+ createComponent({
+ props: { runner: { paused: false } },
+ mountFn: mountExtended,
+ });
+
+ expect(findDisclosureDropdownItem().text()).toBe(I18N_PAUSE);
+ });
+
+ it('Triggers action', () => {
+ const mockOnClick = jest.fn();
+
+ createComponent({ onClick: mockOnClick });
+ findDisclosureDropdownItem().vm.$emit('action');
+
+ expect(mockOnClick).toHaveBeenCalled();
+ });
+
+ it('Emits toggledPaused when done', () => {
+ createComponent();
+
+ findRunnerPauseAction().vm.$emit('done');
+
+ expect(wrapper.emitted('toggledPaused')).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
index 120388900b5..7438c47e32c 100644
--- a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
@@ -9,10 +9,8 @@ import { visitUrl } from '~/lib/utils/url_utility';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import RunnerHeader from '~/ci/runner/components/runner_header.vue';
+import RunnerHeaderActions from '~/ci/runner/components/runner_header_actions.vue';
import RunnerDetails from '~/ci/runner/components/runner_details.vue';
-import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
-import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
-import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue';
import RunnersJobs from '~/ci/runner/components/runner_jobs.vue';
@@ -47,9 +45,7 @@ describe('GroupRunnerShowApp', () => {
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
- const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
- const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
- const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+ const findRunnerHeaderActions = () => wrapper.findComponent(RunnerHeaderActions);
const findRunnerDetailsTabs = () => wrapper.findComponent(RunnerDetailsTabs);
const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
@@ -95,10 +91,11 @@ describe('GroupRunnerShowApp', () => {
expect(findRunnerHeader().text()).toContain(`#${mockRunnerId} (${mockRunnerSha})`);
});
- it('displays the runner edit and pause buttons', () => {
- expect(findRunnerEditButton().attributes('href')).toBe(mockEditGroupRunnerPath);
- expect(findRunnerPauseButton().exists()).toBe(true);
- expect(findRunnerDeleteButton().exists()).toBe(true);
+ it('displays the runner buttons', () => {
+ expect(findRunnerHeaderActions().props()).toEqual({
+ runner: mockRunner,
+ editPath: mockEditGroupRunnerPath,
+ });
});
it('shows runner details', () => {
@@ -127,54 +124,6 @@ describe('GroupRunnerShowApp', () => {
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
});
- describe('when runner cannot be updated', () => {
- beforeEach(async () => {
- mockRunnerQueryResult({
- userPermissions: {
- ...mockRunner.userPermissions,
- updateRunner: false,
- },
- });
-
- await createComponent({
- mountFn: mountExtended,
- });
- });
-
- it('does not display the runner edit and pause buttons', () => {
- expect(findRunnerEditButton().exists()).toBe(false);
- expect(findRunnerPauseButton().exists()).toBe(false);
- });
-
- it('displays delete button', () => {
- expect(findRunnerDeleteButton().exists()).toBe(true);
- });
- });
-
- describe('when runner cannot be deleted', () => {
- beforeEach(async () => {
- mockRunnerQueryResult({
- userPermissions: {
- ...mockRunner.userPermissions,
- deleteRunner: false,
- },
- });
-
- await createComponent({
- mountFn: mountExtended,
- });
- });
-
- it('does not display the delete button', () => {
- expect(findRunnerDeleteButton().exists()).toBe(false);
- });
-
- it('displays edit and pause buttons', () => {
- expect(findRunnerEditButton().exists()).toBe(true);
- expect(findRunnerPauseButton().exists()).toBe(true);
- });
- });
-
describe('when runner is deleted', () => {
beforeEach(async () => {
await createComponent({
@@ -183,7 +132,7 @@ describe('GroupRunnerShowApp', () => {
});
it('redirects to the runner list page', () => {
- findRunnerDeleteButton().vm.$emit('deleted', { message: 'Runner deleted' });
+ findRunnerHeaderActions().vm.$emit('deleted', { message: 'Runner deleted' });
expect(saveAlertToLocalStorage).toHaveBeenCalledWith({
message: 'Runner deleted',
diff --git a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
index 74eeb864cd8..f3d7ae85e0d 100644
--- a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
@@ -483,35 +483,15 @@ describe('GroupRunnersApp', () => {
expect(findRegistrationDropdown().exists()).toBe(true);
});
- it('when create_runner_workflow_for_namespace is enabled', () => {
+ it('shows the create runner button', () => {
createComponent({
props: {
newRunnerPath,
},
- provide: {
- glFeatures: {
- createRunnerWorkflowForNamespace: true,
- },
- },
});
expect(findNewRunnerBtn().attributes('href')).toBe(newRunnerPath);
});
-
- it('when create_runner_workflow_for_namespace is disabled', () => {
- createComponent({
- props: {
- newRunnerPath,
- },
- provide: {
- glFeatures: {
- createRunnerWorkflowForNamespace: false,
- },
- },
- });
-
- expect(findNewRunnerBtn().exists()).toBe(false);
- });
});
describe('when user has no permission to register group runner', () => {
@@ -524,16 +504,11 @@ describe('GroupRunnersApp', () => {
expect(findRegistrationDropdown().exists()).toBe(false);
});
- it('when create_runner_workflow_for_namespace is enabled', () => {
+ it('shows the create runner button', () => {
createComponent({
props: {
newRunnerPath: null,
},
- provide: {
- glFeatures: {
- createRunnerWorkflowForNamespace: true,
- },
- },
});
expect(findNewRunnerBtn().exists()).toBe(false);
diff --git a/spec/frontend/clusters/agents/components/create_token_modal_spec.js b/spec/frontend/clusters/agents/components/create_token_modal_spec.js
index f0fded7b7b2..40cb3b8292f 100644
--- a/spec/frontend/clusters/agents/components/create_token_modal_spec.js
+++ b/spec/frontend/clusters/agents/components/create_token_modal_spec.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
import {
@@ -37,6 +38,7 @@ describe('CreateTokenModal', () => {
};
const agentName = 'cluster-agent';
const projectPath = 'path/to/project';
+ const hideModalMock = jest.fn();
const provide = {
agentName,
@@ -91,10 +93,12 @@ describe('CreateTokenModal', () => {
provide,
propsData,
stubs: {
- GlModal,
+ GlModal: stubComponent(GlModal, {
+ methods: { hide: hideModalMock },
+ template: RENDER_ALL_SLOTS_TEMPLATE,
+ }),
},
});
- wrapper.vm.$refs.modal.hide = jest.fn();
trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
};
@@ -138,6 +142,11 @@ describe('CreateTokenModal', () => {
expectDisabledAttribute(findCancelButton(), false);
});
+ it('cancel button should hide the modal', () => {
+ findCancelButton().vm.$emit('click');
+ expect(hideModalMock).toHaveBeenCalled();
+ });
+
it('renders a disabled next button', () => {
expect(findActionButton().text()).toBe('Create token');
expectDisabledAttribute(findActionButton(), true);
diff --git a/spec/frontend/clusters/agents/components/revoke_token_button_spec.js b/spec/frontend/clusters/agents/components/revoke_token_button_spec.js
index 970782a8e58..de47ff78696 100644
--- a/spec/frontend/clusters/agents/components/revoke_token_button_spec.js
+++ b/spec/frontend/clusters/agents/components/revoke_token_button_spec.js
@@ -2,6 +2,7 @@ import { GlButton, GlModal, GlFormInput, GlTooltip } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { ENTER_KEY } from '~/lib/utils/keys';
@@ -81,12 +82,15 @@ describe('RevokeTokenButton', () => {
},
propsData,
stubs: {
- GlModal,
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ hide: jest.fn(),
+ },
+ }),
GlTooltip,
},
mocks: { $toast: { show: toast } },
});
- wrapper.vm.$refs.modal.hide = jest.fn();
writeQuery();
await nextTick();
diff --git a/spec/frontend/clusters_list/components/clusters_actions_spec.js b/spec/frontend/clusters_list/components/clusters_actions_spec.js
index e4e1986f705..6957862dc2b 100644
--- a/spec/frontend/clusters_list/components/clusters_actions_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_actions_spec.js
@@ -1,4 +1,10 @@
-import { GlButton, GlDropdown, GlDropdownItem, GlTooltip } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlTooltip,
+ GlButtonGroup,
+} from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ClustersActions from '~/clusters_list/components/clusters_actions.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -18,12 +24,13 @@ describe('ClustersActionsComponent', () => {
certificateBasedClustersEnabled: true,
};
+ const findButtonGroup = () => wrapper.findComponent(GlButtonGroup);
const findButton = () => wrapper.findComponent(GlButton);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDropdownItems = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findDropdownItemIds = () =>
- findDropdownItems().wrappers.map((x) => x.attributes('data-testid'));
+ findDropdownItems().wrappers.map((x) => x.find('a').attributes('data-testid'));
const findDropdownItemTexts = () => findDropdownItems().wrappers.map((x) => x.text());
const findNewClusterDocsLink = () => wrapper.findByTestId('create-cluster-link');
const findConnectClusterLink = () => wrapper.findByTestId('connect-cluster-link');
@@ -34,6 +41,10 @@ describe('ClustersActionsComponent', () => {
...defaultProvide,
...provideData,
},
+ stubs: {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ },
directives: {
GlModalDirective: createMockDirective('gl-modal-directive'),
},
@@ -45,25 +56,23 @@ describe('ClustersActionsComponent', () => {
});
describe('when the certificate based clusters are enabled', () => {
- it('renders actions menu', () => {
+ it('renders actions menu button group with dropdown', () => {
+ expect(findButtonGroup().exists()).toBe(true);
+ expect(findButton().exists()).toBe(true);
expect(findDropdown().exists()).toBe(true);
});
- it('shows split button in dropdown', () => {
- expect(findDropdown().props('split')).toBe(true);
- });
-
it("doesn't show the tooltip", () => {
expect(findTooltip().exists()).toBe(false);
});
describe('when on project level', () => {
it(`displays default action as ${CLUSTERS_ACTIONS.connectWithAgent}`, () => {
- expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.connectWithAgent);
+ expect(findButton().text()).toBe(CLUSTERS_ACTIONS.connectWithAgent);
});
it('renders correct modal id for the default action', () => {
- const binding = getBinding(findDropdown().element, 'gl-modal-directive');
+ const binding = getBinding(findButton().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
@@ -91,6 +100,7 @@ describe('ClustersActionsComponent', () => {
it('disables dropdown', () => {
expect(findDropdown().props('disabled')).toBe(true);
+ expect(findButton().props('disabled')).toBe(true);
});
it('shows tooltip explaining why dropdown is disabled', () => {
@@ -98,7 +108,7 @@ describe('ClustersActionsComponent', () => {
});
it('does not bind split dropdown button', () => {
- const binding = getBinding(findDropdown().element, 'gl-modal-directive');
+ const binding = getBinding(findButton().element, 'gl-modal-directive');
expect(binding.value).toBe(false);
});
@@ -148,11 +158,11 @@ describe('ClustersActionsComponent', () => {
});
it(`displays default action as ${CLUSTERS_ACTIONS.connectCluster}`, () => {
- expect(findDropdown().props('text')).toBe(CLUSTERS_ACTIONS.connectCluster);
+ expect(findButton().text()).toBe(CLUSTERS_ACTIONS.connectCluster);
});
it('renders correct modal id for the default action', () => {
- const binding = getBinding(findDropdown().element, 'gl-modal-directive');
+ const binding = getBinding(findButton().element, 'gl-modal-directive');
expect(binding.value).toBe(INSTALL_AGENT_MODAL_ID);
});
diff --git a/spec/frontend/clusters_list/components/delete_agent_button_spec.js b/spec/frontend/clusters_list/components/delete_agent_button_spec.js
index 8bbb5ec92a7..afb12d9c856 100644
--- a/spec/frontend/clusters_list/components/delete_agent_button_spec.js
+++ b/spec/frontend/clusters_list/components/delete_agent_button_spec.js
@@ -9,6 +9,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import DeleteAgentButton from '~/clusters_list/components/delete_agent_button.vue';
import { DELETE_AGENT_BUTTON } from '~/clusters_list/constants';
+import { stubComponent } from 'helpers/stub_component';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getAgentResponse, mockDeleteResponse, mockErrorDeleteResponse } from '../mocks/apollo';
@@ -84,9 +85,14 @@ describe('DeleteAgentButton', () => {
},
propsData,
mocks: { $toast: { show: toast } },
- stubs: { GlModal },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ hide: jest.fn(),
+ },
+ }),
+ },
});
- wrapper.vm.$refs.modal.hide = jest.fn();
writeQuery();
await nextTick();
diff --git a/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
index 8cad483e27e..d0bc7a55f8e 100644
--- a/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
+++ b/spec/frontend/comment_templates/components/__snapshots__/list_item_spec.js.snap
@@ -130,7 +130,7 @@ exports[`Comment templates list item component renders list item 1`] = `
</div>
<div
- class="gl-mt-3 gl-font-monospace"
+ class="gl-mt-3 gl-font-monospace gl-white-space-pre-wrap"
>
/assign_reviewer
</div>
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_spec.js
index e474ef9c635..73031724b12 100644
--- a/spec/frontend/commit/commit_pipeline_status_component_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_spec.js
@@ -5,7 +5,7 @@ import { nextTick } from 'vue';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { createAlert } from '~/alert';
import Poll from '~/lib/utils/poll';
-import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
jest.mock('~/lib/utils/poll');
diff --git a/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
index 85eafa9e85c..53c098ee153 100644
--- a/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/bubble_menu_spec.js
@@ -65,7 +65,7 @@ describe('content_editor/components/bubble_menus/bubble_menu', () => {
onHidden: expect.any(Function),
onShow: expect.any(Function),
strategy: 'fixed',
- maxWidth: 'auto',
+ maxWidth: '400px',
...tippyOptions,
}),
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
index c79df9c9ed8..b219c506753 100644
--- a/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/link_bubble_menu_spec.js
@@ -206,7 +206,7 @@ describe('content_editor/components/bubble_menus/link_bubble_menu', () => {
await buildWrapperAndDisplayMenu();
await wrapper.findByTestId('remove-link').vm.$emit('click');
- expect(tiptapEditor.getHTML()).toBe('<p>Download PDF File</p>');
+ expect(tiptapEditor.getHTML()).toBe('<p dir="auto">Download PDF File</p>');
});
});
diff --git a/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
index 89beb76a6f2..002e19ee8cf 100644
--- a/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/media_bubble_menu_spec.js
@@ -22,19 +22,19 @@ import {
PROJECT_WIKI_ATTACHMENT_DRAWIO_DIAGRAM_HTML,
} from '../../test_constants';
-const TIPTAP_AUDIO_HTML = `<p>
+const TIPTAP_AUDIO_HTML = `<p dir="auto">
<span class="media-container audio-container"><audio src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></audio><a href="https://gitlab.com/favicon.png" class="with-attachment-icon">gitlab favicon</a></span>
</p>`;
-const TIPTAP_DIAGRAM_HTML = `<p>
- <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon">
+const TIPTAP_DIAGRAM_HTML = `<p dir="auto">
+ <img src="https://gitlab.com/favicon.png" alt="gitlab favicon">
</p>`;
-const TIPTAP_IMAGE_HTML = `<p>
- <img src="https://gitlab.com/favicon.png" alt="gitlab favicon" title="gitlab favicon">
+const TIPTAP_IMAGE_HTML = `<p dir="auto">
+ <img src="https://gitlab.com/favicon.png" alt="gitlab favicon">
</p>`;
-const TIPTAP_VIDEO_HTML = `<p>
+const TIPTAP_VIDEO_HTML = `<p dir="auto">
<span class="media-container video-container"><video src="https://gitlab.com/favicon.png" controls="true" data-setup="{}" data-title="gitlab favicon"></video><a href="https://gitlab.com/favicon.png" class="with-attachment-icon">gitlab favicon</a></span>
</p>`;
@@ -101,9 +101,7 @@ describe.each`
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(() => {
@@ -128,14 +126,11 @@ describe.each`
await buildWrapperAndDisplayMenu();
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.attributes()).toMatchObject({
+ href: `/group1/project1/-/wikis/${filePath}`,
+ 'aria-label': filePath,
+ target: '_blank',
+ });
expect(link.text()).toBe(filePath);
});
@@ -190,28 +185,6 @@ describe.each`
});
});
- describe('copy button', () => {
- it(`copies the canonical link to the ${mediaType} to clipboard`, async () => {
- await buildWrapperAndDisplayMenu();
-
- jest.spyOn(navigator.clipboard, 'writeText');
-
- await wrapper.findByTestId('copy-media-src').vm.$emit('click');
-
- expect(navigator.clipboard.writeText).toHaveBeenCalledWith(filePath);
- });
- });
-
- describe(`remove ${mediaType} button`, () => {
- it(`removes the ${mediaType}`, async () => {
- await buildWrapperAndDisplayMenu();
-
- await wrapper.findByTestId('delete-media').vm.$emit('click');
-
- expect(tiptapEditor.getHTML()).toBe('<p>\n \n</p>');
- });
- });
-
describe(`replace ${mediaType} button`, () => {
beforeEach(buildWrapperAndDisplayMenu);
@@ -252,7 +225,6 @@ describe.each`
describe('edit button', () => {
let mediaSrcInput;
- let mediaTitleInput;
let mediaAltInput;
beforeEach(async () => {
@@ -261,7 +233,6 @@ describe.each`
await wrapper.findByTestId('edit-media').vm.$emit('click');
mediaSrcInput = wrapper.findByTestId('media-src');
- mediaTitleInput = wrapper.findByTestId('media-title');
mediaAltInput = wrapper.findByTestId('media-alt');
});
@@ -269,11 +240,10 @@ describe.each`
expectLinkButtonsToExist(false);
});
- it(`shows a form to edit the ${mediaType} src/title/alt`, () => {
+ it(`shows a form to edit the ${mediaType} src/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');
});
@@ -281,7 +251,6 @@ describe.each`
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');
@@ -294,14 +263,11 @@ describe.each`
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.attributes()).toMatchObject({
+ href: 'https://gitlab.com/favicon.png',
+ 'aria-label': 'https://gitlab.com/favicon.png',
+ target: '_blank',
+ });
expect(link.text()).toBe('https://gitlab.com/favicon.png');
});
});
@@ -310,7 +276,6 @@ describe.each`
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');
});
@@ -324,12 +289,10 @@ describe.each`
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/reference_bubble_menu_spec.js b/spec/frontend/content_editor/components/bubble_menus/reference_bubble_menu_spec.js
index 169f77dc054..c46aa1b657e 100644
--- a/spec/frontend/content_editor/components/bubble_menus/reference_bubble_menu_spec.js
+++ b/spec/frontend/content_editor/components/bubble_menus/reference_bubble_menu_spec.js
@@ -241,7 +241,7 @@ describe('content_editor/components/bubble_menus/reference_bubble_menu', () => {
await buildWrapperAndDisplayMenu();
await wrapper.findByTestId('remove-reference').trigger('click');
- expect(tiptapEditor.getHTML()).toBe('<p></p>');
+ expect(tiptapEditor.getHTML()).toBe('<p dir="auto"></p>');
});
});
});
diff --git a/spec/frontend/content_editor/components/content_editor_spec.js b/spec/frontend/content_editor/components/content_editor_spec.js
index 0b8321ba8eb..816c9458201 100644
--- a/spec/frontend/content_editor/components/content_editor_spec.js
+++ b/spec/frontend/content_editor/components/content_editor_spec.js
@@ -14,6 +14,7 @@ import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vu
import LoadingIndicator from '~/content_editor/components/loading_indicator.vue';
import waitForPromises from 'helpers/wait_for_promises';
import { KEYDOWN_EVENT } from '~/content_editor/constants';
+import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
jest.mock('~/emoji');
@@ -92,19 +93,6 @@ describe('ContentEditor', () => {
expect(wrapper.findComponent(FormattingToolbar).exists()).toBe(true);
});
- it('renders footer containing quick actions help text if quick actions docs path is defined', () => {
- createWrapper({ quickActionsDocsPath: '/foo/bar' });
-
- expect(wrapper.text()).toContain('For quick actions, type /');
- expect(wrapper.findComponent(GlLink).attributes('href')).toBe('/foo/bar');
- });
-
- it('does not render footer containing quick actions help text if quick actions docs path is not defined', () => {
- createWrapper();
-
- expect(findEditorElement().text()).not.toContain('For quick actions, type /');
- });
-
it('displays an attachment button', () => {
createWrapper();
@@ -286,4 +274,10 @@ describe('ContentEditor', () => {
expect(wrapper.findComponent(component).exists()).toBe(true);
});
+
+ it('renders an editor mode dropdown', () => {
+ createWrapper();
+
+ expect(wrapper.findComponent(EditorModeSwitcher).exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/content_editor/components/formatting_toolbar_spec.js b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
index 9d835381ff4..6562cb517cd 100644
--- a/spec/frontend/content_editor/components/formatting_toolbar_spec.js
+++ b/spec/frontend/content_editor/components/formatting_toolbar_spec.js
@@ -2,24 +2,31 @@ import { GlTabs, GlTab } from '@gitlab/ui';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import FormattingToolbar from '~/content_editor/components/formatting_toolbar.vue';
+import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.vue';
import {
TOOLBAR_CONTROL_TRACKING_ACTION,
CONTENT_EDITOR_TRACKING_LABEL,
} from '~/content_editor/constants';
-import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
+import { createTestEditor, mockChainedCommands } from '../test_utils';
describe('content_editor/components/formatting_toolbar', () => {
let wrapper;
let trackingSpy;
- const buildWrapper = (props) => {
+ const contentEditor = {
+ codeSuggestionsConfig: {
+ canSuggest: true,
+ },
+ };
+
+ const buildWrapper = ({ props = {}, provide = { contentEditor } } = {}) => {
wrapper = shallowMountExtended(FormattingToolbar, {
stubs: {
GlTabs,
GlTab,
- EditorModeSwitcher,
},
propsData: props,
+ provide,
});
};
@@ -28,20 +35,22 @@ describe('content_editor/components/formatting_toolbar', () => {
});
describe.each`
- testId | controlProps
- ${'text-styles'} | ${{}}
- ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold text', editorCommand: 'toggleBold' }}
- ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic text', editorCommand: 'toggleItalic' }}
- ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough', editorCommand: 'toggleStrike' }}
- ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }}
- ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
- ${'link'} | ${{}}
- ${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }}
- ${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }}
- ${'task-list'} | ${{ contentType: 'taskList', iconName: 'list-task', label: 'Add a checklist', editorCommand: 'toggleTaskList' }}
- ${'attachment'} | ${{}}
- ${'table'} | ${{}}
- ${'more'} | ${{}}
+ testId | controlProps
+ ${'text-styles'} | ${{}}
+ ${'bold'} | ${{ contentType: 'bold', iconName: 'bold', label: 'Bold (Ctrl+B)', editorCommand: 'toggleBold' }}
+ ${'italic'} | ${{ contentType: 'italic', iconName: 'italic', label: 'Italic (Ctrl+I)', editorCommand: 'toggleItalic' }}
+ ${'strike'} | ${{ contentType: 'strike', iconName: 'strikethrough', label: 'Strikethrough (Ctrl+Shift+X)', editorCommand: 'toggleStrike' }}
+ ${'blockquote'} | ${{ contentType: 'blockquote', iconName: 'quote', label: 'Insert a quote', editorCommand: 'toggleBlockquote' }}
+ ${'code'} | ${{ contentType: 'code', iconName: 'code', label: 'Code', editorCommand: 'toggleCode' }}
+ ${'link'} | ${{ contentType: 'link', iconName: 'link', label: 'Insert link (Ctrl+K)', editorCommand: 'editLink' }}
+ ${'link'} | ${{}}
+ ${'bullet-list'} | ${{ contentType: 'bulletList', iconName: 'list-bulleted', label: 'Add a bullet list', editorCommand: 'toggleBulletList' }}
+ ${'ordered-list'} | ${{ contentType: 'orderedList', iconName: 'list-numbered', label: 'Add a numbered list', editorCommand: 'toggleOrderedList' }}
+ ${'task-list'} | ${{ contentType: 'taskList', iconName: 'list-task', label: 'Add a checklist', editorCommand: 'toggleTaskList' }}
+ ${'code-suggestion'} | ${{ contentType: 'codeSuggestion', iconName: 'doc-code', label: 'Insert suggestion', editorCommand: 'insertCodeSuggestion' }}
+ ${'attachment'} | ${{}}
+ ${'table'} | ${{}}
+ ${'more'} | ${{}}
`('given a $testId toolbar control', ({ testId, controlProps }) => {
beforeEach(() => {
buildWrapper();
@@ -69,17 +78,70 @@ describe('content_editor/components/formatting_toolbar', () => {
});
});
- it('renders an editor mode dropdown', () => {
- buildWrapper();
+ describe('MacOS shortcuts', () => {
+ beforeEach(() => {
+ window.gl = { client: { isMac: true } };
+
+ buildWrapper();
+ });
- expect(wrapper.findComponent(EditorModeSwitcher).exists()).toBe(true);
+ it.each`
+ testId | label
+ ${'bold'} | ${'Bold (⌘B)'}
+ ${'italic'} | ${'Italic (⌘I)'}
+ ${'strike'} | ${'Strikethrough (⌘⇧X)'}
+ ${'link'} | ${'Insert link (⌘K)'}
+ `('shows label $label for $testId', ({ testId, label }) => {
+ expect(wrapper.findByTestId(testId).props('label')).toBe(label);
+ });
});
describe('when attachment button is hidden', () => {
it('does not show the attachment button', () => {
- buildWrapper({ hideAttachmentButton: true });
+ buildWrapper({ props: { hideAttachmentButton: true } });
expect(wrapper.findByTestId('attachment').exists()).toBe(false);
});
});
+
+ describe('when selecting a saved reply from the comment templates dropdown', () => {
+ it('updates the rich text editor with the saved comment', async () => {
+ const tiptapEditor = createTestEditor();
+
+ buildWrapper({
+ provide: {
+ tiptapEditor,
+ contentEditor,
+ newCommentTemplatePath: 'some/path',
+ },
+ });
+
+ const commands = mockChainedCommands(tiptapEditor, ['focus', 'pasteContent', 'run']);
+ await wrapper
+ .findComponent(CommentTemplatesDropdown)
+ .vm.$emit('select', 'Some saved comment');
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.pasteContent).toHaveBeenCalledWith('Some saved comment');
+ expect(commands.run).toHaveBeenCalled();
+ });
+
+ it('does not show the saved replies icon if newCommentTemplatePath is not provided', () => {
+ buildWrapper();
+
+ expect(wrapper.findComponent(CommentTemplatesDropdown).exists()).toBe(false);
+ });
+ });
+
+ it('hides code suggestions icon if the user cannot make suggestions', () => {
+ buildWrapper({
+ provide: {
+ contentEditor: {
+ codeSuggestionsConfig: { canSuggest: false },
+ },
+ },
+ });
+
+ expect(wrapper.findByTestId('code-suggestion').exists()).toBe(false);
+ });
});
diff --git a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
index 9d34d9d0e9e..ee3ad59bf9a 100644
--- a/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/suggestions_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem, GlAvatarLabeled, GlLoadingIcon } from '@gitlab/ui';
+import { GlAvatarLabeled, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import SuggestionsDropdown from '~/content_editor/components/suggestions_dropdown.vue';
@@ -113,7 +113,7 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
${'emoji'} | ${'emoji'} | ${':'} | ${exampleEmoji} | ${`😃`} | ${insertedEmojiProps}
`(
'runs a command to insert the selected $referenceType',
- ({ char, nodeType, referenceType, reference, insertedText, insertedProps }) => {
+ async ({ char, nodeType, referenceType, reference, insertedText, insertedProps }) => {
const commandSpy = jest.fn();
buildWrapper({
@@ -129,7 +129,10 @@ describe('~/content_editor/components/suggestions_dropdown', () => {
},
});
- wrapper.findComponent(GlDropdownItem).vm.$emit('click');
+ await wrapper
+ .findByTestId('content-editor-suggestions-dropdown')
+ .find('li .gl-new-dropdown-item-content')
+ .trigger('click');
expect(commandSpy).toHaveBeenCalledWith(
expect.objectContaining({
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 cbeea90dcb4..e802681dfc6 100644
--- a/spec/frontend/content_editor/components/wrappers/code_block_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/code_block_spec.js
@@ -6,11 +6,26 @@ import eventHubFactory from '~/helpers/event_hub_factory';
import SandboxedMermaid from '~/behaviors/components/sandboxed_mermaid.vue';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
import Diagram from '~/content_editor/extensions/diagram';
+import CodeSuggestion from '~/content_editor/extensions/code_suggestion';
import CodeBlockWrapper from '~/content_editor/components/wrappers/code_block.vue';
import codeBlockLanguageLoader from '~/content_editor/services/code_block_language_loader';
-import { emitEditorEvent, createTestEditor } from '../../test_utils';
+import { emitEditorEvent, createTestEditor, mockChainedCommands } from '../../test_utils';
+
+const SAMPLE_README_CONTENT = `# Sample README
+
+This is a sample README.
+
+## Usage
+
+\`\`\`yaml
+foo: bar
+\`\`\`
+`;
jest.mock('~/content_editor/services/code_block_language_loader');
+jest.mock('~/content_editor/services/utils', () => ({
+ memoizedGet: jest.fn().mockResolvedValue(SAMPLE_README_CONTENT),
+}));
describe('content/components/wrappers/code_block', () => {
const language = 'yaml';
@@ -21,7 +36,7 @@ describe('content/components/wrappers/code_block', () => {
let eventHub;
const buildEditor = () => {
- tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight, Diagram] });
+ tiptapEditor = createTestEditor({ extensions: [CodeBlockHighlight, Diagram, CodeSuggestion] });
contentEditor = { renderDiagram: jest.fn().mockResolvedValue('url/to/some/diagram') };
eventHub = eventHubFactory();
};
@@ -76,7 +91,7 @@ describe('content/components/wrappers/code_block', () => {
it('renders label indicating that code block is frontmatter', () => {
createWrapper({ isFrontmatter: true, language });
- const label = wrapper.find('[data-testid="frontmatter-label"]');
+ const label = wrapper.findByTestId('frontmatter-label');
expect(label.text()).toEqual('frontmatter:yaml');
expect(label.classes()).toEqual(['gl-absolute', 'gl-top-0', 'gl-right-3']);
@@ -143,4 +158,222 @@ describe('content/components/wrappers/code_block', () => {
expect(wrapper.find('img').exists()).toBe(false);
});
});
+
+ describe('code suggestions', () => {
+ const nodeAttrs = { language: 'suggestion', isCodeSuggestion: true, langParams: '-0+0' };
+ const findCodeSuggestionBoxText = () =>
+ wrapper.findByTestId('code-suggestion-box').text().replace(/\s+/gm, ' ');
+ const findCodeDeleted = () =>
+ wrapper
+ .findByTestId('suggestion-deleted')
+ .findAll('code')
+ .wrappers.map((w) => w.html())
+ .join('\n');
+ const findCodeAdded = () =>
+ wrapper
+ .findByTestId('suggestion-added')
+ .findAll('code')
+ .wrappers.map((w) => w.html())
+ .join('\n');
+
+ let commands;
+
+ const clickButton = async ({ button, expectedLangParams }) => {
+ await button.trigger('click');
+
+ expect(commands.updateAttributes).toHaveBeenCalledWith('codeSuggestion', {
+ langParams: expectedLangParams,
+ });
+ expect(commands.run).toHaveBeenCalled();
+
+ await wrapper.setProps({ node: { attrs: { ...nodeAttrs, langParams: expectedLangParams } } });
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ };
+
+ beforeEach(async () => {
+ contentEditor = {
+ codeSuggestionsConfig: {
+ canSuggest: true,
+ line: { new_line: 5 },
+ lines: [{ new_line: 5 }],
+ showPopover: false,
+ diffFile: {
+ view_path:
+ '/gitlab-org/gitlab-test/-/blob/468abc807a2b2572f43e72c743b76cee6db24025/README.md',
+ },
+ },
+ };
+
+ commands = mockChainedCommands(tiptapEditor, ['updateAttributes', 'run']);
+
+ createWrapper(nodeAttrs);
+ await emitEditorEvent({ event: 'transaction', tiptapEditor });
+ });
+
+ it('shows a code suggestion block', () => {
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 5');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(
+ `"<code data-line-number=\\"5\\">## Usage\u200b</code>"`,
+ );
+ expect(findCodeAdded()).toMatchInlineSnapshot(
+ `"<code data-line-number=\\"5\\">\u200b</code>"`,
+ );
+ });
+
+ describe('decrement line start button', () => {
+ let button;
+
+ beforeEach(() => {
+ button = wrapper.findByTestId('decrement-line-start');
+ });
+
+ it('decrements the start line number', async () => {
+ await clickButton({ button, expectedLangParams: '-1+0' });
+
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 4 to 5');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(`
+ "<code data-line-number=\\"4\\">\u200b
+ </code>
+ <code data-line-number=\\"5\\">## Usage\u200b</code>"
+ `);
+ });
+
+ it('is disabled if the start line is already 1', async () => {
+ expect(button.attributes('disabled')).toBeUndefined();
+
+ await clickButton({ button, expectedLangParams: '-1+0' });
+ await clickButton({ button, expectedLangParams: '-2+0' });
+ await clickButton({ button, expectedLangParams: '-3+0' });
+ await clickButton({ button, expectedLangParams: '-4+0' });
+
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 1 to 5');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(`
+ "<code data-line-number=\\"1\\"># Sample README\u200b
+ </code>
+ <code data-line-number=\\"2\\">\u200b
+ </code>
+ <code data-line-number=\\"3\\">This is a sample README.\u200b
+ </code>
+ <code data-line-number=\\"4\\">\u200b
+ </code>
+ <code data-line-number=\\"5\\">## Usage\u200b</code>"
+ `);
+
+ expect(button.attributes('disabled')).toBe('disabled');
+ });
+ });
+
+ describe('increment line start button', () => {
+ let decrementButton;
+ let button;
+
+ beforeEach(() => {
+ decrementButton = wrapper.findByTestId('decrement-line-start');
+ button = wrapper.findByTestId('increment-line-start');
+ });
+
+ it('is disabled if the start line is already the current line', async () => {
+ expect(button.attributes('disabled')).toBe('disabled');
+
+ // decrement once, increment once
+ await clickButton({ button: decrementButton, expectedLangParams: '-1+0' });
+ expect(button.attributes('disabled')).toBeUndefined();
+ await clickButton({ button, expectedLangParams: '-0+0' });
+
+ expect(button.attributes('disabled')).toBe('disabled');
+ });
+
+ it('increments the start line number', async () => {
+ // decrement twice, increment once
+ await clickButton({ button: decrementButton, expectedLangParams: '-1+0' });
+ await clickButton({ button: decrementButton, expectedLangParams: '-2+0' });
+ await clickButton({ button, expectedLangParams: '-1+0' });
+
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 4 to 5');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(`
+ "<code data-line-number=\\"4\\">\u200b
+ </code>
+ <code data-line-number=\\"5\\">## Usage\u200b</code>"
+ `);
+ });
+ });
+
+ describe('decrement line end button', () => {
+ let incrementButton;
+ let button;
+
+ beforeEach(() => {
+ incrementButton = wrapper.findByTestId('increment-line-end');
+ button = wrapper.findByTestId('decrement-line-end');
+ });
+
+ it('is disabled if the line end is already the current line', async () => {
+ expect(button.attributes('disabled')).toBe('disabled');
+
+ // increment once, decrement once
+ await clickButton({ button: incrementButton, expectedLangParams: '-0+1' });
+ expect(button.attributes('disabled')).toBeUndefined();
+ await clickButton({ button, expectedLangParams: '-0+0' });
+
+ expect(button.attributes('disabled')).toBe('disabled');
+ });
+
+ it('increments the end line number', async () => {
+ // increment twice, decrement once
+ await clickButton({ button: incrementButton, expectedLangParams: '-0+1' });
+ await clickButton({ button: incrementButton, expectedLangParams: '-0+2' });
+ await clickButton({ button, expectedLangParams: '-0+1' });
+
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 6');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(`
+ "<code data-line-number=\\"5\\">## Usage\u200b
+ </code>
+ <code data-line-number=\\"6\\">\u200b</code>"
+ `);
+ });
+ });
+
+ describe('increment line end button', () => {
+ let button;
+
+ beforeEach(() => {
+ button = wrapper.findByTestId('increment-line-end');
+ });
+
+ it('decrements the start line number', async () => {
+ await clickButton({ button, expectedLangParams: '-0+1' });
+
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 6');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(`
+ "<code data-line-number=\\"5\\">## Usage\u200b
+ </code>
+ <code data-line-number=\\"6\\">\u200b</code>"
+ `);
+ });
+
+ it('is disabled if the end line is EOF', async () => {
+ expect(button.attributes('disabled')).toBeUndefined();
+
+ await clickButton({ button, expectedLangParams: '-0+1' });
+ await clickButton({ button, expectedLangParams: '-0+2' });
+ await clickButton({ button, expectedLangParams: '-0+3' });
+ await clickButton({ button, expectedLangParams: '-0+4' });
+
+ expect(findCodeSuggestionBoxText()).toContain('Suggested change From line 5 to 9');
+ expect(findCodeDeleted()).toMatchInlineSnapshot(`
+ "<code data-line-number=\\"5\\">## Usage\u200b
+ </code>
+ <code data-line-number=\\"6\\">\u200b
+ </code>
+ <code data-line-number=\\"7\\">\`\`\`yaml\u200b
+ </code>
+ <code data-line-number=\\"8\\">foo: bar\u200b
+ </code>
+ <code data-line-number=\\"9\\">\`\`\`\u200b</code>"
+ `);
+
+ expect(button.attributes('disabled')).toBe('disabled');
+ });
+ });
+ });
});
diff --git a/spec/frontend/content_editor/components/wrappers/image_spec.js b/spec/frontend/content_editor/components/wrappers/image_spec.js
new file mode 100644
index 00000000000..0ac3b7e9465
--- /dev/null
+++ b/spec/frontend/content_editor/components/wrappers/image_spec.js
@@ -0,0 +1,100 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ImageWrapper from '~/content_editor/components/wrappers/image.vue';
+import { createTestEditor, mockChainedCommands } from '../../test_utils';
+
+describe('content/components/wrappers/image_spec', () => {
+ let wrapper;
+ let tiptapEditor;
+
+ const createWrapper = (node = {}) => {
+ tiptapEditor = createTestEditor();
+ wrapper = shallowMountExtended(ImageWrapper, {
+ propsData: {
+ editor: tiptapEditor,
+ node,
+ getPos: jest.fn().mockReturnValue(12),
+ },
+ });
+ };
+
+ const findHandle = (handle) => wrapper.findByTestId(`image-resize-${handle}`);
+ const findImage = () => wrapper.find('img');
+
+ it('renders an image with the given attributes', () => {
+ createWrapper({
+ type: 'image',
+ attrs: { src: 'image.png', alt: 'My Image', width: 200, height: 200 },
+ });
+
+ expect(findImage().attributes()).toMatchObject({
+ src: 'image.png',
+ alt: 'My Image',
+ height: '200',
+ width: '200',
+ });
+ });
+
+ it('sets width and height to auto if not provided', () => {
+ createWrapper({ type: 'image', attrs: { src: 'image.png', alt: 'My Image' } });
+
+ expect(findImage().attributes()).toMatchObject({
+ src: 'image.png',
+ alt: 'My Image',
+ height: 'auto',
+ width: 'auto',
+ });
+ });
+
+ it('renders corner resize handles', () => {
+ createWrapper({ type: 'image', attrs: { src: 'image.png', alt: 'My Image' } });
+
+ expect(findHandle('nw').exists()).toBe(true);
+ expect(findHandle('ne').exists()).toBe(true);
+ expect(findHandle('sw').exists()).toBe(true);
+ expect(findHandle('se').exists()).toBe(true);
+ });
+
+ describe.each`
+ handle | htmlElementAttributes | tiptapNodeAttributes
+ ${'nw'} | ${{ width: '300', height: '75' }} | ${{ width: 300, height: 75 }}
+ ${'ne'} | ${{ width: '500', height: '125' }} | ${{ width: 500, height: 125 }}
+ ${'sw'} | ${{ width: '300', height: '75' }} | ${{ width: 300, height: 75 }}
+ ${'se'} | ${{ width: '500', height: '125' }} | ${{ width: 500, height: 125 }}
+ `('resizing using $handle', ({ handle, htmlElementAttributes, tiptapNodeAttributes }) => {
+ let handleEl;
+
+ const initialMousePosition = { screenX: 200, screenY: 200 };
+ const finalMousePosition = { screenX: 300, screenY: 300 };
+
+ beforeEach(() => {
+ createWrapper({
+ type: 'image',
+ attrs: { src: 'image.png', alt: 'My Image', width: 400, height: 100 },
+ });
+
+ handleEl = findHandle(handle);
+ handleEl.element.dispatchEvent(new MouseEvent('mousedown', initialMousePosition));
+ document.dispatchEvent(new MouseEvent('mousemove', finalMousePosition));
+ });
+
+ it('resizes the image properly on mousedown+mousemove', () => {
+ expect(findImage().attributes()).toMatchObject(htmlElementAttributes);
+ });
+
+ it('updates prosemirror doc state on mouse release with final size', () => {
+ const commands = mockChainedCommands(tiptapEditor, [
+ 'focus',
+ 'updateAttributes',
+ 'setNodeSelection',
+ 'run',
+ ]);
+
+ document.dispatchEvent(new MouseEvent('mouseup'));
+
+ expect(commands.focus).toHaveBeenCalled();
+ expect(commands.updateAttributes).toHaveBeenCalledWith('image', tiptapNodeAttributes);
+ expect(commands.setNodeSelection).toHaveBeenCalledWith(12);
+ expect(commands.run).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/components/wrappers/reference_spec.js b/spec/frontend/content_editor/components/wrappers/reference_spec.js
index 828b92a6b1e..132e0e52ae5 100644
--- a/spec/frontend/content_editor/components/wrappers/reference_spec.js
+++ b/spec/frontend/content_editor/components/wrappers/reference_spec.js
@@ -1,4 +1,5 @@
import { GlLink } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ReferenceWrapper from '~/content_editor/components/wrappers/reference.vue';
@@ -8,6 +9,13 @@ describe('content/components/wrappers/reference', () => {
const createWrapper = (node = {}) => {
wrapper = shallowMountExtended(ReferenceWrapper, {
propsData: { node },
+ provide: {
+ contentEditor: {
+ resolveReference: jest.fn().mockResolvedValue({
+ href: 'https://gitlab.com/gitlab-org/gitlab-test/-/issues/252522',
+ }),
+ },
+ },
});
};
@@ -43,4 +51,14 @@ describe('content/components/wrappers/reference', () => {
expect(link.text()).toBe('@root');
expect(link.classes('current-user')).toBe(true);
});
+
+ it('renders the href of the reference correctly', async () => {
+ createWrapper({ attrs: { referenceType: 'issue', text: '#252522' } });
+ await waitForPromises();
+
+ const link = wrapper.findComponent(GlLink);
+ expect(link.attributes('href')).toBe(
+ 'https://gitlab.com/gitlab-org/gitlab-test/-/issues/252522',
+ );
+ });
});
diff --git a/spec/frontend/content_editor/extensions/code_suggestion_spec.js b/spec/frontend/content_editor/extensions/code_suggestion_spec.js
new file mode 100644
index 00000000000..86656fb96c3
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/code_suggestion_spec.js
@@ -0,0 +1,128 @@
+import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import CodeSuggestion from '~/content_editor/extensions/code_suggestion';
+import {
+ createTestEditor,
+ createDocBuilder,
+ triggerNodeInputRule,
+ expectDocumentAfterTransaction,
+ sleep,
+} from '../test_utils';
+
+const SAMPLE_README_CONTENT = `# Sample README
+
+This is a sample README.
+
+## Usage
+
+\`\`\`yaml
+foo: bar
+\`\`\`
+`;
+
+jest.mock('~/content_editor/services/utils', () => ({
+ memoizedGet: jest.fn().mockResolvedValue(SAMPLE_README_CONTENT),
+}));
+
+describe('content_editor/extensions/code_suggestion', () => {
+ let tiptapEditor;
+ let doc;
+ let codeSuggestion;
+
+ const codeSuggestionConfig = {
+ canSuggest: true,
+ line: { new_line: 5 },
+ lines: [{ new_line: 5 }],
+ showPopover: false,
+ diffFile: {
+ view_path:
+ '/gitlab-org/gitlab-test/-/blob/468abc807a2b2572f43e72c743b76cee6db24025/README.md',
+ },
+ };
+
+ const createEditor = (config = {}) => {
+ tiptapEditor = createTestEditor({
+ extensions: [
+ CodeBlockHighlight,
+ CodeSuggestion.configure({ config: { ...codeSuggestionConfig, ...config } }),
+ ],
+ });
+
+ ({
+ builders: { doc, codeSuggestion },
+ } = createDocBuilder({
+ tiptapEditor,
+ names: {
+ codeBlock: { nodeType: CodeBlockHighlight.name },
+ codeSuggestion: { nodeType: CodeSuggestion.name },
+ },
+ }));
+ };
+
+ describe('insertCodeSuggestion command', () => {
+ it('creates a correct suggestion for a single line selection', async () => {
+ createEditor({ line: { new_line: 5 }, lines: [] });
+
+ await expectDocumentAfterTransaction({
+ number: 1,
+ tiptapEditor,
+ action: () => tiptapEditor.commands.insertCodeSuggestion(),
+ expectedDoc: doc(codeSuggestion({ langParams: '-0+0' }, '## Usage')),
+ });
+ });
+
+ it('creates a correct suggestion for a multi-line selection', async () => {
+ createEditor({
+ line: { new_line: 9 },
+ lines: [
+ { new_line: 5 },
+ { new_line: 6 },
+ { new_line: 7 },
+ { new_line: 8 },
+ { new_line: 9 },
+ ],
+ });
+
+ await expectDocumentAfterTransaction({
+ number: 1,
+ tiptapEditor,
+ action: () => tiptapEditor.commands.insertCodeSuggestion(),
+ expectedDoc: doc(
+ codeSuggestion({ langParams: '-4+0' }, '## Usage\n\n```yaml\nfoo: bar\n```'),
+ ),
+ });
+ });
+
+ it('does not insert a new suggestion if already inside a suggestion', async () => {
+ const initialDoc = codeSuggestion({ langParams: '-0+0' }, '## Usage');
+
+ createEditor({ line: { new_line: 5 }, lines: [] });
+
+ tiptapEditor.commands.setContent(doc(initialDoc).toJSON());
+
+ jest.spyOn(tiptapEditor, 'isActive').mockReturnValue(true);
+
+ tiptapEditor.commands.insertCodeSuggestion();
+ // wait some time to be sure no other transaction happened
+ await sleep();
+
+ expect(tiptapEditor.getJSON()).toEqual(doc(initialDoc).toJSON());
+ });
+ });
+
+ describe('when typing ```suggestion input rule', () => {
+ beforeEach(() => {
+ createEditor();
+
+ triggerNodeInputRule({
+ tiptapEditor,
+ inputRuleText: '```suggestion ',
+ });
+ });
+
+ it('creates a new code suggestion block with lines -0+0', () => {
+ const expectedDoc = doc(codeSuggestion({ language: 'suggestion', langParams: '-0+0' }));
+
+ expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/extensions/comment_spec.js b/spec/frontend/content_editor/extensions/comment_spec.js
deleted file mode 100644
index 7d8ff28e4d7..00000000000
--- a/spec/frontend/content_editor/extensions/comment_spec.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import Comment from '~/content_editor/extensions/comment';
-import { createTestEditor, createDocBuilder, triggerNodeInputRule } from '../test_utils';
-
-describe('content_editor/extensions/comment', () => {
- let tiptapEditor;
- let doc;
- let comment;
-
- beforeEach(() => {
- tiptapEditor = createTestEditor({ extensions: [Comment] });
- ({
- builders: { doc, comment },
- } = createDocBuilder({
- tiptapEditor,
- names: {
- comment: { nodeType: Comment.name },
- },
- }));
- });
-
- describe('when typing the comment input rule', () => {
- it('inserts a comment node', () => {
- const expectedDoc = doc(comment());
-
- triggerNodeInputRule({ tiptapEditor, inputRuleText: '<!-- ' });
-
- expect(tiptapEditor.getJSON()).toEqual(expectedDoc.toJSON());
- });
- });
-});
diff --git a/spec/frontend/content_editor/extensions/paste_markdown_spec.js b/spec/frontend/content_editor/extensions/copy_paste_spec.js
index baf0919fec8..f8faa7869c0 100644
--- a/spec/frontend/content_editor/extensions/paste_markdown_spec.js
+++ b/spec/frontend/content_editor/extensions/copy_paste_spec.js
@@ -1,5 +1,6 @@
-import PasteMarkdown from '~/content_editor/extensions/paste_markdown';
+import CopyPaste from '~/content_editor/extensions/copy_paste';
import CodeBlockHighlight from '~/content_editor/extensions/code_block_highlight';
+import Loading from '~/content_editor/extensions/loading';
import Diagram from '~/content_editor/extensions/diagram';
import Frontmatter from '~/content_editor/extensions/frontmatter';
import Heading from '~/content_editor/extensions/heading';
@@ -10,29 +11,48 @@ import eventHubFactory from '~/helpers/event_hub_factory';
import { ALERT_EVENT } from '~/content_editor/constants';
import waitForPromises from 'helpers/wait_for_promises';
import MarkdownSerializer from '~/content_editor/services/markdown_serializer';
-import { createTestEditor, createDocBuilder, waitUntilNextDocTransaction } from '../test_utils';
+import {
+ createTestEditor,
+ createDocBuilder,
+ waitUntilNextDocTransaction,
+ sleep,
+} from '../test_utils';
const CODE_BLOCK_HTML = '<pre class="js-syntax-highlight" lang="javascript">var a = 2;</pre>';
+const CODE_SUGGESTION_HTML =
+ '<pre data-lang-params="-0+0" class="js-syntax-highlight language-suggestion" lang="suggestion">Suggested code</pre>';
const DIAGRAM_HTML =
'<img data-diagram="nomnoml" data-diagram-src="data:text/plain;base64,WzxmcmFtZT5EZWNvcmF0b3IgcGF0dGVybl0=">';
const FRONTMATTER_HTML = '<pre lang="yaml" data-lang-params="frontmatter">key: value</pre>';
-const PARAGRAPH_HTML = '<p>Some text with <strong>bold</strong> and <em>italic</em> text.</p>';
+const PARAGRAPH_HTML =
+ '<p dir="auto">Some text with <strong>bold</strong> and <em>italic</em> text.</p>';
-describe('content_editor/extensions/paste_markdown', () => {
+describe('content_editor/extensions/copy_paste', () => {
let tiptapEditor;
let doc;
let p;
let bold;
let italic;
+ let loading;
let heading;
let codeBlock;
let renderMarkdown;
+ let resolveRenderMarkdownPromise;
+ let resolveRenderMarkdownPromiseAndWait;
+
let eventHub;
const defaultData = { 'text/plain': '**bold text**' };
beforeEach(() => {
- renderMarkdown = jest.fn();
eventHub = eventHubFactory();
+ renderMarkdown = jest.fn().mockImplementation(
+ () =>
+ new Promise((resolve) => {
+ resolveRenderMarkdownPromise = resolve;
+ resolveRenderMarkdownPromiseAndWait = (data) =>
+ waitUntilNextDocTransaction({ tiptapEditor, action: () => resolve(data) });
+ }),
+ );
jest.spyOn(eventHub, '$emit');
@@ -40,21 +60,23 @@ describe('content_editor/extensions/paste_markdown', () => {
extensions: [
Bold,
Italic,
+ Loading,
CodeBlockHighlight,
Diagram,
Frontmatter,
Heading,
- PasteMarkdown.configure({ renderMarkdown, eventHub, serializer: new MarkdownSerializer() }),
+ CopyPaste.configure({ renderMarkdown, eventHub, serializer: new MarkdownSerializer() }),
],
});
({
- builders: { doc, p, bold, italic, heading, codeBlock },
+ builders: { doc, p, bold, italic, heading, loading, codeBlock },
} = createDocBuilder({
tiptapEditor,
names: {
bold: { markType: Bold.name },
italic: { markType: Italic.name },
+ loading: { nodeType: Loading.name },
heading: { nodeType: Heading.name },
codeBlock: { nodeType: CodeBlockHighlight.name },
},
@@ -102,11 +124,12 @@ describe('content_editor/extensions/paste_markdown', () => {
});
it.each`
- nodeType | html | handled | desc
- ${'codeBlock'} | ${CODE_BLOCK_HTML} | ${false} | ${'does not handle'}
- ${'diagram'} | ${DIAGRAM_HTML} | ${false} | ${'does not handle'}
- ${'frontmatter'} | ${FRONTMATTER_HTML} | ${false} | ${'does not handle'}
- ${'paragraph'} | ${PARAGRAPH_HTML} | ${true} | ${'handles'}
+ nodeType | html | handled | desc
+ ${'codeBlock'} | ${CODE_BLOCK_HTML} | ${false} | ${'does not handle'}
+ ${'codeSuggestion'} | ${CODE_SUGGESTION_HTML} | ${false} | ${'does not handle'}
+ ${'diagram'} | ${DIAGRAM_HTML} | ${false} | ${'does not handle'}
+ ${'frontmatter'} | ${FRONTMATTER_HTML} | ${false} | ${'does not handle'}
+ ${'paragraph'} | ${PARAGRAPH_HTML} | ${true} | ${'handles'}
`('$desc paste if currently a `$nodeType` is in focus', async ({ html, handled }) => {
tiptapEditor.commands.insertContent(html);
@@ -153,15 +176,51 @@ describe('content_editor/extensions/paste_markdown', () => {
});
describe('when pasting raw markdown source', () => {
+ it('shows a loading indicator while markdown is being processed', async () => {
+ const expectedDoc = doc(p(loading({ id: expect.any(String) })));
+
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+
+ it('pastes in the correct position if some content is added before the markdown is processed', async () => {
+ const expectedDoc = doc(p(bold('some markdown'), 'some content'));
+ const resolvedValue = '<strong>some markdown</strong>';
+
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+
+ tiptapEditor.commands.insertContent('some content');
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ expect(tiptapEditor.state.selection.from).toEqual(26); // end of the document
+ });
+
+ it('does not paste anything if the loading indicator is deleted before the markdown is processed', async () => {
+ const expectedDoc = doc(p());
+
+ await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ tiptapEditor.chain().selectAll().deleteSelection().run();
+ resolveRenderMarkdownPromise('some markdown');
+
+ // wait some time to be sure no transaction happened
+ await sleep();
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+
describe('when rendering markdown succeeds', () => {
+ let resolvedValue;
+
beforeEach(() => {
- renderMarkdown.mockResolvedValueOnce('<strong>bold text</strong>');
+ resolvedValue = '<strong>bold text</strong>';
});
it('transforms pasted text into a prosemirror node', async () => {
const expectedDoc = doc(p(bold('bold text')));
await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -173,6 +232,7 @@ describe('content_editor/extensions/paste_markdown', () => {
tiptapEditor.commands.setContent('Initial text and ');
await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -186,6 +246,7 @@ describe('content_editor/extensions/paste_markdown', () => {
tiptapEditor.commands.setTextSelection({ from: 13, to: 17 });
await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -193,8 +254,7 @@ describe('content_editor/extensions/paste_markdown', () => {
describe('when pasting block content in an existing paragraph', () => {
beforeEach(() => {
- renderMarkdown.mockReset();
- renderMarkdown.mockResolvedValueOnce('<h1>Heading</h1><p><strong>bold text</strong></p>');
+ resolvedValue = '<h1>Heading</h1><p><strong>bold text</strong></p>';
});
it('inserts the block content after the existing paragraph', async () => {
@@ -207,6 +267,7 @@ describe('content_editor/extensions/paste_markdown', () => {
tiptapEditor.commands.setContent('Initial text and ');
await triggerPasteEventHandlerAndWaitForTransaction(buildClipboardEvent());
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -215,9 +276,8 @@ describe('content_editor/extensions/paste_markdown', () => {
describe('when pasting html content', () => {
it('strips out any stray div, pre, span tags', async () => {
- renderMarkdown.mockResolvedValueOnce(
- '<div><span dir="auto"><strong>bold text</strong></span></div><pre><code>some code</code></pre>',
- );
+ const resolvedValue =
+ '<div><span dir="auto"><strong>bold text</strong></span></div><pre><code>some code</code></pre>';
const expectedDoc = doc(p(bold('bold text')), p('some code'));
@@ -230,6 +290,7 @@ describe('content_editor/extensions/paste_markdown', () => {
},
}),
);
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -237,8 +298,7 @@ describe('content_editor/extensions/paste_markdown', () => {
describe('when pasting text/x-gfm', () => {
it('processes the content as markdown, even if html content exists', async () => {
- renderMarkdown.mockResolvedValueOnce('<strong>bold text</strong>');
-
+ const resolvedValue = '<strong>bold text</strong>';
const expectedDoc = doc(p(bold('bold text')));
await triggerPasteEventHandlerAndWaitForTransaction(
@@ -251,6 +311,7 @@ describe('content_editor/extensions/paste_markdown', () => {
},
}),
);
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
@@ -258,9 +319,8 @@ describe('content_editor/extensions/paste_markdown', () => {
describe('when pasting vscode-editor-data', () => {
it('pastes the content as a code block', async () => {
- renderMarkdown.mockResolvedValueOnce(
- '<div class="gl-relative markdown-code-block js-markdown-code">&#x000A;<pre data-sourcepos="1:1-3:3" data-canonical-lang="ruby" class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="nb">puts</span> <span class="s2">"Hello World"</span></span></code></pre>&#x000A;<copy-code></copy-code>&#x000A;</div>',
- );
+ const resolvedValue =
+ '<div class="gl-relative markdown-code-block js-markdown-code">&#x000A;<pre data-sourcepos="1:1-3:3" data-canonical-lang="ruby" class="code highlight js-syntax-highlight language-ruby" lang="ruby" v-pre="true"><code><span id="LC1" class="line" lang="ruby"><span class="nb">puts</span> <span class="s2">"Hello World"</span></span></code></pre>&#x000A;<copy-code></copy-code>&#x000A;</div>';
const expectedDoc = doc(
codeBlock(
@@ -280,12 +340,13 @@ describe('content_editor/extensions/paste_markdown', () => {
},
}),
);
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
it('pastes as regular markdown if language is markdown', async () => {
- renderMarkdown.mockResolvedValueOnce('<p><strong>bold text</strong></p>');
+ const resolvedValue = '<p><strong>bold text</strong></p>';
const expectedDoc = doc(p(bold('bold text')));
@@ -299,6 +360,7 @@ describe('content_editor/extensions/paste_markdown', () => {
},
}),
);
+ await resolveRenderMarkdownPromiseAndWait(resolvedValue);
expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
diff --git a/spec/frontend/content_editor/extensions/hard_break_spec.js b/spec/frontend/content_editor/extensions/hard_break_spec.js
index 9e2e28b6e72..6a57e7eaa9b 100644
--- a/spec/frontend/content_editor/extensions/hard_break_spec.js
+++ b/spec/frontend/content_editor/extensions/hard_break_spec.js
@@ -3,35 +3,21 @@ import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/extensions/hard_break', () => {
let tiptapEditor;
- let eq;
+
let doc;
let p;
- let hardBreak;
beforeEach(() => {
tiptapEditor = createTestEditor({ extensions: [HardBreak] });
({
- builders: { doc, p, hardBreak },
- eq,
+ builders: { doc, p },
} = createDocBuilder({
tiptapEditor,
names: { hardBreak: { nodeType: HardBreak.name } },
}));
});
- describe('Shift-Enter shortcut', () => {
- it('inserts a hard break when shortcut is executed', () => {
- const initialDoc = doc(p(''));
- const expectedDoc = doc(p(hardBreak()));
-
- tiptapEditor.commands.setContent(initialDoc.toJSON());
- tiptapEditor.commands.keyboardShortcut('Shift-Enter');
-
- expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
- });
- });
-
describe('Mod-Enter shortcut', () => {
it('does not insert a hard break when shortcut is executed', () => {
const initialDoc = doc(p(''));
@@ -40,7 +26,7 @@ describe('content_editor/extensions/hard_break', () => {
tiptapEditor.commands.setContent(initialDoc.toJSON());
tiptapEditor.commands.keyboardShortcut('Mod-Enter');
- expect(eq(tiptapEditor.state.doc, expectedDoc)).toBe(true);
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
});
});
});
diff --git a/spec/frontend/content_editor/extensions/html_nodes_spec.js b/spec/frontend/content_editor/extensions/html_nodes_spec.js
index 24c68239025..3fe496aa708 100644
--- a/spec/frontend/content_editor/extensions/html_nodes_spec.js
+++ b/spec/frontend/content_editor/extensions/html_nodes_spec.js
@@ -28,9 +28,9 @@ describe('content_editor/extensions/html_nodes', () => {
});
it.each`
- input | insertedNodes
- ${'<div><p>foo</p></div>'} | ${() => div(p('foo'))}
- ${'<pre><p>foo</p></pre>'} | ${() => pre(p('foo'))}
+ input | insertedNodes
+ ${'<div><p dir="auto">foo</p></div>'} | ${() => div(p('foo'))}
+ ${'<pre><p dir="auto">foo</p></pre>'} | ${() => pre(p('foo'))}
`('parses and creates nodes for $input', ({ input, insertedNodes }) => {
const expectedDoc = doc(insertedNodes());
diff --git a/spec/frontend/content_editor/extensions/image_spec.js b/spec/frontend/content_editor/extensions/image_spec.js
index f73b0143fd9..69f4f4c6d65 100644
--- a/spec/frontend/content_editor/extensions/image_spec.js
+++ b/spec/frontend/content_editor/extensions/image_spec.js
@@ -35,7 +35,7 @@ describe('content_editor/extensions/image', () => {
tiptapEditor.commands.setContent(initialDoc.toJSON());
expect(tiptapEditor.getHTML()).toEqual(
- '<p><img src="/-/wikis/uploads/image.jpg" alt="image" title="this is an image"></p>',
+ '<p dir="auto"><img src="/-/wikis/uploads/image.jpg" alt="image" title="this is an image"></p>',
);
});
});
diff --git a/spec/frontend/content_editor/extensions/paragraph_spec.js b/spec/frontend/content_editor/extensions/paragraph_spec.js
new file mode 100644
index 00000000000..d04dda1871d
--- /dev/null
+++ b/spec/frontend/content_editor/extensions/paragraph_spec.js
@@ -0,0 +1,29 @@
+import { createTestEditor, createDocBuilder } from '../test_utils';
+
+describe('content_editor/extensions/paragraph', () => {
+ let tiptapEditor;
+ let doc;
+ let p;
+
+ beforeEach(() => {
+ tiptapEditor = createTestEditor();
+
+ ({
+ builders: { doc, p },
+ } = createDocBuilder({ tiptapEditor }));
+ });
+
+ describe('Shift-Enter shortcut', () => {
+ it('inserts a new paragraph when shortcut is executed', async () => {
+ const initialDoc = doc(p('hello'));
+ const expectedDoc = doc(p('hello'), p(''));
+
+ tiptapEditor.commands.setContent(initialDoc.toJSON());
+ tiptapEditor.commands.keyboardShortcut('Shift-Enter');
+
+ await Promise.resolve();
+
+ expect(tiptapEditor.state.doc.toJSON()).toEqual(expectedDoc.toJSON());
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/remark_markdown_processing_spec.js b/spec/frontend/content_editor/remark_markdown_processing_spec.js
index 927a7d59899..3d4d5b13120 100644
--- a/spec/frontend/content_editor/remark_markdown_processing_spec.js
+++ b/spec/frontend/content_editor/remark_markdown_processing_spec.js
@@ -1337,13 +1337,13 @@ content
alert("Hello world")
</script>
`,
- expectedHtml: '<p></p>',
+ expectedHtml: '<p dir="auto"></p>',
},
{
markdown: `
<foo>Hello</foo>
`,
- expectedHtml: '<p></p>',
+ expectedHtml: '<p dir="auto"></p>',
},
{
markdown: `
@@ -1356,7 +1356,7 @@ alert("Hello world")
<a id="link-id">Header</a> and other text
`,
expectedHtml:
- '<p><a target="_blank" rel="noopener noreferrer nofollow">Header</a> and other text</p>',
+ '<p dir="auto"><a target="_blank" rel="noopener noreferrer nofollow">Header</a> and other text</p>',
},
{
markdown: `
@@ -1366,11 +1366,11 @@ body {
}
</style>
`,
- expectedHtml: '<p></p>',
+ expectedHtml: '<p dir="auto"></p>',
},
{
markdown: '<div style="transform">div</div>',
- expectedHtml: '<div><p>div</p></div>',
+ expectedHtml: '<div><p dir="auto">div</p></div>',
},
])(
'removes unknown tags and unsupported attributes from HTML output',
@@ -1421,6 +1421,7 @@ body {
};
};
+ // NOTE: unicode \u001 and \u003 cannot be used in test names because they cause test report XML parsing errors
it.each`
desc | urlInput | urlOutput
${'protocol-based JS injection: simple, no spaces'} | ${protocolBasedInjectionSimpleNoSpaces} | ${null}
@@ -1439,7 +1440,7 @@ body {
${'protocol-based JS injection: preceding colon'} | ${":javascript:alert('XSS');"} | ${":javascript:alert('XSS');"}
${'protocol-based JS injection: null char'} | ${"java\0script:alert('XSS')"} | ${"java�script:alert('XSS')"}
${'protocol-based JS injection: invalid URL char'} | ${"java\\script:alert('XSS')"} | ${"java\\script:alert('XSS')"}
- `('sanitize $desc:\n\tURL "$urlInput" becomes "$urlOutput"', ({ urlInput, urlOutput }) => {
+ `('sanitize $desc becomes "$urlOutput"', ({ urlInput, urlOutput }) => {
const exampleFactories = [docWithImageFactory, docWithLinkFactory];
exampleFactories.forEach(async (exampleFactory) => {
diff --git a/spec/frontend/content_editor/services/code_suggestion_utils_spec.js b/spec/frontend/content_editor/services/code_suggestion_utils_spec.js
new file mode 100644
index 00000000000..f26d33adf4c
--- /dev/null
+++ b/spec/frontend/content_editor/services/code_suggestion_utils_spec.js
@@ -0,0 +1,53 @@
+import {
+ lineOffsetToLangParams,
+ langParamsToLineOffset,
+ toAbsoluteLineOffset,
+ getLines,
+ appendNewlines,
+} from '~/content_editor/services/code_suggestion_utils';
+
+describe('content_editor/services/code_suggestion_utils', () => {
+ describe('lineOffsetToLangParams', () => {
+ it.each`
+ lineOffset | expected
+ ${[0, 0]} | ${'-0+0'}
+ ${[0, 2]} | ${'-0+2'}
+ ${[1, 1]} | ${'+1+1'}
+ ${[-1, 1]} | ${'-1+1'}
+ `('converts line offset $lineOffset to lang params $expected', ({ lineOffset, expected }) => {
+ expect(lineOffsetToLangParams(lineOffset)).toBe(expected);
+ });
+ });
+
+ describe('langParamsToLineOffset', () => {
+ it.each`
+ langParams | expected
+ ${'-0+0'} | ${[-0, 0]}
+ ${'-0+2'} | ${[-0, 2]}
+ ${'+1+1'} | ${[1, 1]}
+ ${'-1+1'} | ${[-1, 1]}
+ `('converts lang params $langParams to line offset $expected', ({ langParams, expected }) => {
+ expect(langParamsToLineOffset(langParams)).toEqual(expected);
+ });
+ });
+
+ describe('toAbsoluteLineOffset', () => {
+ it('adds line number to line offset', () => {
+ expect(toAbsoluteLineOffset([-2, 3], 72)).toEqual([70, 75]);
+ });
+ });
+
+ describe('getLines', () => {
+ it('returns lines from allLines', () => {
+ const allLines = ['foo', 'bar', 'baz', 'qux', 'quux'];
+ expect(getLines([2, 4], allLines)).toEqual(['bar', 'baz', 'qux']);
+ });
+ });
+
+ describe('appendNewlines', () => {
+ it('appends zero-width space to each line', () => {
+ const lines = ['foo', 'bar', 'baz'];
+ expect(appendNewlines(lines)).toEqual(['foo\u200b\n', 'bar\u200b\n', 'baz\u200b']);
+ });
+ });
+});
diff --git a/spec/frontend/content_editor/services/create_content_editor_spec.js b/spec/frontend/content_editor/services/create_content_editor_spec.js
index b9a9c3ccd17..b68d57971b9 100644
--- a/spec/frontend/content_editor/services/create_content_editor_spec.js
+++ b/spec/frontend/content_editor/services/create_content_editor_spec.js
@@ -46,14 +46,6 @@ describe('content_editor/services/create_content_editor', () => {
});
});
- it('sets gl-shadow-none! class selector to the tiptapEditor instance', () => {
- expect(editor.tiptapEditor.options.editorProps).toMatchObject({
- attributes: {
- class: 'gl-shadow-none!',
- },
- });
- });
-
it('allows providing external content editor extensions', () => {
const labelReference = 'this is a ~group::editor';
const { tiptapExtension, serializer } = createTestContentEditorExtension();
diff --git a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
index a9960918e62..1f7b56ef762 100644
--- a/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
+++ b/spec/frontend/content_editor/services/gl_api_markdown_deserializer_spec.js
@@ -1,6 +1,5 @@
import createMarkdownDeserializer from '~/content_editor/services/gl_api_markdown_deserializer';
import Bold from '~/content_editor/extensions/bold';
-import Comment from '~/content_editor/extensions/comment';
import { createTestEditor, createDocBuilder } from '../test_utils';
describe('content_editor/services/gl_api_markdown_deserializer', () => {
@@ -8,21 +7,19 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
let doc;
let p;
let bold;
- let comment;
let tiptapEditor;
beforeEach(() => {
tiptapEditor = createTestEditor({
- extensions: [Bold, Comment],
+ extensions: [Bold],
});
({
- builders: { doc, p, bold, comment },
+ builders: { doc, p, bold },
} = createDocBuilder({
tiptapEditor,
names: {
bold: { markType: Bold.name },
- comment: { nodeType: Comment.name },
},
}));
renderMarkdown = jest.fn();
@@ -35,16 +32,16 @@ describe('content_editor/services/gl_api_markdown_deserializer', () => {
beforeEach(async () => {
const deserializer = createMarkdownDeserializer({ render: renderMarkdown });
- renderMarkdown.mockResolvedValueOnce(`<p><strong>${text}</strong></p><!-- some comment -->`);
+ renderMarkdown.mockResolvedValueOnce(`<p><strong>${text}</strong></p>`);
result = await deserializer.deserialize({
- markdown: '**Bold text**\n<!-- some comment -->',
+ markdown: '**Bold text**',
schema: tiptapEditor.schema,
});
});
it('transforms HTML returned by render function to a ProseMirror document', () => {
- const document = doc(p(bold(text)), comment(' some comment '));
+ const document = doc(p(bold(text)));
expect(result.document.toJSON()).toEqual(document.toJSON());
});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 4521822042c..7be8114902a 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -3,7 +3,6 @@ import Bold from '~/content_editor/extensions/bold';
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 Comment from '~/content_editor/extensions/comment';
import DescriptionItem from '~/content_editor/extensions/description_item';
import DescriptionList from '~/content_editor/extensions/description_list';
import Details from '~/content_editor/extensions/details';
@@ -56,7 +55,6 @@ const {
bulletList,
code,
codeBlock,
- comment,
details,
detailsContent,
div,
@@ -99,7 +97,6 @@ const {
bulletList: { nodeType: BulletList.name },
code: { markType: Code.name },
codeBlock: { nodeType: CodeBlockHighlight.name },
- comment: { nodeType: Comment.name },
details: { nodeType: Details.name },
detailsContent: { nodeType: DetailsContent.name },
descriptionItem: { nodeType: DescriptionItem.name },
@@ -187,30 +184,6 @@ describe('markdownSerializer', () => {
);
});
- it('correctly serializes a comment node', () => {
- expect(serialize(paragraph('hi'), comment(' this is a\ncomment '))).toBe(
- `
-hi
-
-<!-- this is a
-comment -->
- `.trim(),
- );
- });
-
- it('correctly renders a comment with markdown in it without adding any slashes', () => {
- expect(serialize(paragraph('hi'), comment('this is a list\n- a\n- b\n- c'))).toBe(
- `
-hi
-
-<!--this is a list
-- a
-- b
-- c-->
- `.trim(),
- );
- });
-
it('escapes < and > in a paragraph', () => {
expect(
serialize(paragraph(text("some prose: <this> and </this> looks like code, but isn't"))),
diff --git a/spec/frontend/content_editor/test_utils.js b/spec/frontend/content_editor/test_utils.js
index 2184a829cf0..f1c9fd47eb7 100644
--- a/spec/frontend/content_editor/test_utils.js
+++ b/spec/frontend/content_editor/test_utils.js
@@ -1,7 +1,4 @@
import { Node } from '@tiptap/core';
-import { Document } from '@tiptap/extension-document';
-import { Paragraph } from '@tiptap/extension-paragraph';
-import { Text } from '@tiptap/extension-text';
import { Editor } from '@tiptap/vue-2';
import { builders, eq } from 'prosemirror-test-builder';
import { nextTick } from 'vue';
@@ -12,12 +9,12 @@ import Bold from '~/content_editor/extensions/bold';
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 Comment from '~/content_editor/extensions/comment';
import DescriptionItem from '~/content_editor/extensions/description_item';
import DescriptionList from '~/content_editor/extensions/description_list';
import Details from '~/content_editor/extensions/details';
import DetailsContent from '~/content_editor/extensions/details_content';
import Diagram from '~/content_editor/extensions/diagram';
+import Document from '~/content_editor/extensions/document';
import DrawioDiagram from '~/content_editor/extensions/drawio_diagram';
import Emoji from '~/content_editor/extensions/emoji';
import FootnoteDefinition from '~/content_editor/extensions/footnote_definition';
@@ -36,6 +33,7 @@ import Italic from '~/content_editor/extensions/italic';
import Link from '~/content_editor/extensions/link';
import ListItem from '~/content_editor/extensions/list_item';
import OrderedList from '~/content_editor/extensions/ordered_list';
+import Paragraph from '~/content_editor/extensions/paragraph';
import ReferenceDefinition from '~/content_editor/extensions/reference_definition';
import Reference from '~/content_editor/extensions/reference';
import ReferenceLabel from '~/content_editor/extensions/reference_label';
@@ -47,10 +45,13 @@ 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 Text from '~/content_editor/extensions/text';
import Video from '~/content_editor/extensions/video';
import HTMLMarks from '~/content_editor/extensions/html_marks';
import HTMLNodes from '~/content_editor/extensions/html_nodes';
+export const DEFAULT_WAIT_TIMEOUT = 100;
+
export const createDocBuilder = ({ tiptapEditor, names = {} }) => {
const docBuilders = builders(tiptapEditor.schema, {
p: { nodeType: 'paragraph' },
@@ -239,6 +240,16 @@ export const waitUntilTransaction = ({ tiptapEditor, number, action }) => {
});
};
+export const sleep = (time = DEFAULT_WAIT_TIMEOUT) => {
+ jest.useRealTimers();
+ const promise = new Promise((resolve) => {
+ setTimeout(resolve, time);
+ });
+ jest.useFakeTimers();
+
+ return promise;
+};
+
export const expectDocumentAfterTransaction = ({ tiptapEditor, number, expectedDoc, action }) => {
return new Promise((resolve) => {
let counter = 0;
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js
index 6672d3eb18b..5bce0ca3746 100644
--- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_approved_spec.js
@@ -1,21 +1,18 @@
-import events from 'test_fixtures/controller/users/activity.json';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { EVENT_TYPE_APPROVED } from '~/contribution_events/constants';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ContributionEventApproved from '~/contribution_events/components/contribution_event/contribution_event_approved.vue';
import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
-import TargetLink from '~/contribution_events/components/target_link.vue';
-import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
+import { eventApproved } from '../../utils';
-const eventApproved = events.find((event) => event.action === EVENT_TYPE_APPROVED);
+const defaultPropsData = {
+ event: eventApproved(),
+};
describe('ContributionEventApproved', () => {
let wrapper;
const createComponent = () => {
- wrapper = mountExtended(ContributionEventApproved, {
- propsData: {
- event: eventApproved,
- },
+ wrapper = shallowMountExtended(ContributionEventApproved, {
+ propsData: defaultPropsData,
});
};
@@ -25,23 +22,10 @@ describe('ContributionEventApproved', () => {
it('renders `ContributionEventBase`', () => {
expect(wrapper.findComponent(ContributionEventBase).props()).toEqual({
- event: eventApproved,
+ event: defaultPropsData.event,
iconName: 'approval-solid',
iconClass: 'gl-text-green-500',
+ message: ContributionEventApproved.i18n.message,
});
});
-
- it('renders message', () => {
- expect(wrapper.findByTestId('event-body').text()).toBe(
- `Approved merge request ${eventApproved.target.reference_link_text} in ${eventApproved.resource_parent.full_name}.`,
- );
- });
-
- it('renders target link', () => {
- expect(wrapper.findComponent(TargetLink).props('event')).toEqual(eventApproved);
- });
-
- it('renders resource parent link', () => {
- expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual(eventApproved);
- });
});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
index 8c951e20bed..310966243d1 100644
--- a/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_base_spec.js
@@ -1,23 +1,27 @@
import { GlAvatarLabeled, GlAvatarLink, GlIcon } from '@gitlab/ui';
-import events from 'test_fixtures/controller/users/activity.json';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-
-const [event] = events;
+import TargetLink from '~/contribution_events/components/target_link.vue';
+import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
+import { eventApproved } from '../../utils';
describe('ContributionEventBase', () => {
let wrapper;
const defaultPropsData = {
- event,
+ event: eventApproved(),
iconName: 'approval-solid',
iconClass: 'gl-text-green-500',
+ message: 'Approved merge request %{targetLink} in %{resourceParentLink}.',
};
- const createComponent = () => {
- wrapper = shallowMountExtended(ContributionEventBase, {
- propsData: defaultPropsData,
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(ContributionEventBase, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
scopedSlots: {
default: '<div data-testid="default-slot"></div>',
'additional-info': '<div data-testid="additional-info-slot"></div>',
@@ -25,38 +29,75 @@ describe('ContributionEventBase', () => {
});
};
- beforeEach(() => {
+ it('renders avatar', () => {
createComponent();
- });
- it('renders avatar', () => {
const avatarLink = wrapper.findComponent(GlAvatarLink);
+ const avatarLabeled = avatarLink.findComponent(GlAvatarLabeled);
- expect(avatarLink.attributes('href')).toBe(event.author.web_url);
- expect(avatarLink.findComponent(GlAvatarLabeled).attributes()).toMatchObject({
- label: event.author.name,
- sublabel: `@${event.author.username}`,
- src: event.author.avatar_url,
+ expect(avatarLink.attributes('href')).toBe(defaultPropsData.event.author.web_url);
+ expect(avatarLabeled.attributes()).toMatchObject({
+ src: defaultPropsData.event.author.avatar_url,
size: '32',
});
+ expect(avatarLabeled.props()).toMatchObject({
+ label: defaultPropsData.event.author.name,
+ subLabel: `@${defaultPropsData.event.author.username}`,
+ });
});
it('renders time ago tooltip', () => {
- expect(wrapper.findComponent(TimeAgoTooltip).props('time')).toBe(event.created_at);
+ createComponent();
+
+ expect(wrapper.findComponent(TimeAgoTooltip).props('time')).toBe(
+ defaultPropsData.event.created_at,
+ );
});
it('renders icon', () => {
+ createComponent();
+
const icon = wrapper.findComponent(GlIcon);
expect(icon.props('name')).toBe(defaultPropsData.iconName);
expect(icon.classes()).toContain(defaultPropsData.iconClass);
});
- it('renders `default` slot', () => {
- expect(wrapper.findByTestId('default-slot').exists()).toBe(true);
+ describe('when `message` prop is passed', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders message', () => {
+ expect(wrapper.findByTestId('event-body').text()).toBe(
+ `Approved merge request ${defaultPropsData.event.target.reference_link_text} in ${defaultPropsData.event.resource_parent.full_name}.`,
+ );
+ });
+
+ it('renders target link', () => {
+ expect(wrapper.findComponent(TargetLink).props('event')).toEqual(defaultPropsData.event);
+ });
+
+ it('renders resource parent link', () => {
+ expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual(
+ defaultPropsData.event,
+ );
+ });
+ });
+
+ describe('when `message` prop is not passed', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { message: '' } });
+ });
+
+ it('renders `default` slot', () => {
+ expect(wrapper.findByTestId('default-slot').exists()).toBe(true);
+ });
});
it('renders `additional-info` slot', () => {
+ createComponent();
+
expect(wrapper.findByTestId('additional-info-slot').exists()).toBe(true);
});
});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js
new file mode 100644
index 00000000000..c58fca1ad12
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_expired_spec.js
@@ -0,0 +1,30 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventExpired from '~/contribution_events/components/contribution_event/contribution_event_expired.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import { eventExpired } from '../../utils';
+
+const defaultPropsData = {
+ event: eventExpired(),
+};
+
+describe('ContributionEventExpired', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ContributionEventExpired, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ContributionEventBase`', () => {
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event: defaultPropsData.event,
+ iconName: 'expire',
+ message: ContributionEventExpired.i18n.message,
+ });
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js
new file mode 100644
index 00000000000..56688e2ef27
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_joined_spec.js
@@ -0,0 +1,30 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventJoined from '~/contribution_events/components/contribution_event/contribution_event_joined.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import { eventJoined } from '../../utils';
+
+const defaultPropsData = {
+ event: eventJoined(),
+};
+
+describe('ContributionEventJoined', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ContributionEventJoined, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ContributionEventBase`', () => {
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event: defaultPropsData.event,
+ iconName: 'users',
+ message: ContributionEventJoined.i18n.message,
+ });
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js
new file mode 100644
index 00000000000..58cb8714d03
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_left_spec.js
@@ -0,0 +1,30 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventLeft from '~/contribution_events/components/contribution_event/contribution_event_left.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import { eventLeft } from '../../utils';
+
+const defaultPropsData = {
+ event: eventLeft(),
+};
+
+describe('ContributionEventLeft', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ContributionEventLeft, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ContributionEventBase`', () => {
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event: defaultPropsData.event,
+ iconName: 'leave',
+ message: ContributionEventLeft.i18n.message,
+ });
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_merged_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_merged_spec.js
new file mode 100644
index 00000000000..88494c24ddf
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_merged_spec.js
@@ -0,0 +1,31 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventMerged from '~/contribution_events/components/contribution_event/contribution_event_merged.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import { eventMerged } from '../../utils';
+
+const defaultPropsData = {
+ event: eventMerged(),
+};
+
+describe('ContributionEventMerged', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ContributionEventMerged, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ContributionEventBase`', () => {
+ expect(wrapper.findComponent(ContributionEventBase).props()).toEqual({
+ event: defaultPropsData.event,
+ iconName: 'git-merge',
+ iconClass: 'gl-text-blue-600',
+ message: ContributionEventMerged.i18n.message,
+ });
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_private_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_private_spec.js
new file mode 100644
index 00000000000..42855134a09
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_private_spec.js
@@ -0,0 +1,33 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventPrivate from '~/contribution_events/components/contribution_event/contribution_event_private.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import { eventPrivate } from '../../utils';
+
+const defaultPropsData = {
+ event: eventPrivate(),
+};
+
+describe('ContributionEventPrivate', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = mountExtended(ContributionEventPrivate, {
+ propsData: defaultPropsData,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ContributionEventBase`', () => {
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event: defaultPropsData.event,
+ iconName: 'eye-slash',
+ });
+ });
+
+ it('renders message', () => {
+ expect(wrapper.findByTestId('event-body').text()).toBe(ContributionEventPrivate.i18n.message);
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_event/contribution_event_pushed_spec.js b/spec/frontend/contribution_events/components/contribution_event/contribution_event_pushed_spec.js
new file mode 100644
index 00000000000..43f201040e3
--- /dev/null
+++ b/spec/frontend/contribution_events/components/contribution_event/contribution_event_pushed_spec.js
@@ -0,0 +1,141 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ContributionEventPushed from '~/contribution_events/components/contribution_event/contribution_event_pushed.vue';
+import ContributionEventBase from '~/contribution_events/components/contribution_event/contribution_event_base.vue';
+import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
+import {
+ eventPushedNewBranch,
+ eventPushedNewTag,
+ eventPushedBranch,
+ eventPushedTag,
+ eventPushedRemovedBranch,
+ eventPushedRemovedTag,
+ eventBulkPushedBranch,
+} from '../../utils';
+
+describe('ContributionEventPushed', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData }) => {
+ wrapper = mountExtended(ContributionEventPushed, {
+ propsData,
+ });
+ };
+
+ describe.each`
+ event | expectedMessage | expectedIcon
+ ${eventPushedNewBranch()} | ${'Pushed a new branch'} | ${'commit'}
+ ${eventPushedNewTag()} | ${'Pushed a new tag'} | ${'commit'}
+ ${eventPushedBranch()} | ${'Pushed to branch'} | ${'commit'}
+ ${eventPushedTag()} | ${'Pushed to tag'} | ${'commit'}
+ ${eventPushedRemovedBranch()} | ${'Deleted branch'} | ${'remove'}
+ ${eventPushedRemovedTag()} | ${'Deleted tag'} | ${'remove'}
+ `('when event is $event', ({ event, expectedMessage, expectedIcon }) => {
+ beforeEach(() => {
+ createComponent({ propsData: { event } });
+ });
+
+ it('renders `ContributionEventBase` with correct props', () => {
+ expect(wrapper.findComponent(ContributionEventBase).props()).toMatchObject({
+ event,
+ iconName: expectedIcon,
+ });
+ });
+
+ it('renders message', () => {
+ expect(wrapper.findByTestId('event-body').text()).toContain(expectedMessage);
+ });
+
+ it('renders resource parent link', () => {
+ expect(wrapper.findComponent(ResourceParentLink).props('event')).toEqual(event);
+ });
+ });
+
+ describe('when ref has a path', () => {
+ const event = eventPushedNewBranch();
+ const path = '/foo';
+
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ event: {
+ ...event,
+ ref: {
+ ...event.ref,
+ path,
+ },
+ },
+ },
+ });
+ });
+
+ it('renders ref link', () => {
+ expect(wrapper.findByRole('link', { name: event.ref.name }).attributes('href')).toBe(path);
+ });
+ });
+
+ describe('when ref does not have a path', () => {
+ const event = eventPushedRemovedBranch();
+
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ event,
+ },
+ });
+ });
+
+ it('renders ref name without a link', () => {
+ expect(wrapper.findByRole('link', { name: event.ref.name }).exists()).toBe(false);
+ expect(wrapper.findByText(event.ref.name).exists()).toBe(true);
+ });
+ });
+
+ it('renders renders a link to the commit', () => {
+ const event = eventPushedNewBranch();
+ createComponent({
+ propsData: {
+ event,
+ },
+ });
+
+ expect(
+ wrapper.findByRole('link', { name: event.commit.truncated_sha }).attributes('href'),
+ ).toBe(event.commit.path);
+ });
+
+ it('renders commit title', () => {
+ const event = eventPushedNewBranch();
+ createComponent({
+ propsData: {
+ event,
+ },
+ });
+
+ expect(wrapper.findByText(event.commit.title).exists()).toBe(true);
+ });
+
+ describe('when multiple commits are pushed', () => {
+ const event = eventBulkPushedBranch();
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ event,
+ },
+ });
+ });
+
+ it('renders message', () => {
+ expect(wrapper.text()).toContain('…and 4 more commits.');
+ });
+
+ it('renders compare link', () => {
+ expect(
+ wrapper
+ .findByRole('link', {
+ name: `Compare ${event.commit.from_truncated_sha}…${event.commit.to_truncated_sha}`,
+ })
+ .attributes('href'),
+ ).toBe(event.commit.compare_path);
+ });
+ });
+});
diff --git a/spec/frontend/contribution_events/components/contribution_events_spec.js b/spec/frontend/contribution_events/components/contribution_events_spec.js
index 4bc354c393f..31e1bc3e569 100644
--- a/spec/frontend/contribution_events/components/contribution_events_spec.js
+++ b/spec/frontend/contribution_events/components/contribution_events_spec.js
@@ -1,10 +1,21 @@
-import events from 'test_fixtures/controller/users/activity.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { EVENT_TYPE_APPROVED } from '~/contribution_events/constants';
import ContributionEvents from '~/contribution_events/components/contribution_events.vue';
import ContributionEventApproved from '~/contribution_events/components/contribution_event/contribution_event_approved.vue';
-
-const eventApproved = events.find((event) => event.action === EVENT_TYPE_APPROVED);
+import ContributionEventExpired from '~/contribution_events/components/contribution_event/contribution_event_expired.vue';
+import ContributionEventJoined from '~/contribution_events/components/contribution_event/contribution_event_joined.vue';
+import ContributionEventLeft from '~/contribution_events/components/contribution_event/contribution_event_left.vue';
+import ContributionEventPushed from '~/contribution_events/components/contribution_event/contribution_event_pushed.vue';
+import ContributionEventPrivate from '~/contribution_events/components/contribution_event/contribution_event_private.vue';
+import ContributionEventMerged from '~/contribution_events/components/contribution_event/contribution_event_merged.vue';
+import {
+ eventApproved,
+ eventExpired,
+ eventJoined,
+ eventLeft,
+ eventPushedBranch,
+ eventPrivate,
+ eventMerged,
+} from '../utils';
describe('ContributionEvents', () => {
let wrapper;
@@ -12,14 +23,28 @@ describe('ContributionEvents', () => {
const createComponent = () => {
wrapper = shallowMountExtended(ContributionEvents, {
propsData: {
- events,
+ events: [
+ eventApproved(),
+ eventExpired(),
+ eventJoined(),
+ eventLeft(),
+ eventPushedBranch(),
+ eventPrivate(),
+ eventMerged(),
+ ],
},
});
};
it.each`
expectedComponent | expectedEvent
- ${ContributionEventApproved} | ${eventApproved}
+ ${ContributionEventApproved} | ${eventApproved()}
+ ${ContributionEventExpired} | ${eventExpired()}
+ ${ContributionEventJoined} | ${eventJoined()}
+ ${ContributionEventLeft} | ${eventLeft()}
+ ${ContributionEventPushed} | ${eventPushedBranch()}
+ ${ContributionEventPrivate} | ${eventPrivate()}
+ ${ContributionEventMerged} | ${eventMerged()}
`(
'renders `$expectedComponent.name` component and passes expected event',
({ expectedComponent, expectedEvent }) => {
diff --git a/spec/frontend/contribution_events/components/resource_parent_link_spec.js b/spec/frontend/contribution_events/components/resource_parent_link_spec.js
index 8d586db2a30..815a1b751cf 100644
--- a/spec/frontend/contribution_events/components/resource_parent_link_spec.js
+++ b/spec/frontend/contribution_events/components/resource_parent_link_spec.js
@@ -1,30 +1,52 @@
import { GlLink } from '@gitlab/ui';
-import events from 'test_fixtures/controller/users/activity.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { EVENT_TYPE_APPROVED } from '~/contribution_events/constants';
import ResourceParentLink from '~/contribution_events/components/resource_parent_link.vue';
-
-const eventApproved = events.find((event) => event.action === EVENT_TYPE_APPROVED);
+import { EVENT_TYPE_PRIVATE } from '~/contribution_events/constants';
+import { eventApproved } from '../utils';
describe('ResourceParentLink', () => {
let wrapper;
- const createComponent = () => {
+ const defaultPropsData = {
+ event: eventApproved(),
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
wrapper = shallowMountExtended(ResourceParentLink, {
propsData: {
- event: eventApproved,
+ ...defaultPropsData,
+ ...propsData,
},
});
};
- beforeEach(() => {
- createComponent();
+ describe('when resource parent is defined', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders link', () => {
+ const link = wrapper.findComponent(GlLink);
+ const { web_url, full_name } = defaultPropsData.event.resource_parent;
+
+ expect(link.attributes('href')).toBe(web_url);
+ expect(link.text()).toBe(full_name);
+ });
});
- it('renders link', () => {
- const link = wrapper.findComponent(GlLink);
+ describe('when resource parent is not defined', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ event: {
+ type: EVENT_TYPE_PRIVATE,
+ },
+ },
+ });
+ });
- expect(link.attributes('href')).toBe(eventApproved.resource_parent.web_url);
- expect(link.text()).toBe(eventApproved.resource_parent.full_name);
+ it('renders nothing', () => {
+ expect(wrapper.html()).toBe('');
+ });
});
});
diff --git a/spec/frontend/contribution_events/components/target_link_spec.js b/spec/frontend/contribution_events/components/target_link_spec.js
index 7944375487b..b71d6eff432 100644
--- a/spec/frontend/contribution_events/components/target_link_spec.js
+++ b/spec/frontend/contribution_events/components/target_link_spec.js
@@ -1,33 +1,48 @@
import { GlLink } from '@gitlab/ui';
-import events from 'test_fixtures/controller/users/activity.json';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { EVENT_TYPE_APPROVED } from '~/contribution_events/constants';
import TargetLink from '~/contribution_events/components/target_link.vue';
-
-const eventApproved = events.find((event) => event.action === EVENT_TYPE_APPROVED);
+import { eventApproved, eventJoined } from '../utils';
describe('TargetLink', () => {
let wrapper;
- const createComponent = () => {
+ const defaultPropsData = {
+ event: eventApproved(),
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
wrapper = shallowMountExtended(TargetLink, {
propsData: {
- event: eventApproved,
+ ...defaultPropsData,
+ ...propsData,
},
});
};
- beforeEach(() => {
- createComponent();
+ describe('when target is defined', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders link', () => {
+ const link = wrapper.findComponent(GlLink);
+ const { web_url: webUrl, title, reference_link_text } = defaultPropsData.event.target;
+
+ expect(link.attributes()).toMatchObject({
+ href: webUrl,
+ title,
+ });
+ expect(link.text()).toBe(reference_link_text);
+ });
});
- it('renders link', () => {
- const link = wrapper.findComponent(GlLink);
+ describe('when target is not defined', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { event: eventJoined() } });
+ });
- expect(link.attributes()).toMatchObject({
- href: eventApproved.target.web_url,
- title: eventApproved.target.title,
+ it('renders nothing', () => {
+ expect(wrapper.html()).toBe('');
});
- expect(link.text()).toBe(eventApproved.target.reference_link_text);
});
});
diff --git a/spec/frontend/contribution_events/utils.js b/spec/frontend/contribution_events/utils.js
new file mode 100644
index 00000000000..6e97455582d
--- /dev/null
+++ b/spec/frontend/contribution_events/utils.js
@@ -0,0 +1,52 @@
+import events from 'test_fixtures/controller/users/activity.json';
+import {
+ EVENT_TYPE_APPROVED,
+ EVENT_TYPE_EXPIRED,
+ EVENT_TYPE_JOINED,
+ EVENT_TYPE_LEFT,
+ EVENT_TYPE_PUSHED,
+ EVENT_TYPE_PRIVATE,
+ EVENT_TYPE_MERGED,
+ PUSH_EVENT_REF_TYPE_BRANCH,
+ PUSH_EVENT_REF_TYPE_TAG,
+} from '~/contribution_events/constants';
+
+const findEventByAction = (action) => events.find((event) => event.action === action);
+
+export const eventApproved = () => findEventByAction(EVENT_TYPE_APPROVED);
+
+export const eventExpired = () => findEventByAction(EVENT_TYPE_EXPIRED);
+
+export const eventJoined = () => findEventByAction(EVENT_TYPE_JOINED);
+
+export const eventLeft = () => findEventByAction(EVENT_TYPE_LEFT);
+
+export const eventMerged = () => findEventByAction(EVENT_TYPE_MERGED);
+
+const findPushEvent = ({
+ isNew = false,
+ isRemoved = false,
+ refType = PUSH_EVENT_REF_TYPE_BRANCH,
+ commitCount = 1,
+} = {}) => () =>
+ events.find(
+ ({ action, ref, commit }) =>
+ action === EVENT_TYPE_PUSHED &&
+ ref.is_new === isNew &&
+ ref.is_removed === isRemoved &&
+ ref.type === refType &&
+ commit.count === commitCount,
+ );
+
+export const eventPushedNewBranch = findPushEvent({ isNew: true });
+export const eventPushedNewTag = findPushEvent({ isNew: true, refType: PUSH_EVENT_REF_TYPE_TAG });
+export const eventPushedBranch = findPushEvent();
+export const eventPushedTag = findPushEvent({ refType: PUSH_EVENT_REF_TYPE_TAG });
+export const eventPushedRemovedBranch = findPushEvent({ isRemoved: true });
+export const eventPushedRemovedTag = findPushEvent({
+ isRemoved: true,
+ refType: PUSH_EVENT_REF_TYPE_TAG,
+});
+export const eventBulkPushedBranch = findPushEvent({ commitCount: 5 });
+
+export const eventPrivate = () => ({ ...events[0], action: EVENT_TYPE_PRIVATE });
diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js
index 3dfb828b449..de4112134ce 100644
--- a/spec/frontend/deploy_keys/components/app_spec.js
+++ b/spec/frontend/deploy_keys/components/app_spec.js
@@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import deployKeysApp from '~/deploy_keys/components/app.vue';
import ConfirmModal from '~/deploy_keys/components/confirm_modal.vue';
+import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import eventHub from '~/deploy_keys/eventhub';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
@@ -39,6 +40,7 @@ describe('Deploy keys app component', () => {
const findLoadingIcon = () => wrapper.find('.gl-spinner');
const findKeyPanels = () => wrapper.findAll('.deploy-keys .gl-tabs-nav li');
const findModal = () => wrapper.findComponent(ConfirmModal);
+ const findNavigationTabs = () => wrapper.findComponent(NavigationTabs);
it('renders loading icon while waiting for request', async () => {
mock.onGet(TEST_ENDPOINT).reply(() => new Promise());
@@ -74,55 +76,61 @@ describe('Deploy keys app component', () => {
});
});
- it('re-fetches deploy keys when enabling a key', async () => {
- const key = data.public_keys[0];
+ it('hasKeys returns true when there are keys', async () => {
await mountComponent();
- jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'enableKey').mockImplementation(() => Promise.resolve());
- eventHub.$emit('enable.key', key);
-
- await nextTick();
- expect(wrapper.vm.service.enableKey).toHaveBeenCalledWith(key.id);
- expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
+ expect(findNavigationTabs().exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(false);
});
- it('re-fetches deploy keys when disabling a key', async () => {
+ describe('enabling and disabling keys', () => {
const key = data.public_keys[0];
- await mountComponent();
- jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
+ let getMethodMock;
+ let putMethodMock;
- eventHub.$emit('disable.key', key, () => {});
+ const removeKey = async (keyEvent) => {
+ eventHub.$emit(keyEvent, key, () => {});
- await nextTick();
- expect(findModal().props('visible')).toBe(true);
- findModal().vm.$emit('remove');
+ await nextTick();
+ expect(findModal().props('visible')).toBe(true);
+ findModal().vm.$emit('remove');
+ };
- await nextTick();
- expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id);
- expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
- });
+ beforeEach(() => {
+ getMethodMock = jest.spyOn(axios, 'get');
+ putMethodMock = jest.spyOn(axios, 'put');
+ });
- it('calls disableKey when removing a key', async () => {
- const key = data.public_keys[0];
- await mountComponent();
- jest.spyOn(wrapper.vm.service, 'getKeys').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'disableKey').mockImplementation(() => Promise.resolve());
+ afterEach(() => {
+ getMethodMock.mockClear();
+ putMethodMock.mockClear();
+ });
- eventHub.$emit('remove.key', key, () => {});
+ it('re-fetches deploy keys when enabling a key', async () => {
+ await mountComponent();
- await nextTick();
- expect(findModal().props('visible')).toBe(true);
- findModal().vm.$emit('remove');
+ eventHub.$emit('enable.key', key);
- await nextTick();
- expect(wrapper.vm.service.disableKey).toHaveBeenCalledWith(key.id);
- expect(wrapper.vm.service.getKeys).toHaveBeenCalled();
- });
+ expect(putMethodMock).toHaveBeenCalledWith(`${TEST_ENDPOINT}/${key.id}/enable`);
+ expect(getMethodMock).toHaveBeenCalled();
+ });
- it('hasKeys returns true when there are keys', async () => {
- await mountComponent();
- expect(wrapper.vm.hasKeys).toEqual(3);
+ it('re-fetches deploy keys when disabling a key', async () => {
+ await mountComponent();
+
+ await removeKey('disable.key');
+
+ expect(putMethodMock).toHaveBeenCalledWith(`${TEST_ENDPOINT}/${key.id}/disable`);
+ expect(getMethodMock).toHaveBeenCalled();
+ });
+
+ it('calls disableKey when removing a key', async () => {
+ await mountComponent();
+
+ await removeKey('remove.key');
+
+ expect(putMethodMock).toHaveBeenCalledWith(`${TEST_ENDPOINT}/${key.id}/disable`);
+ expect(getMethodMock).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/design_management/components/design_description/description_form_spec.js b/spec/frontend/design_management/components/design_description/description_form_spec.js
index 8c01023b1a8..a61cc2af9b6 100644
--- a/spec/frontend/design_management/components/design_description/description_form_spec.js
+++ b/spec/frontend/design_management/components/design_description/description_form_spec.js
@@ -1,18 +1,15 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-
import { GlAlert } from '@gitlab/ui';
-
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-
import DescriptionForm from '~/design_management/components/design_description/description_form.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import updateDesignDescriptionMutation from '~/design_management/graphql/mutations/update_design_description.mutation.graphql';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
-
+import { mockTracking } from 'helpers/tracking_helper';
import { designFactory, designUpdateFactory } from '../../mock_data/apollo_mock';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -86,6 +83,8 @@ describe('Design description form', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
describe('user has updateDesign permission', () => {
+ let trackingSpy;
+
const ctrlKey = {
ctrlKey: true,
};
@@ -96,6 +95,8 @@ describe('Design description form', () => {
const errorMessage = 'Could not update description. Please try again.';
beforeEach(() => {
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
+
createComponent();
});
@@ -139,19 +140,19 @@ describe('Design description form', () => {
mockDesign.id,
)}`,
markdownDocsPath: '/help/user/markdown',
- quickActionsDocsPath: '/help/user/project/quick_actions',
});
});
- it.each`
+ describe.each`
isKeyEvent | assertionName | key | keyData
${true} | ${'Ctrl + Enter keypress'} | ${'ctrl'} | ${ctrlKey}
${true} | ${'Meta + Enter keypress'} | ${'meta'} | ${metaKey}
${false} | ${'Save button click'} | ${''} | ${null}
- `(
- 'hides form and calls mutation when form is submitted via $assertionName',
- async ({ isKeyEvent, keyData }) => {
- const mockDesignUpdateResponseHandler = jest.fn().mockResolvedValue(
+ `('when form is submitted via $assertionName', ({ isKeyEvent, keyData }) => {
+ let mockDesignUpdateResponseHandler;
+
+ beforeEach(async () => {
+ mockDesignUpdateResponseHandler = jest.fn().mockResolvedValue(
designUpdateFactory({
description: mockDescription,
descriptionHtml: `<p data-sourcepos="1:1-1:16" dir="auto">${mockDescription}</p>`,
@@ -171,7 +172,9 @@ describe('Design description form', () => {
}
await nextTick();
+ });
+ it('hides form and calls mutation', async () => {
expect(mockDesignUpdateResponseHandler).toHaveBeenCalledWith({
input: {
description: 'Hello world',
@@ -182,8 +185,16 @@ describe('Design description form', () => {
await waitForPromises();
expect(findMarkdownEditor().exists()).toBe(false);
- },
- );
+ });
+
+ it('tracks submit action', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context: 'Design',
+ editorType: 'editor_type_plain_text_editor',
+ label: 'editor_tracking',
+ });
+ });
+ });
it('shows error message when mutation fails', async () => {
const failureHandler = jest.fn().mockRejectedValue(new Error(errorMessage));
diff --git a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
deleted file mode 100644
index 9bb85ecf569..00000000000
--- a/spec/frontend/design_management/components/design_notes/__snapshots__/design_note_spec.js.snap
+++ /dev/null
@@ -1,86 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design note component should match the snapshot 1`] = `
-<timelineentryitem-stub
- class="design-note note-form"
- id="note_123"
->
- <glavatarlink-stub
- class="gl-float-left gl-mr-3"
- href="https://gitlab.com/user"
- >
- <glavatar-stub
- alt="avatar"
- entityid="0"
- entityname="foo-bar"
- shape="circle"
- size="32"
- src="https://gitlab.com/avatar"
- />
- </glavatarlink-stub>
-
- <div
- class="gl-display-flex gl-justify-content-space-between"
- >
- <div>
- <gllink-stub
- class="js-user-link"
- data-testid="user-link"
- data-user-id="1"
- data-username="foo-bar"
- href="https://gitlab.com/user"
- >
- <span
- class="note-header-author-name gl-font-weight-bold"
- >
-
- </span>
-
- <!---->
-
- <span
- class="note-headline-light"
- >
- @foo-bar
- </span>
- </gllink-stub>
-
- <span
- class="note-headline-light note-headline-meta"
- >
- <span
- class="system-note-message"
- />
-
- <gllink-stub
- class="note-timestamp system-note-separator gl-display-block gl-mb-2 gl-font-sm"
- href="#note_123"
- >
- <timeagotooltip-stub
- cssclass=""
- datetimeformat="DATE_WITH_TIME_FORMAT"
- time="2019-07-26T15:02:20Z"
- tooltipplacement="bottom"
- />
- </gllink-stub>
- </span>
- </div>
-
- <div
- class="gl-display-flex gl-align-items-baseline gl-mt-n2 gl-mr-n2"
- >
-
- <!---->
-
- <!---->
- </div>
- </div>
-
- <div
- class="note-text md"
- data-qa-selector="note_content"
- data-testid="note-text"
- />
-
-</timelineentryitem-stub>
-`;
diff --git a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
index 664a0974549..797f399eff5 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -29,6 +29,7 @@ const DEFAULT_TODO_COUNT = 2;
describe('Design discussions component', () => {
let wrapper;
+ const findDesignNotesList = () => wrapper.find('[data-testid="design-discussion-content"]');
const findDesignNotes = () => wrapper.findAllComponents(DesignNote);
const findReplyPlaceholder = () => wrapper.findComponent(ReplyPlaceholder);
const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
@@ -88,6 +89,9 @@ describe('Design discussions component', () => {
},
},
},
+ stubs: {
+ EmojiPicker: true,
+ },
});
}
@@ -287,7 +291,7 @@ describe('Design discussions component', () => {
describe('when any note from a discussion is active', () => {
it.each([notes[0], notes[0].discussion.notes.nodes[1]])(
- 'applies correct class to all notes in the active discussion',
+ 'applies correct class to the active discussion',
(note) => {
createComponent({
props: { discussion: mockDiscussion },
@@ -299,11 +303,7 @@ describe('Design discussions component', () => {
},
});
- expect(
- wrapper
- .findAllComponents(DesignNote)
- .wrappers.every((designNote) => designNote.classes('gl-bg-blue-50')),
- ).toBe(true);
+ expect(findDesignNotesList().classes('gl-bg-blue-50')).toBe(true);
},
);
});
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 661d1ac4087..8795b089551 100644
--- a/spec/frontend/design_management/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_note_spec.js
@@ -1,10 +1,18 @@
import { ApolloMutation } from 'vue-apollo';
import { nextTick } from 'vue';
import { GlAvatar, GlAvatarLink, GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import * as Sentry from '@sentry/browser';
+
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import EmojiPicker from '~/emoji/components/picker.vue';
+import DesignNoteAwardsList from '~/design_management/components/design_notes/design_note_awards_list.vue';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import designNoteAwardEmojiToggleMutation from '~/design_management/graphql/mutations/design_note_award_emoji_toggle.mutation.graphql';
+import { mockAwardEmoji } from '../../mock_data/apollo_mock';
const scrollIntoViewMock = jest.fn();
const note = {
@@ -15,9 +23,11 @@ const note = {
avatarUrl: 'https://gitlab.com/avatar',
webUrl: 'https://gitlab.com/user',
},
+ awardEmoji: mockAwardEmoji,
body: 'test',
userPermissions: {
adminNote: false,
+ awardEmoji: true,
},
createdAt: '2019-07-26T15:02:20Z',
};
@@ -27,14 +37,14 @@ const $route = {
hash: '#note_123',
};
-const mutate = jest.fn().mockResolvedValue({ data: { updateNote: {} } });
-
describe('Design note component', () => {
let wrapper;
+ let mutate;
const findUserAvatar = () => wrapper.findComponent(GlAvatar);
const findUserAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findUserLink = () => wrapper.findByTestId('user-link');
+ const findDesignNoteAwardsList = () => wrapper.findComponent(DesignNoteAwardsList);
const findReplyForm = () => wrapper.findComponent(DesignReplyForm);
const findEditButton = () => wrapper.findByTestId('note-edit');
const findNoteContent = () => wrapper.findByTestId('note-text');
@@ -43,97 +53,106 @@ describe('Design note component', () => {
const findEditDropdownItem = () => findDropdownItems().at(0);
const findDeleteDropdownItem = () => findDropdownItems().at(1);
- function createComponent(props = {}, data = { isEditing: false }) {
- wrapper = mountExtended(DesignNote, {
+ function createComponent({
+ props = {},
+ data = { isEditing: false },
+ mountFn = mountExtended,
+ mocks = {
+ $route,
+ $apollo: {
+ mutate: jest.fn().mockResolvedValue({ data: { updateNote: {} } }),
+ },
+ },
+ stubs = {
+ ApolloMutation,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ TimelineEntryItem: true,
+ TimeAgoTooltip: true,
+ GlAvatarLink: true,
+ GlAvatar: true,
+ GlLink: true,
+ },
+ } = {}) {
+ wrapper = mountFn(DesignNote, {
propsData: {
note: {},
noteableId: 'gid://gitlab/DesignManagement::Design/6',
+ designVariables: {
+ atVersion: null,
+ filenames: ['foo.jpg'],
+ fullPath: 'gitlab-org/gitlab-test',
+ iid: '1',
+ },
...props,
},
+ provide: {
+ issueIid: '1',
+ projectPath: 'gitlab-org/gitlab-test',
+ },
data() {
return {
...data,
};
},
- mocks: {
- $route,
- $apollo: {
- mutate,
- },
- },
- stubs: {
- ApolloMutation,
- GlDisclosureDropdown,
- GlDisclosureDropdownItem,
- TimelineEntryItem: true,
- TimeAgoTooltip: true,
- GlAvatarLink: true,
- GlAvatar: true,
- GlLink: true,
- },
+ mocks,
+ stubs,
});
}
- it('should match the snapshot', () => {
- createComponent({
- note,
- });
-
- expect(wrapper.element).toMatchSnapshot();
+ beforeEach(() => {
+ window.gon = { current_user_id: 1 };
});
- it('should render avatar with correct props', () => {
- createComponent({
- note,
- });
-
- expect(findUserAvatar().props()).toMatchObject({
- src: note.author.avatarUrl,
- entityName: note.author.username,
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent({ props: { note } });
});
- expect(findUserAvatarLink().attributes('href')).toBe(note.author.webUrl);
- });
+ it('should render avatar with correct props', () => {
+ expect(findUserAvatar().props()).toMatchObject({
+ src: note.author.avatarUrl,
+ entityName: note.author.username,
+ });
- it('should render author details', () => {
- createComponent({
- note,
+ expect(findUserAvatarLink().attributes()).toMatchObject({
+ href: note.author.webUrl,
+ 'data-user-id': '1',
+ 'data-username': `${note.author.username}`,
+ });
});
- expect(findUserLink().exists()).toBe(true);
- });
-
- it('should render a time ago tooltip if note has createdAt property', () => {
- createComponent({
- note,
+ it('should render author details', () => {
+ expect(findUserLink().exists()).toBe(true);
});
- expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true);
- });
-
- it('should not render edit icon when user does not have a permission', () => {
- createComponent({
- note,
+ it('should render a time ago tooltip if note has createdAt property', () => {
+ expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true);
});
- expect(findEditButton().exists()).toBe(false);
- });
+ it('should render emoji awards list', () => {
+ expect(findDesignNoteAwardsList().exists()).toBe(true);
+ });
- it('should not display a dropdown if user does not have a permission to delete note', () => {
- createComponent({
- note,
+ it('should not render edit icon when user does not have a permission', () => {
+ expect(findEditButton().exists()).toBe(false);
});
- expect(findDropdown().exists()).toBe(false);
+ it('should not display a dropdown if user does not have a permission to delete note', () => {
+ expect(findDropdown().exists()).toBe(false);
+ });
});
describe('when user has a permission to edit note', () => {
it('should open an edit form on edit button click', async () => {
createComponent({
- note: {
- ...note,
- userPermissions: {
- adminNote: true,
+ props: {
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ },
},
},
});
@@ -147,25 +166,29 @@ describe('Design note component', () => {
describe('when edit form is rendered', () => {
beforeEach(() => {
- createComponent(
- {
+ createComponent({
+ props: {
note: {
...note,
userPermissions: {
adminNote: true,
+ awardEmoji: true,
},
},
},
- { isEditing: true },
- );
+ data: { isEditing: true },
+ });
});
it('should open an edit form on edit button click', async () => {
createComponent({
- note: {
- ...note,
- userPermissions: {
- adminNote: true,
+ props: {
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ },
},
},
});
@@ -203,10 +226,13 @@ describe('Design note component', () => {
describe('when user has admin permissions', () => {
it('should display a dropdown', () => {
createComponent({
- note: {
- ...note,
- userPermissions: {
- adminNote: true,
+ props: {
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ },
},
},
});
@@ -223,12 +249,15 @@ describe('Design note component', () => {
...note,
userPermissions: {
adminNote: true,
+ awardEmoji: true,
},
};
createComponent({
- note: {
- ...payload,
+ props: {
+ note: {
+ ...payload,
+ },
},
});
@@ -236,4 +265,91 @@ describe('Design note component', () => {
expect(wrapper.emitted()).toEqual({ 'delete-note': [[{ ...payload }]] });
});
+
+ describe('when user has award emoji permissions', () => {
+ const findEmojiPicker = () => wrapper.findComponent(EmojiPicker);
+ const propsData = {
+ note: {
+ ...note,
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ },
+ },
+ };
+
+ it('should render emoji-picker button', () => {
+ createComponent({ props: propsData, mountFn: shallowMountExtended });
+
+ const emojiPicker = findEmojiPicker();
+
+ expect(emojiPicker.exists()).toBe(true);
+ expect(emojiPicker.props()).toMatchObject({
+ boundary: 'viewport',
+ right: false,
+ });
+ });
+
+ it('should call mutation to add an emoji', () => {
+ mutate = jest.fn().mockResolvedValue({
+ data: {
+ awardEmojiToggle: {
+ errors: [],
+ toggledOn: true,
+ },
+ },
+ });
+ createComponent({
+ props: propsData,
+ mountFn: shallowMountExtended,
+ mocks: {
+ $route,
+ $apollo: {
+ mutate,
+ },
+ },
+ });
+
+ findEmojiPicker().vm.$emit('click', 'thumbsup');
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: designNoteAwardEmojiToggleMutation,
+ variables: {
+ name: 'thumbsup',
+ awardableId: note.id,
+ },
+ optimisticResponse: {
+ awardEmojiToggle: {
+ errors: [],
+ toggledOn: true,
+ },
+ },
+ update: expect.any(Function),
+ });
+ });
+
+ it('should emit an error when mutation fails', async () => {
+ jest.spyOn(Sentry, 'captureException');
+ mutate = jest.fn().mockRejectedValue({});
+ createComponent({
+ props: propsData,
+ mountFn: shallowMountExtended,
+ mocks: {
+ $route,
+ $apollo: {
+ mutate,
+ },
+ },
+ });
+
+ findEmojiPicker().vm.$emit('click', 'thumbsup');
+
+ expect(mutate).toHaveBeenCalled();
+
+ await waitForPromises();
+
+ expect(Sentry.captureException).toHaveBeenCalled();
+ expect(wrapper.emitted('error')).toEqual([[{}]]);
+ });
+ });
});
diff --git a/spec/frontend/design_management/components/design_presentation_spec.js b/spec/frontend/design_management/components/design_presentation_spec.js
index fdcea6d88c0..e64dec14461 100644
--- a/spec/frontend/design_management/components/design_presentation_spec.js
+++ b/spec/frontend/design_management/components/design_presentation_spec.js
@@ -220,10 +220,6 @@ describe('Design management design presentation component', () => {
);
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it('sets overlay position correctly when overlay is smaller than viewport', () => {
jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetWidth', 'get').mockReturnValue(200);
jest.spyOn(wrapper.vm.$refs.presentationViewport, 'offsetHeight', 'get').mockReturnValue(200);
diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js
index 698535d8937..2262e5fdd83 100644
--- a/spec/frontend/design_management/components/design_todo_button_spec.js
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -50,10 +50,6 @@ describe('Design management design todo button', () => {
createComponent();
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it('renders TodoButton component', () => {
expect(wrapper.findComponent(TodoButton).exists()).toBe(true);
});
diff --git a/spec/frontend/design_management/mock_data/apollo_mock.js b/spec/frontend/design_management/mock_data/apollo_mock.js
index 063df9366e9..0d004baafd0 100644
--- a/spec/frontend/design_management/mock_data/apollo_mock.js
+++ b/spec/frontend/design_management/mock_data/apollo_mock.js
@@ -1,3 +1,27 @@
+export const mockAuthor = {
+ id: 'gid://gitlab/User/1',
+ name: 'John',
+ webUrl: 'link-to-john-profile',
+ avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ username: 'john.doe',
+};
+
+export const mockAwardEmoji = {
+ __typename: 'AwardEmojiConnection',
+ nodes: [
+ {
+ __typename: 'AwardEmoji',
+ name: 'briefcase',
+ user: mockAuthor,
+ },
+ {
+ __typename: 'AwardEmoji',
+ name: 'baseball',
+ user: mockAuthor,
+ },
+ ],
+};
+
export const designListQueryResponseNodes = [
{
__typename: 'Design',
@@ -237,6 +261,9 @@ export const mockNoteSubmitSuccessMutationResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
body: 'New comment',
bodyHtml: "<p data-sourcepos='1:1-1:4' dir='auto'>asdd</p>",
createdAt: '2023-02-24T06:49:20Z',
@@ -257,6 +284,7 @@ export const mockNoteSubmitSuccessMutationResponse = {
userPermissions: {
adminNote: true,
repositionNote: true,
+ awardEmoji: true,
__typename: 'NotePermissions',
},
discussion: {
@@ -363,6 +391,7 @@ export const designFactory = ({
},
userPermissions: {
updateDesign,
+ awardEmoji: true,
__typename: 'IssuePermissions',
},
__typename: 'Issue',
diff --git a/spec/frontend/design_management/mock_data/discussion.js b/spec/frontend/design_management/mock_data/discussion.js
index 0e59ef29f8f..fbd5a9e0103 100644
--- a/spec/frontend/design_management/mock_data/discussion.js
+++ b/spec/frontend/design_management/mock_data/discussion.js
@@ -1,3 +1,5 @@
+import { mockAuthor, mockAwardEmoji } from './apollo_mock';
+
export default {
id: 'discussion-id-1',
resolved: false,
@@ -12,13 +14,12 @@ export default {
x: 10,
y: 15,
},
- author: {
- name: 'John',
- webUrl: 'link-to-john-profile',
- },
+ author: mockAuthor,
+ awardEmoji: mockAwardEmoji,
createdAt: '2020-05-08T07:10:45Z',
userPermissions: {
repositionNote: true,
+ awardEmoji: true,
},
resolved: false,
},
@@ -32,12 +33,15 @@ export default {
y: 25,
},
author: {
+ id: 'gid://gitlab/User/2',
name: 'Mary',
webUrl: 'link-to-mary-profile',
},
+ awardEmoji: mockAwardEmoji,
createdAt: '2020-05-08T07:10:45Z',
userPermissions: {
adminNote: true,
+ awardEmoji: true,
},
resolved: false,
},
diff --git a/spec/frontend/design_management/mock_data/notes.js b/spec/frontend/design_management/mock_data/notes.js
index 41cefaca05b..311ce4d1eb9 100644
--- a/spec/frontend/design_management/mock_data/notes.js
+++ b/spec/frontend/design_management/mock_data/notes.js
@@ -1,3 +1,4 @@
+import { mockAwardEmoji } from './apollo_mock';
import DISCUSSION_1 from './discussion';
const DISCUSSION_2 = {
@@ -17,9 +18,11 @@ const DISCUSSION_2 = {
name: 'Mary',
webUrl: 'link-to-mary-profile',
},
+ awardEmoji: mockAwardEmoji,
createdAt: '2020-05-08T07:10:45Z',
userPermissions: {
adminNote: true,
+ awardEmoji: true,
},
resolved: true,
},
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index b69452069c0..fb5cf4dfd0a 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -73,6 +73,8 @@ describe('diffs/components/app', () => {
propsData: {
endpointCoverage: `${TEST_HOST}/diff/endpointCoverage`,
endpointCodequality: '',
+ endpointSast: '',
+ projectPath: 'namespace/project',
currentUser: {},
changesEmptyStateIllustration: '',
...props,
@@ -184,6 +186,16 @@ describe('diffs/components/app', () => {
});
});
+ describe('SAST diff', () => {
+ it('does not fetch Sast data on FOSS', () => {
+ createComponent();
+ jest.spyOn(wrapper.vm, 'fetchSast');
+ wrapper.vm.fetchData(false);
+
+ expect(wrapper.vm.fetchSast).not.toHaveBeenCalled();
+ });
+ });
+
it('displays loading icon on loading', () => {
createComponent({}, ({ state }) => {
state.diffs.isLoading = true;
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index 3c092296130..fa16af92701 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -5,7 +5,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import { trimText } from 'helpers/text_helper';
import Component from '~/diffs/components/commit_item.vue';
import { getTimeago } from '~/lib/utils/datetime_utility';
-import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
+import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status.vue';
const TEST_AUTHOR_NAME = 'test';
const TEST_AUTHOR_EMAIL = 'test+test@gitlab.com';
diff --git a/spec/frontend/diffs/components/diff_code_quality_item_spec.js b/spec/frontend/diffs/components/diff_code_quality_item_spec.js
index be9fb61a77d..085eb096239 100644
--- a/spec/frontend/diffs/components/diff_code_quality_item_spec.js
+++ b/spec/frontend/diffs/components/diff_code_quality_item_spec.js
@@ -2,20 +2,22 @@ import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
import { SEVERITY_CLASSES, SEVERITY_ICONS } from '~/ci/reports/codequality_report/constants';
-import { multipleFindingsArr } from '../mock_data/diff_code_quality';
+import { multipleFindingsArrCodeQualityScale } from '../mock_data/diff_code_quality';
let wrapper;
+const [codeQualityFinding] = multipleFindingsArrCodeQualityScale;
const findIcon = () => wrapper.findComponent(GlIcon);
const findButton = () => wrapper.findComponent(GlLink);
const findDescriptionPlainText = () => wrapper.findByTestId('description-plain-text');
const findDescriptionLinkSection = () => wrapper.findByTestId('description-button-section');
describe('DiffCodeQuality', () => {
- const createWrapper = ({ glFeatures = {} } = {}) => {
+ const createWrapper = ({ glFeatures = {}, link = true } = {}) => {
return shallowMountExtended(DiffCodeQualityItem, {
propsData: {
- finding: multipleFindingsArr[0],
+ finding: codeQualityFinding,
+ link,
},
provide: {
glFeatures,
@@ -28,8 +30,8 @@ describe('DiffCodeQuality', () => {
expect(findIcon().exists()).toBe(true);
expect(findIcon().attributes()).toMatchObject({
- class: `codequality-severity-icon ${SEVERITY_CLASSES[multipleFindingsArr[0].severity]}`,
- name: SEVERITY_ICONS[multipleFindingsArr[0].severity],
+ class: `codequality-severity-icon ${SEVERITY_CLASSES[codeQualityFinding.severity]}`,
+ name: SEVERITY_ICONS[codeQualityFinding.severity],
size: '12',
});
});
@@ -41,26 +43,35 @@ describe('DiffCodeQuality', () => {
codeQualityInlineDrawer: false,
},
});
- expect(findDescriptionPlainText().text()).toContain(multipleFindingsArr[0].severity);
- expect(findDescriptionPlainText().text()).toContain(multipleFindingsArr[0].description);
+ expect(findDescriptionPlainText().text()).toContain(codeQualityFinding.severity);
+ expect(findDescriptionPlainText().text()).toContain(codeQualityFinding.description);
});
});
describe('with codeQualityInlineDrawer flag true', () => {
- beforeEach(() => {
+ const [{ description, severity }] = multipleFindingsArrCodeQualityScale;
+ const renderedText = `${severity} - ${description}`;
+ it('when link prop is true, should render gl-link', () => {
wrapper = createWrapper({
glFeatures: {
codeQualityInlineDrawer: true,
},
});
- });
- it('should render severity as plain text', () => {
- expect(findDescriptionLinkSection().text()).toContain(multipleFindingsArr[0].severity);
+ expect(findButton().exists()).toBe(true);
+ expect(findButton().text()).toBe(renderedText);
});
- it('should render button with description text', () => {
- expect(findButton().text()).toContain(multipleFindingsArr[0].description);
+ it('when link prop is false, should not render gl-link', () => {
+ wrapper = createWrapper({
+ glFeatures: {
+ codeQualityInlineDrawer: true,
+ },
+ link: false,
+ });
+
+ expect(findButton().exists()).toBe(false);
+ expect(findDescriptionLinkSection().text()).toBe(renderedText);
});
});
});
diff --git a/spec/frontend/diffs/components/diff_code_quality_spec.js b/spec/frontend/diffs/components/diff_code_quality_spec.js
index 9ecfb62e1c5..73976ebd713 100644
--- a/spec/frontend/diffs/components/diff_code_quality_spec.js
+++ b/spec/frontend/diffs/components/diff_code_quality_spec.js
@@ -1,38 +1,61 @@
-import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import DiffCodeQuality from '~/diffs/components/diff_code_quality.vue';
-import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
-import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
-import { multipleFindingsArr } from '../mock_data/diff_code_quality';
+import DiffInlineFindings from '~/diffs/components/diff_inline_findings.vue';
+import { NEW_CODE_QUALITY_FINDINGS, NEW_SAST_FINDINGS } from '~/diffs/i18n';
+import {
+ multipleCodeQualityNoSast,
+ multipleSastNoCodeQuality,
+} from '../mock_data/diff_code_quality';
let wrapper;
-const diffItems = () => wrapper.findAllComponents(DiffCodeQualityItem);
-const findHeading = () => wrapper.findByTestId(`diff-codequality-findings-heading`);
+const diffInlineFindings = () => wrapper.findComponent(DiffInlineFindings);
+const allDiffInlineFindings = () => wrapper.findAllComponents(DiffInlineFindings);
describe('DiffCodeQuality', () => {
- const createWrapper = (codeQuality, mountFunction = mountExtended) => {
- return mountFunction(DiffCodeQuality, {
+ const createWrapper = (findings) => {
+ return mountExtended(DiffCodeQuality, {
propsData: {
expandedLines: [],
- codeQuality,
+ codeQuality: findings.codeQuality,
+ sast: findings.sast,
},
});
};
it('hides details and throws hideCodeQualityFindings event on close click', async () => {
- wrapper = createWrapper(multipleFindingsArr);
+ wrapper = createWrapper(multipleCodeQualityNoSast);
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')).toHaveLength(1);
});
- it('renders heading and correct amount of list items for codequality array and their description', () => {
- wrapper = createWrapper(multipleFindingsArr, shallowMountExtended);
+ it('renders diff inline findings component with correct props for codequality array', () => {
+ wrapper = createWrapper(multipleCodeQualityNoSast);
- expect(findHeading().text()).toEqual(NEW_CODE_QUALITY_FINDINGS);
+ expect(diffInlineFindings().props('title')).toBe(NEW_CODE_QUALITY_FINDINGS);
+ expect(diffInlineFindings().props('findings')).toBe(multipleCodeQualityNoSast.codeQuality);
+ });
+
+ it('does not render codeQuality section when codeQuality array is empty', () => {
+ wrapper = createWrapper(multipleSastNoCodeQuality);
+
+ expect(diffInlineFindings().props('title')).toBe(NEW_SAST_FINDINGS);
+ expect(allDiffInlineFindings()).toHaveLength(1);
+ });
+
+ it('renders heading and correct amount of list items for sast array and their description', () => {
+ wrapper = createWrapper(multipleSastNoCodeQuality);
+
+ expect(diffInlineFindings().props('title')).toBe(NEW_SAST_FINDINGS);
+ expect(diffInlineFindings().props('findings')).toBe(multipleSastNoCodeQuality.sast);
+ });
+
+ it('does not render sast section when sast array is empty', () => {
+ wrapper = createWrapper(multipleCodeQualityNoSast);
- expect(diffItems()).toHaveLength(multipleFindingsArr.length);
- expect(diffItems().at(0).props().finding).toEqual(multipleFindingsArr[0]);
+ expect(diffInlineFindings().props('title')).toBe(NEW_CODE_QUALITY_FINDINGS);
+ expect(allDiffInlineFindings()).toHaveLength(1);
});
});
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index 39d9255aaf9..3b37edbcb1d 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -2,6 +2,10 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
+import { sprintf } from '~/locale';
+import { createAlert } from '~/alert';
+import * as diffRowUtils from 'ee_else_ce/diffs/components/diff_row_utils';
import DiffContentComponent from '~/diffs/components/diff_content.vue';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import DiffView from '~/diffs/components/diff_view.vue';
@@ -10,9 +14,11 @@ import { diffViewerModes } from '~/ide/constants';
import NoteForm from '~/notes/components/note_form.vue';
import NoPreviewViewer from '~/vue_shared/components/diff_viewer/viewers/no_preview.vue';
import NotDiffableViewer from '~/vue_shared/components/diff_viewer/viewers/not_diffable.vue';
+import { SOMETHING_WENT_WRONG, SAVING_THE_COMMENT_FAILED } from '~/diffs/i18n';
import { getDiffFileMock } from '../mock_data/diff_file';
Vue.use(Vuex);
+jest.mock('~/alert');
describe('DiffContent', () => {
let wrapper;
@@ -72,6 +78,7 @@ describe('DiffContent', () => {
getCommentFormForDiffFile: getCommentFormForDiffFileGetterMock,
diffLines: () => () => [...getDiffFileMock().parallel_diff_lines],
fileLineCodequality: () => () => [],
+ fileLineSast: () => () => [],
},
actions: {
saveDiffDiscussion: saveDiffDiscussionMock,
@@ -113,6 +120,32 @@ describe('DiffContent', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
+
+ it('should include Sast findings when sastReportsInInlineDiff flag is true', () => {
+ const mapParallelSpy = jest.spyOn(diffRowUtils, 'mapParallel');
+ const mapParallelNoSastSpy = jest.spyOn(diffRowUtils, 'mapParallelNoSast');
+ createComponent({
+ provide: {
+ glFeatures: {
+ sastReportsInInlineDiff: true,
+ },
+ },
+ props: { diffFile: { ...textDiffFile, renderingLines: true } },
+ });
+
+ expect(mapParallelSpy).toHaveBeenCalled();
+ expect(mapParallelNoSastSpy).not.toHaveBeenCalled();
+ });
+
+ it('should not include Sast findings when sastReportsInInlineDiff flag is false', () => {
+ const mapParallelSpy = jest.spyOn(diffRowUtils, 'mapParallel');
+ const mapParallelNoSastSpy = jest.spyOn(diffRowUtils, 'mapParallelNoSast');
+
+ createComponent({ props: { diffFile: { ...textDiffFile, renderingLines: true } } });
+
+ expect(mapParallelNoSastSpy).toHaveBeenCalled();
+ expect(mapParallelSpy).not.toHaveBeenCalled();
+ });
});
describe('with whitespace only change', () => {
@@ -218,5 +251,44 @@ describe('DiffContent', () => {
},
});
});
+
+ describe('when note-form emits `handleFormUpdate`', () => {
+ const noteStub = {};
+ const parentElement = null;
+ const errorCallback = jest.fn();
+
+ describe.each`
+ scenario | serverError | message
+ ${'with server error'} | ${{ data: { errors: 'error' } }} | ${SAVING_THE_COMMENT_FAILED}
+ ${'without server error'} | ${null} | ${SOMETHING_WENT_WRONG}
+ `('$scenario', ({ serverError, message }) => {
+ beforeEach(async () => {
+ saveDiffDiscussionMock.mockRejectedValue({ response: serverError });
+
+ createComponent({
+ props: {
+ diffFile: imageDiffFile,
+ },
+ });
+
+ wrapper
+ .findComponent(NoteForm)
+ .vm.$emit('handleFormUpdate', noteStub, parentElement, errorCallback);
+
+ await waitForPromises();
+ });
+
+ it(`renders ${serverError ? 'server' : 'generic'} error message`, () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: sprintf(message, { reason: serverError?.data?.errors }),
+ parent: parentElement,
+ });
+ });
+
+ it('calls errorCallback', () => {
+ expect(errorCallback).toHaveBeenCalled();
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/diffs/components/diff_discussions_spec.js b/spec/frontend/diffs/components/diff_discussions_spec.js
index 73d9f2d6d45..40c617da0aa 100644
--- a/spec/frontend/diffs/components/diff_discussions_spec.js
+++ b/spec/frontend/diffs/components/diff_discussions_spec.js
@@ -25,11 +25,13 @@ describe('DiffDiscussions', () => {
});
};
+ const findNoteableDiscussion = () => wrapper.findComponent(NoteableDiscussion);
+
describe('template', () => {
it('should have notes list', () => {
createComponent();
- expect(wrapper.findComponent(NoteableDiscussion).exists()).toBe(true);
+ expect(findNoteableDiscussion().exists()).toBe(true);
expect(wrapper.findComponent(DiscussionNotes).exists()).toBe(true);
expect(
wrapper.findComponent(DiscussionNotes).findAllComponents(TimelineEntryItem).length,
@@ -51,11 +53,11 @@ describe('DiffDiscussions', () => {
it('dispatches toggleDiscussion when clicking collapse button', () => {
createComponent({ shouldCollapseDiscussions: true });
- jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation();
- const diffNotesToggle = findDiffNotesToggle();
- diffNotesToggle.trigger('click');
+ jest.spyOn(store, 'dispatch').mockImplementation();
+
+ findDiffNotesToggle().trigger('click');
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
+ expect(store.dispatch).toHaveBeenCalledWith('toggleDiscussion', {
discussionId: discussionsMockData.id,
});
});
@@ -77,12 +79,12 @@ describe('DiffDiscussions', () => {
discussions[0].expanded = false;
createComponent({ discussions, shouldCollapseDiscussions: true });
- expect(wrapper.findComponent(NoteableDiscussion).isVisible()).toBe(false);
+ expect(findNoteableDiscussion().isVisible()).toBe(false);
});
it('renders badge on avatar', () => {
createComponent({ renderAvatarBadge: true });
- const noteableDiscussion = wrapper.findComponent(NoteableDiscussion);
+ const noteableDiscussion = findNoteableDiscussion();
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 3f75b086368..d3afaab492d 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -644,22 +644,14 @@ describe('DiffFileHeader component', () => {
);
});
- it.each`
- commentOnFiles | exists | existsText
- ${false} | ${false} | ${'does not'}
- ${true} | ${true} | ${'does'}
- `(
- '$existsText render comment on files button when commentOnFiles is $commentOnFiles',
- ({ commentOnFiles, exists }) => {
- window.gon = { current_user_id: 1 };
- createComponent({
- props: {
- addMergeRequestButtons: true,
- },
- options: { provide: { glFeatures: { commentOnFiles } } },
- });
+ it('should render the comment on files button', () => {
+ window.gon = { current_user_id: 1 };
+ createComponent({
+ props: {
+ addMergeRequestButtons: true,
+ },
+ });
- expect(wrapper.find('[data-testid="comment-files-button"]').exists()).toEqual(exists);
- },
- );
+ expect(wrapper.find('[data-testid="comment-files-button"]').exists()).toEqual(true);
+ });
});
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index d9c57ed1470..db6cde883f3 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -1,7 +1,11 @@
-import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import waitForPromises from 'helpers/wait_for_promises';
+import { sprintf } from '~/locale';
+import { createAlert } from '~/alert';
import DiffContentComponent from 'jh_else_ce/diffs/components/diff_content.vue';
import DiffFileComponent from '~/diffs/components/diff_file.vue';
@@ -11,19 +15,33 @@ import {
EVT_EXPAND_ALL_FILES,
EVT_PERF_MARK_DIFF_FILES_END,
EVT_PERF_MARK_FIRST_DIFF_FILE_SHOWN,
+ FILE_DIFF_POSITION_TYPE,
} from '~/diffs/constants';
import eventHub from '~/diffs/event_hub';
-import createDiffsStore from '~/diffs/store/modules';
import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
import axios from '~/lib/utils/axios_utils';
import { scrollToElement } from '~/lib/utils/common_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import createNotesStore from '~/notes/stores/modules';
+import diffsModule from '~/diffs/store/modules';
+import { SOMETHING_WENT_WRONG, SAVING_THE_COMMENT_FAILED } from '~/diffs/i18n';
+import diffLineNoteFormMixin from '~/notes/mixins/diff_line_note_form';
import { getDiffFileMock } from '../mock_data/diff_file';
import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable';
+import diffsMockData from '../mock_data/merge_request_diffs';
jest.mock('~/lib/utils/common_utils');
+jest.mock('~/alert');
+jest.mock('~/notes/mixins/diff_line_note_form', () => ({
+ methods: {
+ addToReview: jest.fn(),
+ },
+}));
+
+Vue.use(Vuex);
+
+const saveDiffDiscussionMock = jest.fn();
function changeViewer(store, index, { automaticallyCollapsed, manuallyCollapsed, name }) {
const file = store.state.diffs.diffFiles[index];
@@ -70,18 +88,29 @@ function markFileToBeRendered(store, index = 0) {
}
function createComponent({ file, first = false, last = false, options = {}, props = {} }) {
- Vue.use(Vuex);
+ const diffs = diffsModule();
+ diffs.actions = {
+ ...diffs.actions,
+ saveDiffDiscussion: saveDiffDiscussionMock,
+ };
+
+ diffs.getters = {
+ ...diffs.getters,
+ diffCompareDropdownTargetVersions: () => [],
+ diffCompareDropdownSourceVersions: () => [],
+ };
const store = new Vuex.Store({
...createNotesStore(),
- modules: {
- diffs: createDiffsStore(),
- },
+ modules: { diffs },
});
- store.state.diffs.diffFiles = [file];
+ store.state.diffs = {
+ mergeRequestDiff: diffsMockData[0],
+ diffFiles: [file],
+ };
- const wrapper = shallowMount(DiffFileComponent, {
+ const wrapper = shallowMountExtended(DiffFileComponent, {
store,
propsData: {
file,
@@ -101,9 +130,10 @@ function createComponent({ file, first = false, last = false, options = {}, prop
}
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"]');
+const findDiffContentArea = (wrapper) => wrapper.findByTestId('content-area');
+const findLoader = (wrapper) => wrapper.findByTestId('loader-icon');
+const findToggleButton = (wrapper) => wrapper.findByTestId('expand-button');
+const findNoteForm = (wrapper) => wrapper.findByTestId('file-note-form');
const toggleFile = (wrapper) => findDiffHeader(wrapper).vm.$emit('toggleFile');
const getReadableFile = () => getDiffFileMock();
@@ -118,6 +148,12 @@ const makeFileManuallyCollapsed = (store, index = 0) =>
const changeViewerType = (store, newType, index = 0) =>
changeViewer(store, index, { name: diffViewerModes[newType] });
+const triggerSaveNote = (wrapper, note, parent, error) =>
+ findNoteForm(wrapper).vm.$emit('handleFormUpdate', note, parent, error);
+
+const triggerSaveDraftNote = (wrapper, note, parent, error) =>
+ findNoteForm(wrapper).vm.$emit('handleFormUpdateAddToReview', note, false, parent, error);
+
describe('DiffFile', () => {
let wrapper;
let store;
@@ -502,7 +538,7 @@ describe('DiffFile', () => {
await nextTick();
- const button = wrapper.find('[data-testid="blob-button"]');
+ const button = wrapper.findByTestId('blob-button');
expect(wrapper.text()).toContain('Changes are too large to be shown.');
expect(button.html()).toContain('View file @');
@@ -510,24 +546,6 @@ describe('DiffFile', () => {
});
});
- it('loads collapsed file on mounted when single file mode is enabled', async () => {
- const file = {
- ...getReadableFile(),
- load_collapsed_diff_url: '/diff_for_path',
- highlighted_diff_lines: [],
- parallel_diff_lines: [],
- viewer: { name: 'collapsed', automaticallyCollapsed: true },
- };
-
- axiosMock.onGet(file.load_collapsed_diff_url).reply(HTTP_STATUS_OK, getReadableFile());
-
- ({ wrapper, store } = createComponent({ file, props: { viewDiffsFileByFile: true } }));
-
- await nextTick();
-
- expect(findLoader(wrapper).exists()).toBe(true);
- });
-
describe('merge conflicts', () => {
it('does not render conflict alert', () => {
const file = {
@@ -538,7 +556,7 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({ file }));
- expect(wrapper.find('[data-testid="conflictsAlert"]').exists()).toBe(false);
+ expect(wrapper.findByTestId('conflictsAlert').exists()).toBe(false);
});
it('renders conflict alert when conflict_type is present', () => {
@@ -550,7 +568,7 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({ file }));
- expect(wrapper.find('[data-testid="conflictsAlert"]').exists()).toBe(true);
+ expect(wrapper.findByTestId('conflictsAlert').exists()).toBe(true);
});
});
@@ -572,10 +590,9 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({
file,
- options: { provide: { glFeatures: { commentOnFiles: true } } },
}));
- expect(wrapper.find('[data-testid="file-discussions"]').exists()).toEqual(exists);
+ expect(wrapper.findByTestId('file-discussions').exists()).toEqual(exists);
},
);
@@ -593,10 +610,9 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({
file,
- options: { provide: { glFeatures: { commentOnFiles: true } } },
}));
- expect(wrapper.find('[data-testid="file-note-form"]').exists()).toEqual(exists);
+ expect(findNoteForm(wrapper).exists()).toEqual(exists);
},
);
@@ -612,10 +628,99 @@ describe('DiffFile', () => {
({ wrapper, store } = createComponent({
file,
- options: { provide: { glFeatures: { commentOnFiles: true } } },
}));
- expect(wrapper.find('[data-testid="diff-file-discussions"]').exists()).toEqual(exists);
+ expect(wrapper.findByTestId('diff-file-discussions').exists()).toEqual(exists);
+ });
+
+ describe('when note-form emits `handleFormUpdate`', () => {
+ const file = {
+ ...getReadableFile(),
+ hasCommentForm: true,
+ };
+
+ const note = {};
+ const parentElement = null;
+ const errorCallback = jest.fn();
+
+ beforeEach(() => {
+ ({ wrapper, store } = createComponent({
+ file,
+ options: { provide: { glFeatures: { commentOnFiles: true } } },
+ }));
+ });
+
+ it('calls saveDiffDiscussionMock', () => {
+ triggerSaveNote(wrapper, note, parentElement, errorCallback);
+
+ expect(saveDiffDiscussionMock).toHaveBeenCalledWith(expect.any(Object), {
+ note,
+ formData: {
+ noteableData: expect.any(Object),
+ diffFile: file,
+ positionType: FILE_DIFF_POSITION_TYPE,
+ noteableType: store.getters.noteableType,
+ },
+ });
+ });
+
+ describe('when saveDiffDiscussionMock throws an error', () => {
+ describe.each`
+ scenario | serverError | message
+ ${'with server error'} | ${{ data: { errors: 'error' } }} | ${SAVING_THE_COMMENT_FAILED}
+ ${'without server error'} | ${{}} | ${SOMETHING_WENT_WRONG}
+ `('$scenario', ({ serverError, message }) => {
+ beforeEach(async () => {
+ saveDiffDiscussionMock.mockRejectedValue({ response: serverError });
+
+ triggerSaveNote(wrapper, note, parentElement, errorCallback);
+
+ await waitForPromises();
+ });
+
+ it(`renders ${serverError ? 'server' : 'generic'} error message`, () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: sprintf(message, { reason: serverError?.data?.errors }),
+ parent: parentElement,
+ });
+ });
+
+ it('calls errorCallback', () => {
+ expect(errorCallback).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+
+ describe('when note-form emits `handleFormUpdateAddToReview`', () => {
+ const file = {
+ ...getReadableFile(),
+ hasCommentForm: true,
+ };
+
+ const note = {};
+ const parentElement = null;
+ const errorCallback = jest.fn();
+
+ beforeEach(async () => {
+ ({ wrapper, store } = createComponent({
+ file,
+ options: { provide: { glFeatures: { commentOnFiles: true } } },
+ }));
+
+ triggerSaveDraftNote(wrapper, note, parentElement, errorCallback);
+
+ await nextTick();
+ });
+
+ it('calls addToReview mixin', () => {
+ expect(diffLineNoteFormMixin.methods.addToReview).toHaveBeenCalledWith(
+ note,
+ FILE_DIFF_POSITION_TYPE,
+ parentElement,
+ errorCallback,
+ );
+ });
});
});
});
diff --git a/spec/frontend/diffs/components/diff_inline_findings_spec.js b/spec/frontend/diffs/components/diff_inline_findings_spec.js
new file mode 100644
index 00000000000..9ccfb2a613d
--- /dev/null
+++ b/spec/frontend/diffs/components/diff_inline_findings_spec.js
@@ -0,0 +1,33 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import DiffInlineFindings from '~/diffs/components/diff_inline_findings.vue';
+import DiffCodeQualityItem from '~/diffs/components/diff_code_quality_item.vue';
+import { NEW_CODE_QUALITY_FINDINGS } from '~/diffs/i18n';
+import { multipleCodeQualityNoSast } from '../mock_data/diff_code_quality';
+
+let wrapper;
+const heading = () => wrapper.findByTestId('diff-inline-findings-heading');
+const diffCodeQualityItems = () => wrapper.findAllComponents(DiffCodeQualityItem);
+
+describe('DiffInlineFindings', () => {
+ const createWrapper = () => {
+ return shallowMountExtended(DiffInlineFindings, {
+ propsData: {
+ title: NEW_CODE_QUALITY_FINDINGS,
+ findings: multipleCodeQualityNoSast.codeQuality,
+ },
+ });
+ };
+
+ it('renders the title correctly', () => {
+ wrapper = createWrapper();
+ expect(heading().text()).toBe(NEW_CODE_QUALITY_FINDINGS);
+ });
+
+ it('renders the correct number of DiffCodeQualityItem components with correct props', () => {
+ wrapper = createWrapper();
+ expect(diffCodeQualityItems()).toHaveLength(multipleCodeQualityNoSast.codeQuality.length);
+ expect(diffCodeQualityItems().wrappers[0].props('finding')).toEqual(
+ wrapper.props('findings')[0],
+ );
+ });
+});
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 e42b98e4d68..0ca48db2497 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -1,15 +1,20 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { sprintf } from '~/locale';
+import { createAlert } from '~/alert';
import DiffLineNoteForm from '~/diffs/components/diff_line_note_form.vue';
import store from '~/mr_notes/stores';
import NoteForm from '~/notes/components/note_form.vue';
import MultilineCommentForm from '~/notes/components/multiline_comment_form.vue';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import { noteableDataMock } from 'jest/notes/mock_data';
+import { SOMETHING_WENT_WRONG, SAVING_THE_COMMENT_FAILED } from '~/diffs/i18n';
import { getDiffFileMock } from '../mock_data/diff_file';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores'));
+jest.mock('~/alert');
describe('DiffLineNoteForm', () => {
let wrapper;
@@ -17,6 +22,8 @@ describe('DiffLineNoteForm', () => {
let diffLines;
beforeEach(() => {
+ store.reset();
+
diffFile = getDiffFileMock();
diffLines = diffFile.highlighted_diff_lines;
@@ -214,5 +221,38 @@ describe('DiffLineNoteForm', () => {
fileHash: diffFile.file_hash,
});
});
+
+ describe('when note-form emits `handleFormUpdate`', () => {
+ const noteStub = 'invalid note';
+ const parentElement = null;
+ const errorCallback = jest.fn();
+
+ describe.each`
+ scenario | serverError | message
+ ${'with server error'} | ${{ data: { errors: 'error' } }} | ${SAVING_THE_COMMENT_FAILED}
+ ${'without server error'} | ${null} | ${SOMETHING_WENT_WRONG}
+ `('$scenario', ({ serverError, message }) => {
+ beforeEach(async () => {
+ store.dispatch.mockRejectedValue({ response: serverError });
+
+ createComponent();
+
+ await findNoteForm().vm.$emit('handleFormUpdate', noteStub, parentElement, errorCallback);
+
+ await waitForPromises();
+ });
+
+ it(`renders ${serverError ? 'server' : 'generic'} error message`, () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: sprintf(message, { reason: serverError?.data?.errors }),
+ parent: parentElement,
+ });
+ });
+
+ it('calls errorCallback', () => {
+ expect(errorCallback).toHaveBeenCalled();
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/diffs/components/diff_line_spec.js b/spec/frontend/diffs/components/diff_line_spec.js
index 37368eb1461..a552a9d3e7f 100644
--- a/spec/frontend/diffs/components/diff_line_spec.js
+++ b/spec/frontend/diffs/components/diff_line_spec.js
@@ -16,6 +16,13 @@ const left = {
severity: EXAMPLE_SEVERITY,
},
],
+ sast: [
+ {
+ line: EXAMPLE_LINE_NUMBER,
+ description: EXAMPLE_DESCRIPTION,
+ severity: EXAMPLE_SEVERITY,
+ },
+ ],
},
},
};
@@ -30,6 +37,13 @@ const right = {
severity: EXAMPLE_SEVERITY,
},
],
+ sast: [
+ {
+ line: EXAMPLE_LINE_NUMBER,
+ description: EXAMPLE_DESCRIPTION,
+ severity: EXAMPLE_SEVERITY,
+ },
+ ],
},
},
};
@@ -60,6 +74,13 @@ describe('DiffLine', () => {
severity: EXAMPLE_SEVERITY,
},
]);
+ expect(wrapper.findComponent(DiffCodeQuality).props('sast')).toEqual([
+ {
+ line: EXAMPLE_LINE_NUMBER,
+ description: EXAMPLE_DESCRIPTION,
+ severity: EXAMPLE_SEVERITY,
+ },
+ ]);
});
});
});
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index 356c7ef925a..119b8f9ad7f 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -33,6 +33,14 @@ describe('DiffRow', () => {
left: { old_line: 1, discussions: [] },
right: { new_line: 1, discussions: [] },
},
+ {
+ left: {},
+ right: {},
+ isMetaLineLeft: true,
+ isMetaLineRight: false,
+ isContextLineLeft: true,
+ isContextLineRight: false,
+ },
];
const createWrapper = ({ props, state = {}, actions, isLoggedIn = true }) => {
@@ -273,6 +281,12 @@ describe('DiffRow', () => {
expect(findInteropAttributes(wrapper, '[data-testid="right-side"]')).toEqual(rightSide);
});
});
+
+ it('renders comment button when isMetaLineLeft is false and isMetaLineRight is true', () => {
+ wrapper = createWrapper({ props: { line: testLines[4], inline: false } });
+
+ expect(wrapper.find('.add-diff-note').exists()).toBe(true);
+ });
});
describe('coverage state memoization', () => {
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index 1ec8547d325..f56dd28ce9c 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -1,4 +1,3 @@
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import TreeList from '~/diffs/components/tree_list.vue';
@@ -6,18 +5,21 @@ import createStore from '~/diffs/store/modules';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import DiffFileRow from '~/diffs/components//diff_file_row.vue';
import { stubComponent } from 'helpers/stub_component';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
describe('Diffs tree list component', () => {
let wrapper;
let store;
const getScroller = () => wrapper.findComponent({ name: 'RecycleScroller' });
const getFileRow = () => wrapper.findComponent(DiffFileRow);
+ const findDiffTreeSearch = () => wrapper.findByTestId('diff-tree-search');
+
Vue.use(Vuex);
- const createComponent = () => {
- wrapper = shallowMount(TreeList, {
+ const createComponent = ({ hideFileStats = false } = {}) => {
+ wrapper = shallowMountExtended(TreeList, {
store,
- propsData: { hideFileStats: false },
+ propsData: { hideFileStats },
stubs: {
// eslint will fail if we import the real component
RecycleScroller: stubComponent(
@@ -116,7 +118,10 @@ describe('Diffs tree list component', () => {
describe('search by file extension', () => {
it('hides scroller for no matches', async () => {
- wrapper.find('[data-testid="diff-tree-search"]').setValue('*.md');
+ const input = findDiffTreeSearch();
+
+ input.element.value = '*.md';
+ input.trigger('input');
await nextTick();
@@ -131,7 +136,10 @@ describe('Diffs tree list component', () => {
${'app/*.js'} | ${2}
${'*.js, *.rb'} | ${3}
`('returns $itemSize item for $extension', async ({ extension, itemSize }) => {
- wrapper.find('[data-testid="diff-tree-search"]').setValue(extension);
+ const input = findDiffTreeSearch();
+
+ input.element.value = extension;
+ input.trigger('input');
await nextTick();
@@ -143,23 +151,21 @@ describe('Diffs tree list component', () => {
expect(getScroller().props('items')).toHaveLength(2);
});
- it('hides file stats', async () => {
- wrapper.setProps({ hideFileStats: true });
-
- await nextTick();
- expect(wrapper.find('.file-row-stats').exists()).toBe(false);
+ it('hides file stats', () => {
+ createComponent({ hideFileStats: true });
+ expect(getFileRow().props('hideFileStats')).toBe(true);
});
it('calls toggleTreeOpen when clicking folder', () => {
- jest.spyOn(wrapper.vm.$store, 'dispatch').mockReturnValue(undefined);
+ jest.spyOn(store, 'dispatch').mockReturnValue(undefined);
getFileRow().vm.$emit('toggleTreeOpen', 'app');
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith('diffs/toggleTreeOpen', 'app');
+ expect(store.dispatch).toHaveBeenCalledWith('diffs/toggleTreeOpen', 'app');
});
it('renders when renderTreeList is false', async () => {
- wrapper.vm.$store.state.diffs.renderTreeList = false;
+ store.state.diffs.renderTreeList = false;
await nextTick();
expect(getScroller().props('items')).toHaveLength(3);
@@ -178,7 +184,7 @@ describe('Diffs tree list component', () => {
createComponent();
await nextTick();
- expect(wrapper.findComponent(DiffFileRow).props('viewedFiles')).toBe(viewedDiffFileIds);
+ expect(getFileRow().props('viewedFiles')).toBe(viewedDiffFileIds);
});
});
});
diff --git a/spec/frontend/diffs/mock_data/diff_code_quality.js b/spec/frontend/diffs/mock_data/diff_code_quality.js
index 29f16da8d89..5b9ed538e01 100644
--- a/spec/frontend/diffs/mock_data/diff_code_quality.js
+++ b/spec/frontend/diffs/mock_data/diff_code_quality.js
@@ -1,49 +1,120 @@
-export const multipleFindingsArr = [
+export const multipleFindingsArrCodeQualityScale = [
{
severity: 'minor',
description: 'mocked minor Issue',
line: 2,
+ scale: 'codeQuality',
},
{
severity: 'major',
description: 'mocked major Issue',
line: 3,
+ scale: 'codeQuality',
},
{
severity: 'info',
description: 'mocked info Issue',
line: 3,
+ scale: 'codeQuality',
},
{
severity: 'critical',
description: 'mocked critical Issue',
line: 3,
+ scale: 'codeQuality',
},
{
severity: 'blocker',
description: 'mocked blocker Issue',
line: 3,
+ scale: 'codeQuality',
},
{
severity: 'unknown',
description: 'mocked unknown Issue',
line: 3,
+ scale: 'codeQuality',
},
];
-export const fiveFindings = {
+export const multipleFindingsArrSastScale = [
+ {
+ severity: 'low',
+ description: 'mocked low Issue',
+ line: 2,
+ scale: 'sast',
+ },
+ {
+ severity: 'medium',
+ description: 'mocked medium Issue',
+ line: 3,
+ scale: 'sast',
+ },
+ {
+ severity: 'info',
+ description: 'mocked info Issue',
+ line: 3,
+ scale: 'sast',
+ },
+ {
+ severity: 'high',
+ description: 'mocked high Issue',
+ line: 3,
+ scale: 'sast',
+ },
+ {
+ severity: 'critical',
+ description: 'mocked critical Issue',
+ line: 3,
+ scale: 'sast',
+ },
+ {
+ severity: 'unknown',
+ description: 'mocked unknown Issue',
+ line: 3,
+ scale: 'sast',
+ },
+];
+
+export const multipleCodeQualityNoSast = {
+ codeQuality: multipleFindingsArrCodeQualityScale,
+ sast: [],
+};
+
+export const multipleSastNoCodeQuality = {
+ codeQuality: [],
+ sast: multipleFindingsArrSastScale,
+};
+
+export const fiveCodeQualityFindings = {
+ filePath: 'index.js',
+ codequality: multipleFindingsArrCodeQualityScale.slice(0, 5),
+};
+
+export const threeCodeQualityFindings = {
+ filePath: 'index.js',
+ codequality: multipleFindingsArrCodeQualityScale.slice(0, 3),
+};
+
+export const singularCodeQualityFinding = {
+ filePath: 'index.js',
+ codequality: [multipleFindingsArrCodeQualityScale[0]],
+};
+
+export const singularFindingSast = {
filePath: 'index.js',
- codequality: multipleFindingsArr.slice(0, 5),
+ sast: [multipleFindingsArrSastScale[0]],
};
-export const threeFindings = {
+export const threeSastFindings = {
filePath: 'index.js',
- codequality: multipleFindingsArr.slice(0, 3),
+ sast: multipleFindingsArrSastScale.slice(0, 3),
};
-export const singularFinding = {
+export const oneCodeQualityTwoSastFindings = {
filePath: 'index.js',
- codequality: [multipleFindingsArr[0]],
+ sast: multipleFindingsArrSastScale.slice(0, 2),
+ codequality: [multipleFindingsArrCodeQualityScale[0]],
};
export const diffCodeQuality = {
@@ -73,7 +144,7 @@ export const diffCodeQuality = {
old_line: null,
new_line: 2,
- codequality: [multipleFindingsArr[0]],
+ codequality: [multipleFindingsArrCodeQualityScale[0]],
lineDrafts: [],
},
},
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 7534fe741e7..bbe748b8e1f 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -11,7 +11,7 @@ import {
PARALLEL_DIFF_VIEW_TYPE,
EVT_MR_PREPARED,
} from '~/diffs/constants';
-import { LOAD_SINGLE_DIFF_FAILED } from '~/diffs/i18n';
+import { LOAD_SINGLE_DIFF_FAILED, BUILDING_YOUR_MR, SOMETHING_WENT_WRONG } from '~/diffs/i18n';
import * as diffActions from '~/diffs/store/actions';
import * as types from '~/diffs/store/mutation_types';
import * as utils from '~/diffs/store/utils';
@@ -87,6 +87,7 @@ describe('DiffsStoreActions', () => {
a: ['z', 'hash:a'],
b: ['y', 'hash:a'],
};
+ const diffViewType = 'inline';
return testAction(
diffActions.setBaseConfig,
@@ -100,6 +101,7 @@ describe('DiffsStoreActions', () => {
dismissEndpoint,
showSuggestPopover,
mrReviews,
+ diffViewType,
},
{
endpoint: '',
@@ -124,6 +126,7 @@ describe('DiffsStoreActions', () => {
dismissEndpoint,
showSuggestPopover,
mrReviews,
+ diffViewType,
},
},
{
@@ -362,7 +365,7 @@ describe('DiffsStoreActions', () => {
{ type: types.SET_RETRIEVING_BATCHES, payload: false },
{ type: types.SET_BATCH_LOADING_STATE, payload: 'error' },
],
- [{ type: 'startRenderDiffsQueue' }, { type: 'startRenderDiffsQueue' }],
+ [],
);
});
});
@@ -418,9 +421,7 @@ describe('DiffsStoreActions', () => {
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
- message: expect.stringMatching(
- 'Building your merge request… This page will update when the build is complete.',
- ),
+ message: BUILDING_YOUR_MR,
variant: 'warning',
});
});
@@ -482,7 +483,7 @@ describe('DiffsStoreActions', () => {
await testAction(diffActions.fetchCoverageFiles, {}, { endpointCoverage }, [], []);
expect(createAlert).toHaveBeenCalledTimes(1);
expect(createAlert).toHaveBeenCalledWith({
- message: expect.stringMatching('Something went wrong'),
+ message: SOMETHING_WENT_WRONG,
});
});
});
@@ -663,41 +664,6 @@ describe('DiffsStoreActions', () => {
});
});
- describe('startRenderDiffsQueue', () => {
- it('should set all files to RENDER_FILE', () => {
- const state = {
- diffFiles: [
- {
- id: 1,
- renderIt: false,
- viewer: {
- automaticallyCollapsed: false,
- },
- },
- {
- id: 2,
- renderIt: false,
- viewer: {
- automaticallyCollapsed: false,
- },
- },
- ],
- };
-
- const pseudoCommit = (commitType, file) => {
- expect(commitType).toBe(types.RENDER_FILE);
- Object.assign(file, {
- renderIt: true,
- });
- };
-
- diffActions.startRenderDiffsQueue({ state, commit: pseudoCommit });
-
- expect(state.diffFiles[0].renderIt).toBe(true);
- expect(state.diffFiles[1].renderIt).toBe(true);
- });
- });
-
describe('setInlineDiffViewType', () => {
it('should set diff view type to inline and also set the cookie properly', async () => {
await testAction(
@@ -1285,12 +1251,11 @@ describe('DiffsStoreActions', () => {
$emit = jest.spyOn(eventHub, '$emit');
});
- it('renders and expands file for the given discussion id', () => {
+ it('expands the file for the given discussion id', () => {
const localState = state({ collapsed: true, renderIt: false });
diffActions.renderFileForDiscussionId({ rootState, state: localState, commit }, '123');
- expect(commit).toHaveBeenCalledWith('RENDER_FILE', localState.diffFiles[0]);
expect($emit).toHaveBeenCalledTimes(1);
expect(commonUtils.scrollToElement).toHaveBeenCalledTimes(1);
});
@@ -1377,18 +1342,6 @@ describe('DiffsStoreActions', () => {
});
});
- describe('setRenderIt', () => {
- it('commits RENDER_FILE', () => {
- return testAction(
- diffActions.setRenderIt,
- 'file',
- {},
- [{ type: types.RENDER_FILE, payload: 'file' }],
- [],
- );
- });
- });
-
describe('receiveFullDiffError', () => {
it('updates state with the file that did not load', () => {
return testAction(
@@ -1513,7 +1466,7 @@ describe('DiffsStoreActions', () => {
payload: { filePath: testFilePath, lines: [preparedLine, preparedLine] },
},
],
- [{ type: 'startRenderDiffsQueue' }],
+ [],
);
},
);
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index b089cf22b14..274cb40dac8 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -12,6 +12,7 @@ describe('DiffsStoreMutations', () => {
${'endpoint'} | ${'/diffs/endpoint'}
${'projectPath'} | ${'/root/project'}
${'endpointUpdateUser'} | ${'/user/preferences'}
+ ${'diffViewType'} | ${'parallel'}
`('should set the $prop property into state', ({ prop, value }) => {
const state = {};
@@ -104,7 +105,6 @@ describe('DiffsStoreMutations', () => {
mutations[types.SET_DIFF_DATA_BATCH](state, diffMock);
- expect(state.diffFiles[0].renderIt).toEqual(true);
expect(state.diffFiles[0].collapsed).toEqual(false);
expect(state.treeEntries[mockFile.file_path].diffLoaded).toBe(true);
});
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 888df06d6b9..117ed56e347 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -437,7 +437,7 @@ describe('DiffsStoreUtils', () => {
});
});
- it('sets the renderIt and collapsed attribute on files', () => {
+ it('sets the collapsed attribute on files', () => {
const checkLine = preparedDiff.diff_files[0][INLINE_DIFF_LINES_KEY][0];
expect(checkLine.discussions.length).toBe(0);
@@ -448,7 +448,6 @@ describe('DiffsStoreUtils', () => {
expect(firstChar).not.toBe('+');
expect(firstChar).not.toBe('-');
- expect(preparedDiff.diff_files[0].renderIt).toBe(true);
expect(preparedDiff.diff_files[0].collapsed).toBe(false);
});
@@ -529,8 +528,7 @@ describe('DiffsStoreUtils', () => {
preparedDiffFiles = utils.prepareDiffData({ diff: mock, meta: true });
});
- it('sets the renderIt and collapsed attribute on files', () => {
- expect(preparedDiffFiles[0].renderIt).toBe(true);
+ it('sets the collapsed attribute on files', () => {
expect(preparedDiffFiles[0].collapsed).toBeUndefined();
});
diff --git a/spec/frontend/drawio/drawio_editor_spec.js b/spec/frontend/drawio/drawio_editor_spec.js
index 4d93908b757..5a77b9d4689 100644
--- a/spec/frontend/drawio/drawio_editor_spec.js
+++ b/spec/frontend/drawio/drawio_editor_spec.js
@@ -66,7 +66,6 @@ describe('drawio/drawio_editor', () => {
});
afterEach(() => {
- jest.clearAllMocks();
findDrawioIframe()?.remove();
});
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index 57debf79c7b..ba4d838e44b 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -1,6 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
-import htmlNewMilestone from 'test_fixtures/milestones/new-milestone.html';
import mock from 'xhr-mock';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
@@ -9,6 +8,7 @@ import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
import dropzoneInput from '~/dropzone_input';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import htmlNewMilestone from 'test_fixtures_static/textarea.html';
const TEST_FILE = new File([], 'somefile.jpg');
TEST_FILE.upload = {};
diff --git a/spec/frontend/editor/source_editor_extension_base_spec.js b/spec/frontend/editor/source_editor_extension_base_spec.js
index 70bc1dee0ee..c820d6ac63d 100644
--- a/spec/frontend/editor/source_editor_extension_base_spec.js
+++ b/spec/frontend/editor/source_editor_extension_base_spec.js
@@ -56,7 +56,6 @@ describe('The basis for an Source Editor extension', () => {
});
afterEach(() => {
- jest.clearAllMocks();
resetHTMLFixture();
});
diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
index 512b298bbbd..d9e1a22d60d 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -182,10 +182,6 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
instance.togglePreview();
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it('does not do anything if there is no model', () => {
instance.setModel(null);
@@ -199,9 +195,6 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
mockAxios.onPost().reply(HTTP_STATUS_OK, { body: responseData });
await togglePreview();
});
- afterEach(() => {
- jest.clearAllMocks();
- });
it('removes the registered buttons from the toolbar', () => {
expect(instance.toolbar.removeItems).not.toHaveBeenCalled();
diff --git a/spec/frontend/editor/source_editor_yaml_ext_spec.js b/spec/frontend/editor/source_editor_yaml_ext_spec.js
index 14ec7f8b93f..4b1ed0fbb42 100644
--- a/spec/frontend/editor/source_editor_yaml_ext_spec.js
+++ b/spec/frontend/editor/source_editor_yaml_ext_spec.js
@@ -368,10 +368,6 @@ abc: def
let highlightLinesSpy;
let removeHighlightsSpy;
- afterEach(() => {
- jest.clearAllMocks();
- });
-
it.each`
highlightPathOnSetup | path | keepOnNotFound | expectHighlightLinesToBeCalled | withLines | expectRemoveHighlightsToBeCalled | storedHighlightPath
${null} | ${undefined} | ${false} | ${false} | ${undefined} | ${true} | ${null}
diff --git a/spec/frontend/emoji/index_spec.js b/spec/frontend/emoji/index_spec.js
index 36c3eeb5a52..1b948cce73a 100644
--- a/spec/frontend/emoji/index_spec.js
+++ b/spec/frontend/emoji/index_spec.js
@@ -7,6 +7,7 @@ import {
clearEmojiMock,
} from 'helpers/emoji';
import { trimText } from 'helpers/text_helper';
+import { createMockClient } from 'helpers/mock_apollo_helper';
import {
glEmojiTag,
searchEmoji,
@@ -14,6 +15,8 @@ import {
sortEmoji,
initEmojiMap,
getAllEmoji,
+ emojiFallbackImageSrc,
+ loadCustomEmojiWithNames,
} from '~/emoji';
import isEmojiUnicodeSupported, {
@@ -25,6 +28,12 @@ import isEmojiUnicodeSupported, {
isPersonZwjEmoji,
} from '~/emoji/support/is_emoji_unicode_supported';
import { NEUTRAL_INTENT_MULTIPLIER } from '~/emoji/constants';
+import customEmojiQuery from '~/emoji/queries/custom_emoji.query.graphql';
+
+let mockClient;
+jest.mock('~/lib/graphql', () => {
+ return () => mockClient;
+});
const emptySupportMap = {
personZwj: false,
@@ -45,12 +54,35 @@ const emptySupportMap = {
1.1: false,
};
+function createMockEmojiClient() {
+ mockClient = createMockClient([
+ [
+ customEmojiQuery,
+ jest.fn().mockResolvedValue({
+ data: {
+ group: {
+ id: 1,
+ customEmoji: {
+ nodes: [{ id: 1, name: 'parrot', url: 'parrot.gif' }],
+ },
+ },
+ },
+ }),
+ ],
+ ]);
+
+ window.gon = { features: { customEmoji: true } };
+ document.body.dataset.groupFullPath = 'test-group';
+}
+
describe('emoji', () => {
beforeEach(async () => {
await initEmojiMock();
});
afterEach(() => {
+ window.gon = {};
+ delete document.body.dataset.groupFullPath;
clearEmojiMock();
});
@@ -690,4 +722,67 @@ describe('emoji', () => {
expect(scoredItems.sort(sortEmoji)).toEqual(expected);
});
});
+
+ describe('emojiFallbackImageSrc', () => {
+ beforeEach(async () => {
+ createMockEmojiClient();
+
+ await initEmojiMock();
+ });
+
+ it.each`
+ emoji | src
+ ${'thumbsup'} | ${'/-/emojis/2/thumbsup.png'}
+ ${'parrot'} | ${'parrot.gif'}
+ `('returns $src for emoji with name $emoji', ({ emoji, src }) => {
+ expect(emojiFallbackImageSrc(emoji)).toBe(src);
+ });
+ });
+
+ describe('loadCustomEmojiWithNames', () => {
+ beforeEach(() => {
+ createMockEmojiClient();
+ });
+
+ describe('flag disabled', () => {
+ beforeEach(() => {
+ window.gon = {};
+ });
+
+ it('returns empty object', async () => {
+ const result = await loadCustomEmojiWithNames();
+
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('when not in a group', () => {
+ beforeEach(() => {
+ delete document.body.dataset.groupFullPath;
+ });
+
+ it('returns empty object', async () => {
+ const result = await loadCustomEmojiWithNames();
+
+ expect(result).toEqual({});
+ });
+ });
+
+ describe('when in a group with flag enabled', () => {
+ it('returns empty object', async () => {
+ const result = await loadCustomEmojiWithNames();
+
+ expect(result).toEqual({
+ parrot: {
+ c: 'custom',
+ d: 'parrot',
+ e: undefined,
+ name: 'parrot',
+ src: 'parrot.gif',
+ u: 'custom',
+ },
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js
index f436c96f4a5..93fe9ed9400 100644
--- a/spec/frontend/environments/edit_environment_spec.js
+++ b/spec/frontend/environments/edit_environment_spec.js
@@ -1,15 +1,13 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import EditEnvironment from '~/environments/components/edit_environment.vue';
import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import getEnvironment from '~/environments/graphql/queries/environment.query.graphql';
+import getEnvironmentWithNamespace from '~/environments/graphql/queries/environment_with_namespace.graphql';
import updateEnvironment from '~/environments/graphql/mutations/update_environment.mutation.graphql';
import { __ } from '~/locale';
import createMockApollo from '../__helpers__/mock_apollo_helper';
@@ -17,15 +15,15 @@ import createMockApollo from '../__helpers__/mock_apollo_helper';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/alert');
-const newExternalUrl = 'https://google.ca';
const environment = {
id: '1',
name: 'foo',
externalUrl: 'https://foo.example.com',
clusterAgent: null,
+ kubernetesNamespace: null,
};
const resolvedEnvironment = { project: { id: '1', environment } };
-const environmentUpdate = {
+const environmentUpdateSuccess = {
environment: { id: '1', path: 'path/to/environment', clusterAgentId: null },
errors: [],
};
@@ -36,46 +34,51 @@ const environmentUpdateError = {
const provide = {
projectEnvironmentsPath: '/projects/environments',
- updateEnvironmentPath: '/projects/environments/1',
protectedEnvironmentSettingsPath: '/projects/1/settings/ci_cd',
projectPath: '/path/to/project',
+ environmentName: 'foo',
};
describe('~/environments/components/edit.vue', () => {
let wrapper;
- let mock;
- const createMockApolloProvider = (mutationResult) => {
+ const getEnvironmentQuery = jest.fn().mockResolvedValue({ data: resolvedEnvironment });
+ const getEnvironmentWithNamespaceQuery = jest
+ .fn()
+ .mockResolvedValue({ data: resolvedEnvironment });
+
+ const updateEnvironmentSuccess = jest
+ .fn()
+ .mockResolvedValue({ data: { environmentUpdate: environmentUpdateSuccess } });
+ const updateEnvironmentFail = jest
+ .fn()
+ .mockResolvedValue({ data: { environmentUpdate: environmentUpdateError } });
+
+ const createMockApolloProvider = (mutationHandler) => {
Vue.use(VueApollo);
const mocks = [
- [getEnvironment, jest.fn().mockResolvedValue({ data: resolvedEnvironment })],
- [
- updateEnvironment,
- jest.fn().mockResolvedValue({ data: { environmentUpdate: mutationResult } }),
- ],
+ [getEnvironment, getEnvironmentQuery],
+ [getEnvironmentWithNamespace, getEnvironmentWithNamespaceQuery],
+ [updateEnvironment, mutationHandler],
];
return createMockApollo(mocks);
};
- const createWrapper = () => {
- wrapper = mountExtended(EditEnvironment, {
- propsData: { environment: { id: '1', name: 'foo', external_url: 'https://foo.example.com' } },
- provide,
- });
- };
-
- const createWrapperWithApollo = async ({ mutationResult = environmentUpdate } = {}) => {
+ const createWrapperWithApollo = async ({
+ mutationHandler = updateEnvironmentSuccess,
+ kubernetesNamespaceForEnvironment = false,
+ } = {}) => {
wrapper = mountExtended(EditEnvironment, {
propsData: { environment: {} },
provide: {
...provide,
glFeatures: {
- environmentSettingsToGraphql: true,
+ kubernetesNamespaceForEnvironment,
},
},
- apolloProvider: createMockApolloProvider(mutationResult),
+ apolloProvider: createMockApolloProvider(mutationHandler),
});
await waitForPromises();
@@ -87,43 +90,46 @@ describe('~/environments/components/edit.vue', () => {
const showsLoading = () => wrapper.findComponent(GlLoadingIcon).exists();
- const submitForm = async () => {
- await findExternalUrlInput().setValue(newExternalUrl);
- await findForm().trigger('submit');
- };
-
describe('default', () => {
- beforeEach(async () => {
- await createWrapper();
+ it('performs the environment apollo query', () => {
+ createWrapperWithApollo();
+ expect(getEnvironmentQuery).toHaveBeenCalled();
+ });
+
+ it('renders loading icon when environment query is loading', () => {
+ createWrapperWithApollo();
+ expect(showsLoading()).toBe(true);
});
- it('sets the title to Edit environment', () => {
+ it('sets the title to Edit environment', async () => {
+ await createWrapperWithApollo();
+
const header = wrapper.findByRole('heading', { name: __('Edit environment') });
expect(header.exists()).toBe(true);
});
- it('renders a disabled "Name" field', () => {
- const nameInput = findNameInput();
+ it('renders a disabled "Name" field', async () => {
+ await createWrapperWithApollo();
+ const nameInput = findNameInput();
expect(nameInput.attributes().disabled).toBe('disabled');
expect(nameInput.element.value).toBe(environment.name);
});
- it('renders an "External URL" field', () => {
- const urlInput = findExternalUrlInput();
+ it('renders an "External URL" field', async () => {
+ await createWrapperWithApollo();
+ const urlInput = findExternalUrlInput();
expect(urlInput.element.value).toBe(environment.externalUrl);
});
});
- describe('when environmentSettingsToGraphql feature is enabled', () => {
- describe('when mounted', () => {
- beforeEach(() => {
- createWrapperWithApollo();
- });
- it('renders loading icon when environment query is loading', () => {
- expect(showsLoading()).toBe(true);
- });
+ describe('on submit', () => {
+ it('performs the updateEnvironment apollo mutation', async () => {
+ await createWrapperWithApollo();
+ await findForm().trigger('submit');
+
+ expect(updateEnvironmentSuccess).toHaveBeenCalled();
});
describe('when mutation successful', () => {
@@ -134,28 +140,28 @@ describe('~/environments/components/edit.vue', () => {
it('shows loader after form is submitted', async () => {
expect(showsLoading()).toBe(false);
- await submitForm();
+ await findForm().trigger('submit');
expect(showsLoading()).toBe(true);
});
it('submits the updated environment on submit', async () => {
- await submitForm();
+ await findForm().trigger('submit');
await waitForPromises();
- expect(visitUrl).toHaveBeenCalledWith(environmentUpdate.environment.path);
+ expect(visitUrl).toHaveBeenCalledWith(environmentUpdateSuccess.environment.path);
});
});
describe('when mutation failed', () => {
beforeEach(async () => {
await createWrapperWithApollo({
- mutationResult: environmentUpdateError,
+ mutationHandler: updateEnvironmentFail,
});
});
it('shows errors on error', async () => {
- await submitForm();
+ await findForm().trigger('submit');
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' });
@@ -164,58 +170,10 @@ describe('~/environments/components/edit.vue', () => {
});
});
- describe('when environmentSettingsToGraphql feature is disabled', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios);
- createWrapper();
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('shows loader after form is submitted', async () => {
- expect(showsLoading()).toBe(false);
-
- mock
- .onPut(provide.updateEnvironmentPath, {
- external_url: newExternalUrl,
- id: environment.id,
- })
- .reply(...[HTTP_STATUS_OK, { path: '/test' }]);
-
- await submitForm();
-
- expect(showsLoading()).toBe(true);
- });
-
- it('submits the updated environment on submit', async () => {
- mock
- .onPut(provide.updateEnvironmentPath, {
- external_url: newExternalUrl,
- id: environment.id,
- })
- .reply(...[HTTP_STATUS_OK, { path: '/test' }]);
-
- await submitForm();
- await waitForPromises();
-
- expect(visitUrl).toHaveBeenCalledWith('/test');
- });
-
- it('shows errors on error', async () => {
- mock
- .onPut(provide.updateEnvironmentPath, {
- external_url: newExternalUrl,
- id: environment.id,
- })
- .reply(...[HTTP_STATUS_BAD_REQUEST, { message: ['uh oh!'] }]);
-
- await submitForm();
- await waitForPromises();
-
- expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' });
- expect(showsLoading()).toBe(false);
+ describe('when `kubernetesNamespaceForEnvironment` is enabled', () => {
+ it('calls the `getEnvironmentWithNamespace` query', () => {
+ createWrapperWithApollo({ kubernetesNamespaceForEnvironment: true });
+ expect(getEnvironmentWithNamespaceQuery).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index db81c490747..803207bcce8 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -1,4 +1,4 @@
-import { GlLoadingIcon, GlCollapsibleListbox } from '@gitlab/ui';
+import { GlLoadingIcon, GlAlert } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
@@ -6,6 +6,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import EnvironmentForm from '~/environments/components/environment_form.vue';
import getUserAuthorizedAgents from '~/environments/graphql/queries/user_authorized_agents.query.graphql';
import createMockApollo from '../__helpers__/mock_apollo_helper';
+import { mockKasTunnelUrl } from './mock_data';
jest.mock('~/lib/utils/csrf');
@@ -15,7 +16,10 @@ const DEFAULT_PROPS = {
cancelPath: '/cancel',
};
-const PROVIDE = { protectedEnvironmentSettingsPath: '/projects/not_real/settings/ci_cd' };
+const PROVIDE = {
+ protectedEnvironmentSettingsPath: '/projects/not_real/settings/ci_cd',
+ kasTunnelUrl: mockKasTunnelUrl,
+};
const userAccessAuthorizedAgents = [
{ agent: { id: '1', name: 'agent-1' } },
{ agent: { id: '2', name: 'agent-2' } },
@@ -24,6 +28,10 @@ const userAccessAuthorizedAgents = [
describe('~/environments/components/form.vue', () => {
let wrapper;
+ const getNamespacesQueryResult = jest
+ .fn()
+ .mockReturnValue([{ metadata: { name: 'default' } }, { metadata: { name: 'agent' } }]);
+
const createWrapper = (propsData = {}, options = {}) =>
mountExtended(EnvironmentForm, {
provide: PROVIDE,
@@ -34,37 +42,57 @@ describe('~/environments/components/form.vue', () => {
},
});
- const createWrapperWithApollo = ({ propsData = {} } = {}) => {
+ const createWrapperWithApollo = ({
+ propsData = {},
+ kubernetesNamespaceForEnvironment = false,
+ queryResult = null,
+ } = {}) => {
Vue.use(VueApollo);
+ const requestHandlers = [
+ [
+ getUserAuthorizedAgents,
+ jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: '1',
+ userAccessAuthorizedAgents: { nodes: userAccessAuthorizedAgents },
+ },
+ },
+ }),
+ ],
+ ];
+
+ const mockResolvers = {
+ Query: {
+ k8sNamespaces: queryResult || getNamespacesQueryResult,
+ },
+ };
+
return mountExtended(EnvironmentForm, {
provide: {
...PROVIDE,
glFeatures: {
- environmentSettingsToGraphql: true,
+ kubernetesNamespaceForEnvironment,
},
},
propsData: {
...DEFAULT_PROPS,
...propsData,
},
- apolloProvider: createMockApollo([
- [
- getUserAuthorizedAgents,
- jest.fn().mockResolvedValue({
- data: {
- project: {
- id: '1',
- userAccessAuthorizedAgents: { nodes: userAccessAuthorizedAgents },
- },
- },
- }),
- ],
- ]),
+ apolloProvider: createMockApollo(requestHandlers, mockResolvers),
});
};
- const findAgentSelector = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAgentSelector = () => wrapper.findByTestId('agent-selector');
+ const findNamespaceSelector = () => wrapper.findByTestId('namespace-selector');
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ const selectAgent = async () => {
+ findAgentSelector().vm.$emit('shown');
+ await waitForPromises();
+ await findAgentSelector().vm.$emit('select', '2');
+ };
describe('default', () => {
beforeEach(() => {
@@ -207,12 +235,6 @@ describe('~/environments/components/form.vue', () => {
expect(urlInput.element.value).toBe('https://example.com');
});
- });
-
- describe('when `environmentSettingsToGraphql feature flag is enabled', () => {
- beforeEach(() => {
- wrapper = createWrapperWithApollo();
- });
it('renders an agent selector listbox', () => {
expect(findAgentSelector().props()).toMatchObject({
@@ -224,6 +246,12 @@ describe('~/environments/components/form.vue', () => {
items: [],
});
});
+ });
+
+ describe('agent selector', () => {
+ beforeEach(() => {
+ wrapper = createWrapperWithApollo();
+ });
it('sets the items prop of the agent selector after fetching the list', async () => {
findAgentSelector().vm.$emit('shown');
@@ -253,24 +281,146 @@ describe('~/environments/components/form.vue', () => {
});
it('updates agent selector field with the name of selected agent', async () => {
- findAgentSelector().vm.$emit('shown');
- await waitForPromises();
- await findAgentSelector().vm.$emit('select', '2');
+ await selectAgent();
expect(findAgentSelector().props('toggleText')).toBe('agent-2');
});
it('emits changes to the clusterAgentId', async () => {
- findAgentSelector().vm.$emit('shown');
- await waitForPromises();
- await findAgentSelector().vm.$emit('select', '2');
+ await selectAgent();
expect(wrapper.emitted('change')).toEqual([
- [{ name: '', externalUrl: '', clusterAgentId: '2' }],
+ [{ name: '', externalUrl: '', clusterAgentId: '2', kubernetesNamespace: null }],
]);
});
});
+ describe('namespace selector', () => {
+ it("doesn't render namespace selector if `kubernetesNamespaceForEnvironment` feature flag is disabled", () => {
+ wrapper = createWrapperWithApollo();
+ expect(findNamespaceSelector().exists()).toBe(false);
+ });
+
+ describe('when `kubernetesNamespaceForEnvironment` feature flag is enabled', () => {
+ beforeEach(() => {
+ wrapper = createWrapperWithApollo({
+ kubernetesNamespaceForEnvironment: true,
+ });
+ });
+
+ it("doesn't render namespace selector by default", () => {
+ expect(findNamespaceSelector().exists()).toBe(false);
+ });
+
+ describe('when the agent was selected', () => {
+ beforeEach(async () => {
+ await selectAgent();
+ });
+
+ it('renders namespace selector', () => {
+ expect(findNamespaceSelector().exists()).toBe(true);
+ });
+
+ it('requests the kubernetes namespaces with the correct configuration', async () => {
+ const configuration = {
+ basePath: mockKasTunnelUrl.replace(/\/$/, ''),
+ baseOptions: {
+ headers: {
+ 'GitLab-Agent-Id': 2,
+ },
+ withCredentials: true,
+ },
+ };
+
+ await waitForPromises();
+
+ expect(getNamespacesQueryResult).toHaveBeenCalledWith(
+ {},
+ { configuration },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+
+ it('sets the loading prop while fetching the list', async () => {
+ expect(findNamespaceSelector().props('loading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findNamespaceSelector().props('loading')).toBe(false);
+ });
+
+ it('renders a list of available namespaces', async () => {
+ await waitForPromises();
+
+ expect(findNamespaceSelector().props('items')).toEqual([
+ { text: 'default', value: 'default' },
+ { text: 'agent', value: 'agent' },
+ ]);
+ });
+
+ it('filters the namespaces list on user search', async () => {
+ await waitForPromises();
+ await findNamespaceSelector().vm.$emit('search', 'default');
+
+ expect(findNamespaceSelector().props('items')).toEqual([
+ { value: 'default', text: 'default' },
+ ]);
+ });
+
+ it('updates namespace selector field with the name of selected namespace', async () => {
+ await waitForPromises();
+ await findNamespaceSelector().vm.$emit('select', 'agent');
+
+ expect(findNamespaceSelector().props('toggleText')).toBe('agent');
+ });
+
+ it('emits changes to the kubernetesNamespace', async () => {
+ await waitForPromises();
+ await findNamespaceSelector().vm.$emit('select', 'agent');
+
+ expect(wrapper.emitted('change')[1]).toEqual([
+ { name: '', externalUrl: '', kubernetesNamespace: 'agent' },
+ ]);
+ });
+
+ it('clears namespace selector when another agent was selected', async () => {
+ await waitForPromises();
+ await findNamespaceSelector().vm.$emit('select', 'agent');
+
+ expect(findNamespaceSelector().props('toggleText')).toBe('agent');
+
+ await findAgentSelector().vm.$emit('select', '1');
+ expect(findNamespaceSelector().props('toggleText')).toBe(
+ EnvironmentForm.i18n.namespaceHelpText,
+ );
+ });
+ });
+
+ describe('when cannot connect to the cluster', () => {
+ const error = new Error('Error from the cluster_client API');
+
+ beforeEach(async () => {
+ wrapper = createWrapperWithApollo({
+ kubernetesNamespaceForEnvironment: true,
+ queryResult: jest.fn().mockRejectedValueOnce(error),
+ });
+
+ await selectAgent();
+ await waitForPromises();
+ });
+
+ it("doesn't render the namespace selector", () => {
+ expect(findNamespaceSelector().exists()).toBe(false);
+ });
+
+ it('renders an alert', () => {
+ expect(findAlert().text()).toBe('Error from the cluster_client API');
+ });
+ });
+ });
+ });
+
describe('when environment has an associated agent', () => {
const environmentWithAgent = {
...DEFAULT_PROPS.environment,
@@ -280,11 +430,46 @@ describe('~/environments/components/form.vue', () => {
beforeEach(() => {
wrapper = createWrapperWithApollo({
propsData: { environment: environmentWithAgent },
+ kubernetesNamespaceForEnvironment: true,
});
});
it('updates agent selector field with the name of the associated agent', () => {
expect(findAgentSelector().props('toggleText')).toBe('agent-1');
});
+
+ it('renders namespace selector', async () => {
+ await waitForPromises();
+ expect(findNamespaceSelector().exists()).toBe(true);
+ });
+
+ it('renders a list of available namespaces', async () => {
+ await waitForPromises();
+
+ expect(findNamespaceSelector().props('items')).toEqual([
+ { text: 'default', value: 'default' },
+ { text: 'agent', value: 'agent' },
+ ]);
+ });
+ });
+
+ describe('when environment has an associated kubernetes namespace', () => {
+ const environmentWithAgentAndNamespace = {
+ ...DEFAULT_PROPS.environment,
+ clusterAgent: { id: '1', name: 'agent-1' },
+ clusterAgentId: '1',
+ kubernetesNamespace: 'default',
+ };
+ beforeEach(() => {
+ wrapper = createWrapperWithApollo({
+ propsData: { environment: environmentWithAgentAndNamespace },
+ kubernetesNamespaceForEnvironment: true,
+ });
+ });
+
+ it('updates namespace selector with the name of the associated namespace', async () => {
+ await waitForPromises();
+ expect(findNamespaceSelector().props('toggleText')).toBe('default');
+ });
});
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index 91268ade1e9..c2eafa5f51e 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -909,3 +909,8 @@ export const k8sWorkloadsMock = {
JobList: [completedJob, completedJob, failedJob],
CronJobList: [completedCronJob, suspendedCronJob, failedCronJob],
};
+
+export const k8sNamespacesMock = [
+ { metadata: { name: 'default' } },
+ { metadata: { name: 'agent' } },
+];
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index edffc00e185..be210ed619e 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -12,6 +12,7 @@ import pollIntervalQuery from '~/environments/graphql/queries/poll_interval.quer
import isEnvironmentStoppingQuery from '~/environments/graphql/queries/is_environment_stopping.query.graphql';
import pageInfoQuery from '~/environments/graphql/queries/page_info.query.graphql';
import { TEST_HOST } from 'helpers/test_constants';
+import { CLUSTER_AGENT_ERROR_MESSAGES } from '~/environments/constants';
import {
environmentsApp,
resolvedEnvironmentsApp,
@@ -20,6 +21,7 @@ import {
resolvedFolder,
k8sPodsMock,
k8sServicesMock,
+ k8sNamespacesMock,
} from './mock_data';
const ENDPOINT = `${TEST_HOST}/environments`;
@@ -319,6 +321,50 @@ describe('~/frontend/environments/graphql/resolvers', () => {
);
});
});
+ describe('k8sNamespaces', () => {
+ const mockNamespacesListFn = jest.fn().mockImplementation(() => {
+ return Promise.resolve({
+ data: {
+ items: k8sNamespacesMock,
+ },
+ });
+ });
+
+ beforeEach(() => {
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1Namespace')
+ .mockImplementation(mockNamespacesListFn);
+ });
+
+ it('should request all namespaces from the cluster_client library', async () => {
+ const namespaces = await mockResolvers.Query.k8sNamespaces(null, { configuration });
+
+ expect(mockNamespacesListFn).toHaveBeenCalled();
+
+ expect(namespaces).toEqual(k8sNamespacesMock);
+ });
+ it.each([
+ ['Unauthorized', CLUSTER_AGENT_ERROR_MESSAGES.unauthorized],
+ ['Forbidden', CLUSTER_AGENT_ERROR_MESSAGES.forbidden],
+ ['Not found', CLUSTER_AGENT_ERROR_MESSAGES['not found']],
+ ['Unknown', CLUSTER_AGENT_ERROR_MESSAGES.other],
+ ])(
+ 'should throw an error if the API call fails with the reason "%s"',
+ async (reason, message) => {
+ jest.spyOn(CoreV1Api.prototype, 'listCoreV1Namespace').mockRejectedValue({
+ response: {
+ data: {
+ reason,
+ },
+ },
+ });
+
+ await expect(mockResolvers.Query.k8sNamespaces(null, { configuration })).rejects.toThrow(
+ message,
+ );
+ },
+ );
+ });
describe('stopEnvironmentREST', () => {
it('should post to the stop environment path', async () => {
mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
diff --git a/spec/frontend/environments/new_environment_item_spec.js b/spec/frontend/environments/new_environment_item_spec.js
index eb6990ba8a8..387bc31c9aa 100644
--- a/spec/frontend/environments/new_environment_item_spec.js
+++ b/spec/frontend/environments/new_environment_item_spec.js
@@ -13,6 +13,7 @@ import Deployment from '~/environments/components/deployment.vue';
import DeployBoardWrapper from '~/environments/components/deploy_board_wrapper.vue';
import KubernetesOverview from '~/environments/components/kubernetes_overview.vue';
import getEnvironmentClusterAgent from '~/environments/graphql/queries/environment_cluster_agent.query.graphql';
+import getEnvironmentClusterAgentWithNamespace from '~/environments/graphql/queries/environment_cluster_agent_with_namespace.query.graphql';
import { resolvedEnvironment, rolloutStatus, agent } from './graphql/mock_data';
import { mockKasTunnelUrl } from './mock_data';
@@ -21,6 +22,7 @@ Vue.use(VueApollo);
describe('~/environments/components/new_environment_item.vue', () => {
let wrapper;
let queryResponseHandler;
+ let queryWithNamespaceResponseHandler;
const projectPath = '/1';
@@ -37,7 +39,21 @@ describe('~/environments/components/new_environment_item.vue', () => {
},
};
queryResponseHandler = jest.fn().mockResolvedValue(response);
- return createMockApollo([[getEnvironmentClusterAgent, queryResponseHandler]]);
+ queryWithNamespaceResponseHandler = jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: response.data.project.id,
+ environment: {
+ ...response.data.project.environment,
+ kubernetesNamespace: 'default',
+ },
+ },
+ },
+ });
+ return createMockApollo([
+ [getEnvironmentClusterAgent, queryResponseHandler],
+ [getEnvironmentClusterAgentWithNamespace, queryWithNamespaceResponseHandler],
+ ]);
};
const createWrapper = ({ propsData = {}, provideData = {}, apolloProvider } = {}) =>
@@ -521,11 +537,6 @@ describe('~/environments/components/new_environment_item.vue', () => {
it('should request agent data when the environment is visible if the feature flag is enabled', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
- provideData: {
- glFeatures: {
- kasUserAccessProject: true,
- },
- },
apolloProvider: createApolloProvider(agent),
});
@@ -537,45 +548,62 @@ describe('~/environments/components/new_environment_item.vue', () => {
});
});
- it('should render if the feature flag is enabled and the environment has an agent associated', async () => {
+ it('should request agent data with kubernetes namespace when `kubernetesNamespaceForEnvironment` feature flag is enabled', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
provideData: {
glFeatures: {
- kasUserAccessProject: true,
+ kubernetesNamespaceForEnvironment: true,
},
},
apolloProvider: createApolloProvider(agent),
});
await expandCollapsedSection();
- await waitForPromises();
- expect(findKubernetesOverview().props()).toMatchObject({
- clusterAgent: agent,
+ expect(queryWithNamespaceResponseHandler).toHaveBeenCalledWith({
+ environmentName: resolvedEnvironment.name,
+ projectFullPath: projectPath,
});
});
- it('should not render if the feature flag is not enabled', async () => {
+ it('should render if the environment has an agent associated', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
apolloProvider: createApolloProvider(agent),
});
await expandCollapsedSection();
+ await waitForPromises();
- expect(queryResponseHandler).not.toHaveBeenCalled();
- expect(findKubernetesOverview().exists()).toBe(false);
+ expect(findKubernetesOverview().props()).toMatchObject({
+ clusterAgent: agent,
+ });
});
- it('should not render if the environment has no agent object', async () => {
+ it('should render with the namespace if `kubernetesNamespaceForEnvironment` feature flag is enabled and the environment has an agent associated', async () => {
wrapper = createWrapper({
propsData: { environment: resolvedEnvironment },
provideData: {
glFeatures: {
- kasUserAccessProject: true,
+ kubernetesNamespaceForEnvironment: true,
},
},
+ apolloProvider: createApolloProvider(agent),
+ });
+
+ await expandCollapsedSection();
+ await waitForPromises();
+
+ expect(findKubernetesOverview().props()).toMatchObject({
+ clusterAgent: agent,
+ namespace: 'default',
+ });
+ });
+
+ it('should not render if the environment has no agent object', async () => {
+ wrapper = createWrapper({
+ propsData: { environment: resolvedEnvironment },
apolloProvider: createApolloProvider(),
});
diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js
index 749e4e5caa4..30cd9265d0d 100644
--- a/spec/frontend/environments/new_environment_spec.js
+++ b/spec/frontend/environments/new_environment_spec.js
@@ -1,5 +1,4 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -7,8 +6,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import NewEnvironment from '~/environments/components/new_environment.vue';
import createEnvironment from '~/environments/graphql/mutations/create_environment.mutation.graphql';
import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import { __ } from '~/locale';
import createMockApollo from '../__helpers__/mock_apollo_helper';
@@ -16,9 +13,6 @@ import createMockApollo from '../__helpers__/mock_apollo_helper';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/alert');
-const newName = 'test';
-const newExternalUrl = 'https://google.ca';
-
const provide = {
projectEnvironmentsPath: '/projects/environments',
projectPath: '/path/to/project',
@@ -32,7 +26,6 @@ const environmentCreateError = {
describe('~/environments/components/new.vue', () => {
let wrapper;
- let mock;
const createMockApolloProvider = (mutationResult) => {
Vue.use(VueApollo);
@@ -47,29 +40,13 @@ describe('~/environments/components/new.vue', () => {
const createWrapperWithApollo = async (mutationResult = environmentCreate) => {
wrapper = mountExtended(NewEnvironment, {
- provide: {
- ...provide,
- glFeatures: {
- environmentSettingsToGraphql: true,
- },
- },
+ provide,
apolloProvider: createMockApolloProvider(mutationResult),
});
await waitForPromises();
};
- const createWrapperWithAxios = () => {
- wrapper = mountExtended(NewEnvironment, {
- provide: {
- ...provide,
- glFeatures: {
- environmentSettingsToGraphql: false,
- },
- },
- });
- };
-
const findNameInput = () => wrapper.findByLabelText(__('Name'));
const findExternalUrlInput = () => wrapper.findByLabelText(__('External URL'));
const findForm = () => wrapper.findByRole('form', { name: __('New environment') });
@@ -84,7 +61,7 @@ describe('~/environments/components/new.vue', () => {
describe('default', () => {
beforeEach(() => {
- createWrapperWithAxios();
+ createWrapperWithApollo();
});
it('sets the title to New environment', () => {
@@ -103,93 +80,36 @@ describe('~/environments/components/new.vue', () => {
});
});
- describe('when environmentSettingsToGraphql feature is enabled', () => {
- describe('when mutation successful', () => {
- beforeEach(() => {
- createWrapperWithApollo();
- });
-
- it('shows loader after form is submitted', async () => {
- expect(showsLoading()).toBe(false);
-
- await submitForm();
-
- expect(showsLoading()).toBe(true);
- });
-
- it('submits the new environment on submit', async () => {
- submitForm();
- await waitForPromises();
-
- expect(visitUrl).toHaveBeenCalledWith('path/to/environment');
- });
- });
-
- describe('when failed', () => {
- beforeEach(async () => {
- createWrapperWithApollo(environmentCreateError);
- submitForm();
- await waitForPromises();
- });
-
- it('shows errors on error', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' });
- expect(showsLoading()).toBe(false);
- });
- });
- });
-
- describe('when environmentSettingsToGraphql feature is disabled', () => {
+ describe('when mutation successful', () => {
beforeEach(() => {
- mock = new MockAdapter(axios);
- createWrapperWithAxios();
- });
-
- afterEach(() => {
- mock.restore();
+ createWrapperWithApollo();
});
it('shows loader after form is submitted', async () => {
expect(showsLoading()).toBe(false);
- mock
- .onPost(provide.projectEnvironmentsPath, {
- name: newName,
- external_url: newExternalUrl,
- })
- .reply(HTTP_STATUS_OK, { path: '/test' });
-
await submitForm();
expect(showsLoading()).toBe(true);
});
it('submits the new environment on submit', async () => {
- mock
- .onPost(provide.projectEnvironmentsPath, {
- name: newName,
- external_url: newExternalUrl,
- })
- .reply(HTTP_STATUS_OK, { path: '/test' });
-
- await submitForm();
+ submitForm();
await waitForPromises();
- expect(visitUrl).toHaveBeenCalledWith('/test');
+ expect(visitUrl).toHaveBeenCalledWith('path/to/environment');
});
+ });
- it('shows errors on error', async () => {
- mock
- .onPost(provide.projectEnvironmentsPath, {
- name: newName,
- external_url: newExternalUrl,
- })
- .reply(HTTP_STATUS_BAD_REQUEST, { message: ['name taken'] });
-
- await submitForm();
+ describe('when failed', () => {
+ beforeEach(async () => {
+ createWrapperWithApollo(environmentCreateError);
+ submitForm();
await waitForPromises();
+ });
- expect(createAlert).toHaveBeenCalledWith({ message: 'name taken' });
+ it('display errors', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' });
expect(showsLoading()).toBe(false);
});
});
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index c9238c4b636..6ef34504da7 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -6,6 +6,9 @@ import {
GlFormInput,
GlAlert,
GlSprintf,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdownGroup,
} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -46,7 +49,13 @@ describe('ErrorDetails', () => {
function mountComponent({ integratedErrorTrackingEnabled = false } = {}) {
wrapper = shallowMount(ErrorDetails, {
- stubs: { GlButton, GlSprintf },
+ stubs: {
+ GlButton,
+ GlSprintf,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdownGroup,
+ },
store,
mocks,
propsData: {
diff --git a/spec/frontend/fixtures/groups.rb b/spec/frontend/fixtures/groups.rb
index 9c22ff176ff..e69287c879b 100644
--- a/spec/frontend/fixtures/groups.rb
+++ b/spec/frontend/fixtures/groups.rb
@@ -2,24 +2,39 @@
require 'spec_helper'
-RSpec.describe 'Groups (JavaScript fixtures)', type: :controller do
+RSpec.describe 'Groups (JavaScript fixtures)', feature_category: :groups_and_projects do
+ include ApiHelpers
include JavaScriptFixturesHelpers
- let(:user) { create(:user) }
- let(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre') }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:group) { create(:group, name: 'frontend-fixtures-group', runners_token: 'runnerstoken:intabulasreferre') }
+ let_it_be(:projects) { create_list(:project, 2, namespace: group) }
- before do
- group.add_owner(user)
- sign_in(user)
- end
+ describe GroupsController, '(JavaScript fixtures)', type: :controller do
+ render_views
- render_views
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
- describe GroupsController, '(JavaScript fixtures)', type: :controller do
it 'groups/edit.html' do
get :edit, params: { id: group }
expect(response).to be_successful
end
end
+
+ describe API::Groups, '(JavaScript fixtures)', type: :request do
+ before do
+ group.add_owner(user)
+ sign_in(user)
+ end
+
+ it 'api/groups/projects/get.json' do
+ get api("/groups/#{group.id}/projects", user)
+
+ expect(response).to be_successful
+ end
+ end
end
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index e85e683b599..73594ddf686 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -105,7 +105,6 @@ RSpec.describe GraphQL::Query, type: :request do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
- let_it_be(:issue_type) { 'issue' }
before_all do
project.add_reporter(user)
@@ -128,8 +127,7 @@ RSpec.describe GraphQL::Query, type: :request do
title: '15.2',
start_date: Date.new(2020, 7, 1),
due_date: Date.new(2020, 7, 30)
- ),
- issue_type: issue_type
+ )
)
post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: issue.iid.to_s })
diff --git a/spec/frontend/fixtures/metrics_dashboard.rb b/spec/frontend/fixtures/metrics_dashboard.rb
deleted file mode 100644
index 036ce9eea3a..00000000000
--- a/spec/frontend/fixtures/metrics_dashboard.rb
+++ /dev/null
@@ -1,42 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe MetricsDashboard, '(JavaScript fixtures)', type: :controller do
- include JavaScriptFixturesHelpers
- include MetricsDashboardHelpers
-
- let_it_be(:user) { create(:user) }
- 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 } }
-
- controller(::ApplicationController) do
- include MetricsDashboard
- end
-
- before do
- stub_feature_flags(remove_monitor_metrics: false)
- sign_in(user)
- project.add_maintainer(user)
-
- allow(controller).to receive(:project).and_return(project)
- allow(controller).to receive(:environment).and_return(environment)
- allow(controller)
- .to receive(:metrics_dashboard_params)
- .and_return(params)
- end
-
- after do
- remove_repository(project)
- end
-
- it 'metrics_dashboard/environment_metrics_dashboard.json' do
- routes.draw { get "metrics_dashboard" => "anonymous#metrics_dashboard" }
-
- response = get :metrics_dashboard, format: :json
-
- expect(response).to be_successful
- end
-end
diff --git a/spec/frontend/fixtures/milestones.rb b/spec/frontend/fixtures/milestones.rb
deleted file mode 100644
index 5e39dcf190a..00000000000
--- a/spec/frontend/fixtures/milestones.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-# frozen_string_literal: true
-
-require 'spec_helper'
-
-RSpec.describe Projects::MilestonesController, '(JavaScript fixtures)', :with_license, feature_category: :team_planning, type: :controller do
- include JavaScriptFixturesHelpers
-
- let_it_be(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
- let_it_be(:namespace) { create(:namespace, name: 'frontend-fixtures') }
- let_it_be(:project) { create(:project_empty_repo, namespace: namespace, path: 'milestones-project') }
-
- render_views
-
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- after do
- remove_repository(project)
- end
-
- it 'milestones/new-milestone.html' do
- get :new, params: {
- namespace_id: project.namespace.to_param,
- project_id: project
- }
-
- expect(response).to be_successful
- end
-
- private
-
- def render_milestone(milestone)
- get :show, params: {
- namespace_id: project.namespace.to_param,
- project_id: project,
- id: milestone.to_param
- }
-
- expect(response).to be_successful
- end
-end
diff --git a/spec/frontend/fixtures/pipeline_schedules.rb b/spec/frontend/fixtures/pipeline_schedules.rb
index 3bfe9113e83..7bba7910b87 100644
--- a/spec/frontend/fixtures/pipeline_schedules.rb
+++ b/spec/frontend/fixtures/pipeline_schedules.rb
@@ -63,6 +63,12 @@ RSpec.describe 'Pipeline schedules (JavaScript fixtures)' do
expect_graphql_errors_to_be_empty
end
+ it "#{fixtures_path}#{get_pipeline_schedules_query}.single.json" do
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path, ids: pipeline_schedule_populated.id })
+
+ expect_graphql_errors_to_be_empty
+ end
+
it "#{fixtures_path}#{get_pipeline_schedules_query}.as_guest.json" do
guest = create(:user)
project.add_guest(user)
diff --git a/spec/frontend/fixtures/static/line_highlighter.html b/spec/frontend/fixtures/static/line_highlighter.html
index 1667097bc3b..4e1795dfcfa 100644
--- a/spec/frontend/fixtures/static/line_highlighter.html
+++ b/spec/frontend/fixtures/static/line_highlighter.html
@@ -1,154 +1,79 @@
<div class="file-holder">
<div class="file-content">
<div class="line-numbers">
-<a data-line-number="1" href="#L1" id="L1">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
+<a data-line-number="1" href="#L1" id="L1">
1
</a>
-<a data-line-number="2" href="#L2" id="L2">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
+<a data-line-number="2" href="#L2" id="L2">
2
</a>
-<a data-line-number="3" href="#L3" id="L3">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
+<a data-line-number="3" href="#L3" id="L3">
3
</a>
-<a data-line-number="4" href="#L4" id="L4">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
+<a data-line-number="4" href="#L4" id="L4">
4
</a>
-<a data-line-number="5" href="#L5" id="L5">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
+<a data-line-number="5" href="#L5" id="L5">
5
</a>
<a data-line-number="6" href="#L6" id="L6">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
6
</a>
<a data-line-number="7" href="#L7" id="L7">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
7
</a>
<a data-line-number="8" href="#L8" id="L8">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
8
</a>
<a data-line-number="9" href="#L9" id="L9">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
9
</a>
<a data-line-number="10" href="#L10" id="L10">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
10
</a>
<a data-line-number="11" href="#L11" id="L11">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
11
</a>
<a data-line-number="12" href="#L12" id="L12">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
12
</a>
<a data-line-number="13" href="#L13" id="L13">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
13
</a>
<a data-line-number="14" href="#L14" id="L14">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
14
</a>
<a data-line-number="15" href="#L15" id="L15">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
15
</a>
<a data-line-number="16" href="#L16" id="L16">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
16
</a>
<a data-line-number="17" href="#L17" id="L17">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
17
</a>
<a data-line-number="18" href="#L18" id="L18">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
18
</a>
<a data-line-number="19" href="#L19" id="L19">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
19
</a>
<a data-line-number="20" href="#L20" id="L20">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
20
</a>
<a data-line-number="21" href="#L21" id="L21">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
21
</a>
<a data-line-number="22" href="#L22" id="L22">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
22
</a>
<a data-line-number="23" href="#L23" id="L23">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
23
</a>
<a data-line-number="24" href="#L24" id="L24">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
24
</a>
<a data-line-number="25" href="#L25" id="L25">
-<svg data-testid="link-icon" class="s12">
-<use href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#link"></use>
-</svg>
25
</a>
</div>
diff --git a/spec/frontend/fixtures/static/textarea.html b/spec/frontend/fixtures/static/textarea.html
new file mode 100644
index 00000000000..68d5a0f2d4d
--- /dev/null
+++ b/spec/frontend/fixtures/static/textarea.html
@@ -0,0 +1,27 @@
+<body>
+<meta charset="utf-8">
+<title>Document with Textarea</title>
+<form class="milestone-form common-note-form js-quick-submit js-requires-input" id="new_milestone"
+ action="http://test.host/frontend-fixtures/milestones-project/-/milestones"
+ accept-charset="UTF-8" method="post">
+ <div class="form-group">
+ <div class="md-write-holder">
+ <div class="zen-backdrop">
+ <textarea class="note-textarea js-gfm-input js-autosize markdown-area"
+ placeholder="Write milestone description..." dir="auto"
+ data-supports-quick-actions="false" data-supports-autocomplete="true"
+ data-qa-selector="milestone_description_field" data-autofocus="false"
+ name="milestone[description]"
+ id="milestone_description"></textarea>
+ <a class="zen-control zen-control-leave js-zen-leave gl-text-gray-500"
+ href="#">
+ <svg class="s16" data-testid="minimize-icon">
+ <use href="http://test.host/assets/icons-b8c5a9711f73b1de3c81754da0aca72f43b0e6844aa06dd03092b601a493f45b.svg#minimize"></use>
+ </svg>
+ </a>
+ </div>
+ </div>
+ </div>
+</form>
+
+</body>
diff --git a/spec/frontend/fixtures/timezones.rb b/spec/frontend/fixtures/timezones.rb
index 2393f4e797d..f04e647c8eb 100644
--- a/spec/frontend/fixtures/timezones.rb
+++ b/spec/frontend/fixtures/timezones.rb
@@ -4,7 +4,7 @@ require 'spec_helper'
RSpec.describe TimeZoneHelper, '(JavaScript fixtures)' do
include JavaScriptFixturesHelpers
- include TimeZoneHelper
+ include described_class
let(:response) { @timezones.sort_by! { |tz| tz[:name] }.to_json }
diff --git a/spec/frontend/fixtures/users.rb b/spec/frontend/fixtures/users.rb
index 89bffea7e4c..800a9af194e 100644
--- a/spec/frontend/fixtures/users.rb
+++ b/spec/frontend/fixtures/users.rb
@@ -7,7 +7,8 @@ RSpec.describe 'Users (JavaScript fixtures)', feature_category: :user_profile do
include ApiHelpers
let_it_be(:followers) { create_list(:user, 5) }
- let_it_be(:user) { create(:user, followers: followers) }
+ let_it_be(:followees) { create_list(:user, 5) }
+ let_it_be(:user) { create(:user, followers: followers, followees: followees) }
describe API::Users, '(JavaScript fixtures)', type: :request do
it 'api/users/followers/get.json' do
@@ -15,6 +16,12 @@ RSpec.describe 'Users (JavaScript fixtures)', feature_category: :user_profile do
expect(response).to be_successful
end
+
+ it 'api/users/following/get.json' do
+ get api("/users/#{user.id}/following", user)
+
+ expect(response).to be_successful
+ end
end
describe UsersController, '(JavaScript fixtures)', type: :controller do
diff --git a/spec/frontend/frequent_items/mock_data.js b/spec/frontend/frequent_items/mock_data.js
index 5e15b4b33e0..6563daee6c3 100644
--- a/spec/frontend/frequent_items/mock_data.js
+++ b/spec/frontend/frequent_items/mock_data.js
@@ -69,7 +69,7 @@ export const mockFrequentGroups = [
},
];
-export const mockSearchedGroups = [mockRawGroup];
+export const mockSearchedGroups = { data: [mockRawGroup] };
export const mockProcessedSearchedGroups = [mockGroup];
export const mockProject = {
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 73284fbe5e5..2d19c9871b6 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -866,7 +866,7 @@ describe('GfmAutoComplete', () => {
it('should return a correct template', () => {
const actual = GfmAutoComplete.Emoji.templateFunction(mockItem);
const glEmojiTag = `<gl-emoji data-name="${mockItem.emoji.name}"></gl-emoji>`;
- const expected = `<li>${mockItem.fieldValue} ${glEmojiTag}</li>`;
+ const expected = `<li>${glEmojiTag} ${mockItem.fieldValue}</li>`;
expect(actual).toBe(expected);
});
diff --git a/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js
index f1ed32a5f79..b1a1d2d1372 100644
--- a/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js
+++ b/spec/frontend/gitlab_version_check/components/security_patch_upgrade_alert_modal_spec.js
@@ -1,6 +1,7 @@
import { GlModal, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
import { sprintf } from '~/locale';
import SecurityPatchUpgradeAlertModal from '~/gitlab_version_check/components/security_patch_upgrade_alert_modal.vue';
import * as utils from '~/gitlab_version_check/utils';
@@ -14,6 +15,8 @@ import {
describe('SecurityPatchUpgradeAlertModal', () => {
let wrapper;
let trackingSpy;
+ const hideMock = jest.fn();
+ const { i18n } = SecurityPatchUpgradeAlertModal;
const defaultProps = {
currentVersion: '11.1.1',
@@ -28,14 +31,20 @@ describe('SecurityPatchUpgradeAlertModal', () => {
...props,
},
stubs: {
- GlModal,
GlSprintf,
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ hide: hideMock,
+ },
+ template: RENDER_ALL_SLOTS_TEMPLATE,
+ }),
},
});
};
afterEach(() => {
unmockTracking();
+ hideMock.mockClear();
});
const expectDispatchedTracking = (action, label) => {
@@ -63,12 +72,12 @@ describe('SecurityPatchUpgradeAlertModal', () => {
});
it('renders the modal title correctly', () => {
- expect(findGlModalTitle().text()).toBe(wrapper.vm.$options.i18n.modalTitle);
+ expect(findGlModalTitle().text()).toBe(i18n.modalTitle);
});
it('renders modal body without suggested versions', () => {
expect(findGlModalBody().text()).toBe(
- sprintf(wrapper.vm.$options.i18n.modalBodyNoStableVersions, {
+ sprintf(i18n.modalBodyNoStableVersions, {
currentVersion: defaultProps.currentVersion,
}),
);
@@ -90,7 +99,7 @@ describe('SecurityPatchUpgradeAlertModal', () => {
describe('Learn more link', () => {
it('renders with correct text and link', () => {
- expect(findGlLink().text()).toBe(wrapper.vm.$options.i18n.learnMore);
+ expect(findGlLink().text()).toBe(i18n.learnMore);
expect(findGlLink().attributes('href')).toBe(ABOUT_RELEASES_PAGE);
});
@@ -102,12 +111,8 @@ describe('SecurityPatchUpgradeAlertModal', () => {
});
describe('Remind me button', () => {
- beforeEach(() => {
- wrapper.vm.$refs.alertModal.hide = jest.fn();
- });
-
it('renders with correct text', () => {
- expect(findGlRemindButton().text()).toBe(wrapper.vm.$options.i18n.secondaryButtonText);
+ expect(findGlRemindButton().text()).toBe(i18n.secondaryButtonText);
});
it(`tracks click ${TRACKING_LABELS.REMIND_ME_BTN} when clicked`, async () => {
@@ -126,13 +131,13 @@ describe('SecurityPatchUpgradeAlertModal', () => {
it('hides the modal', async () => {
await findGlRemindButton().vm.$emit('click');
- expect(wrapper.vm.$refs.alertModal.hide).toHaveBeenCalled();
+ expect(hideMock).toHaveBeenCalled();
});
});
describe('Upgrade button', () => {
it('renders with correct text and link', () => {
- expect(findGlUpgradeButton().text()).toBe(wrapper.vm.$options.i18n.primaryButtonText);
+ expect(findGlUpgradeButton().text()).toBe(i18n.primaryButtonText);
expect(findGlUpgradeButton().attributes('href')).toBe(UPGRADE_DOCS_URL);
});
@@ -160,7 +165,7 @@ describe('SecurityPatchUpgradeAlertModal', () => {
it('renders modal body with suggested versions', () => {
expect(findGlModalBody().text()).toBe(
- sprintf(wrapper.vm.$options.i18n.modalBodyStableVersions, {
+ sprintf(i18n.modalBodyStableVersions, {
currentVersion: defaultProps.currentVersion,
latestStableVersions: latestStableVersions.join(', '),
}),
@@ -176,9 +181,7 @@ describe('SecurityPatchUpgradeAlertModal', () => {
});
it('renders modal details', () => {
- expect(findGlModalDetails().text()).toBe(
- sprintf(wrapper.vm.$options.i18n.modalDetails, { details }),
- );
+ expect(findGlModalDetails().text()).toBe(sprintf(i18n.modalDetails, { details }));
});
});
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index b474745790e..e32c50db8bf 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -93,10 +93,9 @@ describe('AppComponent', () => {
page: 2,
filterGroupsBy: 'git',
sortBy: 'created_desc',
- archived: true,
})
.then(() => {
- expect(getGroupsSpy).toHaveBeenCalledWith(1, 2, 'git', 'created_desc', true);
+ expect(getGroupsSpy).toHaveBeenCalledWith(1, 2, 'git', 'created_desc');
});
});
@@ -154,7 +153,6 @@ describe('AppComponent', () => {
filterGroupsBy: 'foobar',
sortBy: null,
updatePagination: true,
- archived: null,
});
return fetchPromise.then(() => {
expect(vm.updateGroups).toHaveBeenCalledWith(mockSearchedGroups, true);
@@ -177,7 +175,6 @@ describe('AppComponent', () => {
page: 2,
filterGroupsBy: null,
sortBy: null,
- archived: true,
});
expect(vm.isLoading).toBe(true);
@@ -186,7 +183,6 @@ describe('AppComponent', () => {
filterGroupsBy: null,
sortBy: null,
updatePagination: true,
- archived: true,
});
return fetchPagePromise.then(() => {
@@ -471,7 +467,7 @@ describe('AppComponent', () => {
it('calls API with expected params', () => {
emitFetchFilteredAndSortedGroups();
- expect(getGroupsSpy).toHaveBeenCalledWith(undefined, undefined, search, sort, undefined);
+ expect(getGroupsSpy).toHaveBeenCalledWith(undefined, undefined, search, sort);
});
it('updates pagination', () => {
diff --git a/spec/frontend/groups/components/overview_tabs_spec.js b/spec/frontend/groups/components/overview_tabs_spec.js
index ca852f398d0..8db69295ac4 100644
--- a/spec/frontend/groups/components/overview_tabs_spec.js
+++ b/spec/frontend/groups/components/overview_tabs_spec.js
@@ -10,26 +10,29 @@ import SharedProjectsEmptyState from '~/groups/components/empty_states/shared_pr
import ArchivedProjectsEmptyState from '~/groups/components/empty_states/archived_projects_empty_state.vue';
import GroupsStore from '~/groups/store/groups_store';
import GroupsService from '~/groups/service/groups_service';
+import ArchivedProjectsService from '~/groups/service/archived_projects_service';
import { createRouter } from '~/groups/init_overview_tabs';
import eventHub from '~/groups/event_hub';
import {
ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
ACTIVE_TAB_SHARED,
ACTIVE_TAB_ARCHIVED,
- OVERVIEW_TABS_SORTING_ITEMS,
+ SORTING_ITEM_NAME,
+ SORTING_ITEM_UPDATED,
+ SORTING_ITEM_STARS,
} from '~/groups/constants';
import axios from '~/lib/utils/axios_utils';
import waitForPromises from 'helpers/wait_for_promises';
Vue.component('GroupFolder', GroupFolderComponent);
const router = createRouter();
-const [SORTING_ITEM_NAME, , SORTING_ITEM_UPDATED] = OVERVIEW_TABS_SORTING_ITEMS;
describe('OverviewTabs', () => {
let wrapper;
let axiosMock;
const defaultProvide = {
+ groupId: '1',
endpoints: {
subgroups_and_projects: '/groups/foobar/-/children.json',
shared: '/groups/foobar/-/shared_projects.json',
@@ -92,7 +95,10 @@ describe('OverviewTabs', () => {
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
store: new GroupsStore({ showSchemaMarkup: true }),
- service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS]),
+ service: new GroupsService(
+ defaultProvide.endpoints[ACTIVE_TAB_SUBGROUPS_AND_PROJECTS],
+ defaultProvide.initialSort,
+ ),
});
await waitForPromises();
@@ -115,7 +121,10 @@ describe('OverviewTabs', () => {
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_SHARED,
store: new GroupsStore(),
- service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_SHARED]),
+ service: new GroupsService(
+ defaultProvide.endpoints[ACTIVE_TAB_SHARED],
+ defaultProvide.initialSort,
+ ),
});
expect(tabPanel.vm.$attrs.lazy).toBe(false);
@@ -140,7 +149,7 @@ describe('OverviewTabs', () => {
expect(tabPanel.findComponent(GroupsApp).props()).toMatchObject({
action: ACTIVE_TAB_ARCHIVED,
store: new GroupsStore(),
- service: new GroupsService(defaultProvide.endpoints[ACTIVE_TAB_ARCHIVED]),
+ service: new ArchivedProjectsService(defaultProvide.groupId, defaultProvide.initialSort),
});
expect(tabPanel.vm.$attrs.lazy).toBe(false);
@@ -219,7 +228,7 @@ describe('OverviewTabs', () => {
it(`pushes expected route when ${tabToClick} tab is clicked`, async () => {
await findTab(tabToClick).trigger('click');
- expect(routerMock.push).toHaveBeenCalledWith(expectedRoute);
+ expect(routerMock.push).toHaveBeenCalledWith(expect.objectContaining(expectedRoute));
});
});
@@ -304,6 +313,52 @@ describe('OverviewTabs', () => {
sharedAssertions({ search: '', sort: SORTING_ITEM_UPDATED.asc });
});
+ describe('when tab is changed', () => {
+ describe('when selected sort is supported', () => {
+ beforeEach(async () => {
+ await createComponent({
+ route: {
+ name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ params: { group: 'foo/bar/baz' },
+ query: { sort: SORTING_ITEM_NAME.asc },
+ },
+ });
+ });
+
+ it('adds sort query string', async () => {
+ await findTab(OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED]).trigger('click');
+
+ expect(routerMock.push).toHaveBeenCalledWith(
+ expect.objectContaining({
+ query: { sort: SORTING_ITEM_NAME.asc },
+ }),
+ );
+ });
+ });
+
+ describe('when selected sort is not supported', () => {
+ beforeEach(async () => {
+ await createComponent({
+ route: {
+ name: ACTIVE_TAB_SUBGROUPS_AND_PROJECTS,
+ params: { group: 'foo/bar/baz' },
+ query: { sort: SORTING_ITEM_STARS.asc },
+ },
+ });
+ });
+
+ it('defaults to sorting by name', async () => {
+ await findTab(OverviewTabs.i18n[ACTIVE_TAB_ARCHIVED]).trigger('click');
+
+ expect(routerMock.push).toHaveBeenCalledWith(
+ expect.objectContaining({
+ query: { sort: SORTING_ITEM_NAME.asc },
+ }),
+ );
+ });
+ });
+ });
+
describe('when sort direction is changed', () => {
beforeEach(async () => {
await setup();
diff --git a/spec/frontend/groups/service/archived_projects_service_spec.js b/spec/frontend/groups/service/archived_projects_service_spec.js
new file mode 100644
index 00000000000..3aec9d57ee1
--- /dev/null
+++ b/spec/frontend/groups/service/archived_projects_service_spec.js
@@ -0,0 +1,90 @@
+import projects from 'test_fixtures/api/groups/projects/get.json';
+import ArchivedProjectsService from '~/groups/service/archived_projects_service';
+import Api from '~/api';
+
+jest.mock('~/api');
+
+describe('ArchivedProjectsService', () => {
+ const groupId = 1;
+ let service;
+
+ beforeEach(() => {
+ service = new ArchivedProjectsService(groupId, 'name_asc');
+ });
+
+ describe('getGroups', () => {
+ const headers = { 'x-next-page': '2', 'x-page': '1', 'x-per-page': '20' };
+ const page = 2;
+ const query = 'git';
+ const sort = 'created_asc';
+
+ beforeEach(() => {
+ Api.groupProjects.mockResolvedValueOnce({ data: projects, headers });
+ });
+
+ it('returns promise the resolves with formatted project', async () => {
+ await expect(service.getGroups(undefined, page, query, sort)).resolves.toEqual({
+ data: projects.map((project) => {
+ return {
+ id: project.id,
+ name: project.name,
+ full_name: project.name_with_namespace,
+ markdown_description: project.description_html,
+ visibility: project.visibility,
+ avatar_url: project.avatar_url,
+ relative_path: `/${project.path_with_namespace}`,
+ edit_path: null,
+ leave_path: null,
+ can_edit: false,
+ can_leave: false,
+ can_remove: false,
+ type: 'project',
+ permission: null,
+ children: [],
+ parent_id: project.namespace.id,
+ project_count: 0,
+ subgroup_count: 0,
+ number_users_with_delimiter: 0,
+ star_count: project.star_count,
+ updated_at: project.updated_at,
+ marked_for_deletion: project.marked_for_deletion_at !== null,
+ last_activity_at: project.last_activity_at,
+ };
+ }),
+ headers,
+ });
+
+ expect(Api.groupProjects).toHaveBeenCalledWith(groupId, query, {
+ archived: true,
+ page,
+ order_by: 'created_at',
+ sort: 'asc',
+ });
+ });
+
+ describe.each`
+ sortArgument | expectedOrderByParameter | expectedSortParameter
+ ${'name_asc'} | ${'name'} | ${'asc'}
+ ${'name_desc'} | ${'name'} | ${'desc'}
+ ${'created_asc'} | ${'created_at'} | ${'asc'}
+ ${'created_desc'} | ${'created_at'} | ${'desc'}
+ ${'latest_activity_asc'} | ${'last_activity_at'} | ${'asc'}
+ ${'latest_activity_desc'} | ${'last_activity_at'} | ${'desc'}
+ ${undefined} | ${'name'} | ${'asc'}
+ `(
+ 'when the sort argument is $sortArgument',
+ ({ sortArgument, expectedSortParameter, expectedOrderByParameter }) => {
+ it(`calls the API with sort parameter set to ${expectedSortParameter} and order_by parameter set to ${expectedOrderByParameter}`, () => {
+ service.getGroups(undefined, page, query, sortArgument);
+
+ expect(Api.groupProjects).toHaveBeenCalledWith(groupId, query, {
+ archived: true,
+ page,
+ order_by: expectedOrderByParameter,
+ sort: expectedSortParameter,
+ });
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/groups/service/groups_service_spec.js b/spec/frontend/groups/service/groups_service_spec.js
index e037a6df1e2..ef0a7fde70a 100644
--- a/spec/frontend/groups/service/groups_service_spec.js
+++ b/spec/frontend/groups/service/groups_service_spec.js
@@ -7,7 +7,7 @@ describe('GroupsService', () => {
let service;
beforeEach(() => {
- service = new GroupsService(mockEndpoint);
+ service = new GroupsService(mockEndpoint, 'created_asc');
});
describe('getGroups', () => {
@@ -17,17 +17,28 @@ describe('GroupsService', () => {
page: 2,
filter: 'git',
sort: 'created_asc',
- archived: true,
};
- service.getGroups(55, 2, 'git', 'created_asc', true);
+ service.getGroups(55, 2, 'git', 'created_asc');
expect(axios.get).toHaveBeenCalledWith(mockEndpoint, { params: { parent_id: 55 } });
- service.getGroups(null, 2, 'git', 'created_asc', true);
+ service.getGroups(null, 2, 'git', 'created_asc');
expect(axios.get).toHaveBeenCalledWith(mockEndpoint, { params });
});
+
+ describe('when sort argument is undefined', () => {
+ it('calls API with `initialSort` argument', () => {
+ jest.spyOn(axios, 'get').mockResolvedValue();
+
+ service.getGroups(undefined, 2, 'git', undefined);
+
+ expect(axios.get).toHaveBeenCalledWith(mockEndpoint, {
+ params: { sort: 'created_asc', filter: 'git', page: 2 },
+ });
+ });
+ });
});
describe('leaveGroup', () => {
diff --git a/spec/frontend/header_search/init_spec.js b/spec/frontend/header_search/init_spec.js
index baf3c6f08b2..459ca33ee66 100644
--- a/spec/frontend/header_search/init_spec.js
+++ b/spec/frontend/header_search/init_spec.js
@@ -5,7 +5,6 @@ import initHeaderSearch, { eventHandler, cleanEventListeners } from '~/header_se
describe('Header Search EventListener', () => {
beforeEach(() => {
jest.resetModules();
- jest.restoreAllMocks();
setHTMLFixture(`
<div class="js-header-content">
<div class="header-search-form" id="js-header-search" data-autocomplete-path="/search/autocomplete" data-issues-path="/dashboard/issues" data-mr-path="/dashboard/merge_requests" data-search-context="{}" data-search-path="/search">
@@ -16,7 +15,6 @@ describe('Header Search EventListener', () => {
afterEach(() => {
resetHTMLFixture();
- jest.clearAllMocks();
});
it('attached event listener', () => {
diff --git a/spec/frontend/ide/components/ide_status_bar_spec.js b/spec/frontend/ide/components/ide_status_bar_spec.js
index 0ee16f98e7e..fe392a64013 100644
--- a/spec/frontend/ide/components/ide_status_bar_spec.js
+++ b/spec/frontend/ide/components/ide_status_bar_spec.js
@@ -1,6 +1,6 @@
-import { mount } from '@vue/test-utils';
import _ from 'lodash';
import { TEST_HOST } from 'helpers/test_constants';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import IdeStatusBar from '~/ide/components/ide_status_bar.vue';
import IdeStatusMR from '~/ide/components/ide_status_mr.vue';
import { rightSidebarViews } from '~/ide/constants';
@@ -15,6 +15,8 @@ jest.mock('~/lib/utils/poll');
describe('IdeStatusBar component', () => {
let wrapper;
+ const dummyIntervalId = 1337;
+ let dispatchMock;
const findMRStatus = () => wrapper.findComponent(IdeStatusMR);
@@ -31,14 +33,21 @@ describe('IdeStatusBar component', () => {
...state,
});
- wrapper = mount(IdeStatusBar, { store });
+ wrapper = mountExtended(IdeStatusBar, { store });
+ dispatchMock = jest.spyOn(store, 'dispatch');
};
+ beforeEach(() => {
+ jest.spyOn(window, 'setInterval').mockReturnValue(dummyIntervalId);
+ });
+
+ const findCommitShaLink = () => wrapper.findByTestId('commit-sha-content');
+
describe('default', () => {
it('triggers a setInterval', () => {
mountComponent();
- expect(wrapper.vm.intervalId).not.toBe(null);
+ expect(window.setInterval).toHaveBeenCalledTimes(1);
});
it('renders the statusbar', () => {
@@ -47,34 +56,10 @@ describe('IdeStatusBar component', () => {
expect(wrapper.classes()).toEqual(['ide-status-bar']);
});
- describe('commitAgeUpdate', () => {
- beforeEach(() => {
- mountComponent();
- jest.spyOn(wrapper.vm, 'commitAgeUpdate').mockImplementation(() => {});
- });
-
- afterEach(() => {
- jest.clearAllTimers();
- });
-
- it('gets called every second', () => {
- expect(wrapper.vm.commitAgeUpdate).not.toHaveBeenCalled();
-
- jest.advanceTimersByTime(1000);
-
- expect(wrapper.vm.commitAgeUpdate.mock.calls).toHaveLength(1);
-
- jest.advanceTimersByTime(1000);
-
- expect(wrapper.vm.commitAgeUpdate.mock.calls).toHaveLength(2);
- });
- });
-
describe('getCommitPath', () => {
it('returns the path to the commit details', () => {
mountComponent();
-
- expect(wrapper.vm.getCommitPath('abc123de')).toBe('/commit/abc123de');
+ expect(findCommitShaLink().attributes('href')).toBe('/commit/abc123de');
});
});
@@ -95,11 +80,10 @@ describe('IdeStatusBar component', () => {
},
};
mountComponent({ pipelines });
- jest.spyOn(wrapper.vm, 'openRightPane').mockImplementation(() => {});
wrapper.find('button').trigger('click');
- expect(wrapper.vm.openRightPane).toHaveBeenCalledWith(rightSidebarViews.pipelines);
+ expect(dispatchMock).toHaveBeenCalledWith('rightPane/open', rightSidebarViews.pipelines);
});
});
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 6747ec97050..aa99b1cacef 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -158,7 +158,6 @@ describe('RepoEditor', () => {
});
afterEach(() => {
- jest.clearAllMocks();
// create a new model each time, otherwise tests conflict with each other
// because of same model being used in multiple tests
monacoEditor.getModels().forEach((model) => model.dispose());
diff --git a/spec/frontend/ide/mock_data.js b/spec/frontend/ide/mock_data.js
index 557626b3cca..b1f192e1d98 100644
--- a/spec/frontend/ide/mock_data.js
+++ b/spec/frontend/ide/mock_data.js
@@ -13,6 +13,7 @@ export const projectData = {
can_push: true,
commit: {
id: '123',
+ short_id: 'abc123de',
},
},
},
@@ -79,6 +80,7 @@ export const jobs = [
path: 'testing',
status: {
icon: 'status_success',
+ group: 'success',
text: 'passed',
},
stage: 'test',
diff --git a/spec/frontend/invite_members/components/group_select_spec.js b/spec/frontend/invite_members/components/group_select_spec.js
index a1ca9a69926..bd90832f497 100644
--- a/spec/frontend/invite_members/components/group_select_spec.js
+++ b/spec/frontend/invite_members/components/group_select_spec.js
@@ -1,82 +1,76 @@
-import { GlAvatarLabeled, GlDropdown, GlSearchBoxByType } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { GlAvatarLabeled, GlCollapsibleListbox } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import * as groupsApi from '~/api/groups_api';
+import { getGroups } from '~/api/groups_api';
import GroupSelect from '~/invite_members/components/group_select.vue';
+jest.mock('~/api/groups_api');
+
const group1 = { id: 1, full_name: 'Group One', avatar_url: 'test' };
const group2 = { id: 2, full_name: 'Group Two', avatar_url: 'test' };
const allGroups = [group1, group2];
-
-const createComponent = (props = {}) => {
- return mount(GroupSelect, {
- propsData: {
- invalidGroups: [],
- ...props,
- },
- });
+const headers = {
+ 'X-Next-Page': 2,
+ 'X-Page': 1,
+ 'X-Per-Page': 20,
+ 'X-Prev-Page': '',
+ 'X-Total': 40,
+ 'X-Total-Pages': 2,
};
describe('GroupSelect', () => {
let wrapper;
- beforeEach(() => {
- jest.spyOn(groupsApi, 'getGroups').mockResolvedValue(allGroups);
+ const createComponent = (props = {}) => {
+ wrapper = mount(GroupSelect, {
+ propsData: {
+ selectedGroup: {},
+ invalidGroups: [],
+ ...props,
+ },
+ });
+ };
- wrapper = createComponent();
+ beforeEach(() => {
+ getGroups.mockResolvedValueOnce({ data: allGroups, headers });
});
- const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownToggle = () => findDropdown().find('button[aria-haspopup="menu"]');
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findListboxToggle = () => findListbox().find('button[aria-haspopup="listbox"]');
const findAvatarByLabel = (text) =>
wrapper
.findAllComponents(GlAvatarLabeled)
.wrappers.find((dropdownItemWrapper) => dropdownItemWrapper.props('label') === text);
- it('renders GlSearchBoxByType with default attributes', () => {
- expect(findSearchBoxByType().exists()).toBe(true);
- expect(findSearchBoxByType().vm.$attrs).toMatchObject({
- placeholder: 'Search groups',
- });
- });
-
describe('when user types in the search input', () => {
- let resolveApiRequest;
-
- beforeEach(() => {
- jest.spyOn(groupsApi, 'getGroups').mockImplementation(
- () =>
- new Promise((resolve) => {
- resolveApiRequest = resolve;
- }),
- );
-
- findSearchBoxByType().vm.$emit('input', group1.name);
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ getGroups.mockClear();
+ getGroups.mockReturnValueOnce(new Promise(() => {}));
+ findListbox().vm.$emit('search', group1.name);
+ await nextTick();
});
it('calls the API', () => {
- resolveApiRequest({ data: allGroups });
-
- expect(groupsApi.getGroups).toHaveBeenCalledWith(group1.name, {
+ expect(getGroups).toHaveBeenCalledWith(group1.name, {
exclude_internal: true,
active: true,
order_by: 'similarity',
});
});
- it('displays loading icon while waiting for API call to resolve', async () => {
- expect(findSearchBoxByType().props('isLoading')).toBe(true);
-
- resolveApiRequest({ data: allGroups });
- await waitForPromises();
-
- expect(findSearchBoxByType().props('isLoading')).toBe(false);
+ it('displays loading icon while waiting for API call to resolve', () => {
+ expect(findListbox().props('searching')).toBe(true);
});
});
describe('avatar label', () => {
- it('includes the correct attributes with name and avatar_url', () => {
+ it('includes the correct attributes with name and avatar_url', async () => {
+ createComponent();
+ await waitForPromises();
+
expect(findAvatarByLabel(group1.full_name).attributes()).toMatchObject({
src: group1.avatar_url,
'entity-id': `${group1.id}`,
@@ -86,8 +80,9 @@ describe('GroupSelect', () => {
});
describe('when filtering out the group from results', () => {
- beforeEach(() => {
- wrapper = createComponent({ invalidGroups: [group1.id] });
+ beforeEach(async () => {
+ createComponent({ invalidGroups: [group1.id] });
+ await waitForPromises();
});
it('does not find an invalid group', () => {
@@ -101,16 +96,93 @@ describe('GroupSelect', () => {
});
describe('when group is selected from the dropdown', () => {
- beforeEach(() => {
- findAvatarByLabel(group1.full_name).trigger('click');
+ beforeEach(async () => {
+ createComponent({
+ selectedGroup: {
+ value: group1.id,
+ id: group1.id,
+ name: group1.full_name,
+ path: group1.path,
+ avatarUrl: group1.avatar_url,
+ },
+ });
+ await waitForPromises();
+ findListbox().vm.$emit('select', group1.id);
+ await nextTick();
});
it('emits `input` event used by `v-model`', () => {
- expect(wrapper.emitted('input')[0][0].id).toEqual(group1.id);
+ expect(wrapper.emitted('input')).toMatchObject([
+ [
+ {
+ value: group1.id,
+ id: group1.id,
+ name: group1.full_name,
+ path: group1.path,
+ avatarUrl: group1.avatar_url,
+ },
+ ],
+ ]);
});
it('sets dropdown toggle text to selected item', () => {
- expect(findDropdownToggle().text()).toBe(group1.full_name);
+ expect(findListboxToggle().text()).toBe(group1.full_name);
+ });
+ });
+
+ describe('infinite scroll', () => {
+ it('sets infinite scroll related props', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findListbox().props()).toMatchObject({
+ infiniteScroll: true,
+ infiniteScrollLoading: false,
+ totalItems: 40,
+ });
+ });
+
+ describe('when `bottom-reached` event is fired', () => {
+ it('indicates new groups are loading and adds them to the listbox', async () => {
+ createComponent();
+ await waitForPromises();
+
+ const infiniteScrollGroup = {
+ id: 3,
+ full_name: 'Infinite scroll group',
+ avatar_url: 'test',
+ };
+
+ getGroups.mockResolvedValueOnce({ data: [infiniteScrollGroup], headers });
+
+ findListbox().vm.$emit('bottom-reached');
+ await nextTick();
+
+ expect(findListbox().props('infiniteScrollLoading')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findListbox().props('items')[2]).toMatchObject({
+ value: infiniteScrollGroup.id,
+ id: infiniteScrollGroup.id,
+ name: infiniteScrollGroup.full_name,
+ avatarUrl: infiniteScrollGroup.avatar_url,
+ });
+ });
+
+ describe('when API request fails', () => {
+ it('emits `error` event', async () => {
+ createComponent();
+ await waitForPromises();
+
+ getGroups.mockRejectedValueOnce();
+
+ findListbox().vm.$emit('bottom-reached');
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[GroupSelect.i18n.errorMessage]]);
+ });
+ });
});
});
});
diff --git a/spec/frontend/invite_members/components/invite_groups_modal_spec.js b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
index 4f082145562..4136de75545 100644
--- a/spec/frontend/invite_members/components/invite_groups_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_groups_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlModal, GlSprintf } from '@gitlab/ui';
+import { GlModal, GlSprintf, GlAlert } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Api from '~/api';
@@ -24,6 +24,7 @@ jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
describe('InviteGroupsModal', () => {
let wrapper;
+ const mockToastShow = jest.fn();
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(InviteGroupsModal, {
@@ -39,9 +40,18 @@ describe('InviteGroupsModal', () => {
template: '<div><slot></slot><slot name="modal-footer"></slot></div>',
}),
},
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
});
};
+ afterEach(() => {
+ mockToastShow.mockClear();
+ });
+
const createInviteGroupToProjectWrapper = () => {
createComponent({ isProject: true });
};
@@ -133,7 +143,6 @@ describe('InviteGroupsModal', () => {
createComponent();
triggerGroupSelect(sharedGroup);
- wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'groupShareWithGroup').mockImplementation(
() =>
new Promise((resolve, reject) => {
@@ -167,7 +176,7 @@ describe('InviteGroupsModal', () => {
});
it('displays the successful toastMessage', () => {
- expect(wrapper.vm.$toast.show).toHaveBeenCalledWith('Members were successfully added', {
+ expect(mockToastShow).toHaveBeenCalledWith('Members were successfully added', {
onComplete: expect.any(Function),
});
});
@@ -187,7 +196,7 @@ describe('InviteGroupsModal', () => {
});
it('does not show the toast message on failure', () => {
- expect(wrapper.vm.$toast.show).not.toHaveBeenCalled();
+ expect(mockToastShow).not.toHaveBeenCalled();
});
it('displays the generic error for http server error', () => {
@@ -222,7 +231,6 @@ describe('InviteGroupsModal', () => {
createComponent({ reloadPageOnSubmit: true });
triggerGroupSelect(sharedGroup);
- wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'groupShareWithGroup').mockResolvedValue({ data: groupPostData });
clickInviteButton();
@@ -238,8 +246,19 @@ describe('InviteGroupsModal', () => {
});
it('does not show the toast message on failure', () => {
- expect(wrapper.vm.$toast.show).not.toHaveBeenCalled();
+ expect(mockToastShow).not.toHaveBeenCalled();
});
});
});
+
+ describe('when group select emits an error event', () => {
+ it('displays error alert', async () => {
+ createComponent();
+
+ findGroupSelect().vm.$emit('error', GroupSelect.i18n.errorMessage);
+ await nextTick();
+
+ expect(wrapper.findComponent(GlAlert).text()).toBe(GroupSelect.i18n.errorMessage);
+ });
+ });
});
diff --git a/spec/frontend/invite_members/components/members_token_select_spec.js b/spec/frontend/invite_members/components/members_token_select_spec.js
index ff0313cc49e..925534edd7c 100644
--- a/spec/frontend/invite_members/components/members_token_select_spec.js
+++ b/spec/frontend/invite_members/components/members_token_select_spec.js
@@ -143,12 +143,19 @@ describe('MembersTokenSelect', () => {
});
describe('when input text is an email', () => {
- it('allows user defined tokens', async () => {
- tokenSelector.vm.$emit('text-input', 'foo@bar.com');
+ it.each`
+ email | result
+ ${'foo@bar.com'} | ${true}
+ ${'foo@bar.com '} | ${false}
+ ${' foo@bar.com'} | ${false}
+ ${'foo@ba r.com'} | ${false}
+ ${'fo o@bar.com'} | ${false}
+ `(`with token creation validation on $email`, async ({ email, result }) => {
+ tokenSelector.vm.$emit('text-input', email);
await nextTick();
- expect(tokenSelector.props('allowUserDefinedTokens')).toBe(true);
+ expect(tokenSelector.props('allowUserDefinedTokens')).toBe(result);
});
});
});
diff --git a/spec/frontend/issuable/components/related_issuable_item_spec.js b/spec/frontend/issuable/components/related_issuable_item_spec.js
index 7322894164b..bfb0aaa1c67 100644
--- a/spec/frontend/issuable/components/related_issuable_item_spec.js
+++ b/spec/frontend/issuable/components/related_issuable_item_spec.js
@@ -236,23 +236,21 @@ describe('RelatedIssuableItem', () => {
describe('when work item is issue and the related issue title is clicked', () => {
it('does not open', () => {
mountComponent({ props: { workItemType: 'ISSUE' } });
- wrapper.vm.$refs.modal.show = jest.fn();
findTitleLink().vm.$emit('click', { preventDefault: () => {} });
- expect(wrapper.vm.$refs.modal.show).not.toHaveBeenCalled();
+ expect(showModalSpy).not.toHaveBeenCalled();
});
});
describe('when work item is task and the related issue title is clicked', () => {
beforeEach(() => {
mountComponent({ props: { workItemType: 'TASK' } });
- wrapper.vm.$refs.modal.show = jest.fn();
findTitleLink().vm.$emit('click', { preventDefault: () => {} });
});
it('opens', () => {
- expect(wrapper.vm.$refs.modal.show).toHaveBeenCalled();
+ expect(showModalSpy).toHaveBeenCalled();
});
it('updates the url params with the work item id', () => {
diff --git a/spec/frontend/issuable/components/status_box_spec.js b/spec/frontend/issuable/components/status_box_spec.js
index d26f287d90c..0d47595c9e6 100644
--- a/spec/frontend/issuable/components/status_box_spec.js
+++ b/spec/frontend/issuable/components/status_box_spec.js
@@ -18,6 +18,8 @@ describe('Merge request status box component', () => {
${'merge_request'} | ${'Merged'} | ${'merged'} | ${'issuable-status-badge-merged'} | ${'info'} | ${'merge'}
${'issue'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'issues'}
${'issue'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'info'} | ${'issue-closed'}
+ ${'epic'} | ${'Open'} | ${'opened'} | ${'issuable-status-badge-open'} | ${'success'} | ${'epic'}
+ ${'epic'} | ${'Closed'} | ${'closed'} | ${'issuable-status-badge-closed'} | ${'info'} | ${'epic-closed'}
`(
'with issuableType set to "$issuableType" and state set to "$initialState"',
({ issuableType, badgeText, initialState, badgeClass, badgeVariant, badgeIcon }) => {
diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js
index d7e5f9083b0..b9652327e3d 100644
--- a/spec/frontend/issuable/issuable_form_spec.js
+++ b/spec/frontend/issuable/issuable_form_spec.js
@@ -4,6 +4,9 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableForm from '~/issuable/issuable_form';
import setWindowLocation from 'helpers/set_window_location_helper';
import { confirmSensitiveAction, i18n } from '~/lib/utils/secret_detection';
+import { mockTracking } from 'helpers/tracking_helper';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { TEST_HOST } from 'helpers/test_constants';
import { getSaveableFormChildren } from './helpers';
jest.mock('~/autosave');
@@ -20,9 +23,12 @@ const createIssuable = (form) => {
};
describe('IssuableForm', () => {
+ let trackingSpy;
let $form;
let instance;
+ useLocalStorageSpy();
+
beforeEach(() => {
setHTMLFixture(`
<form>
@@ -32,6 +38,7 @@ describe('IssuableForm', () => {
</form>
`);
$form = $('form');
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
});
afterEach(() => {
@@ -266,6 +273,34 @@ describe('IssuableForm', () => {
expect(resetAutosave).toHaveBeenCalled();
});
+ it.each`
+ windowLocation | context | localStorageValue | editorType
+ ${'/gitlab-org/gitlab/-/issues/412699'} | ${'Issue'} | ${'contentEditor'} | ${'editor_type_rich_text_editor'}
+ ${'/gitlab-org/gitlab/-/merge_requests/125979/diffs'} | ${'MergeRequest'} | ${'contentEditor'} | ${'editor_type_rich_text_editor'}
+ ${'/groups/gitlab-org/-/milestones/8/edit'} | ${'Other'} | ${'contentEditor'} | ${'editor_type_rich_text_editor'}
+ ${'/gitlab-org/gitlab/-/issues/412699'} | ${'Issue'} | ${'markdownField'} | ${'editor_type_plain_text_editor'}
+ ${'/gitlab-org/gitlab/-/merge_requests/125979/diffs'} | ${'MergeRequest'} | ${'markdownField'} | ${'editor_type_plain_text_editor'}
+ ${'/groups/gitlab-org/-/milestones/8/edit'} | ${'Other'} | ${'markdownField'} | ${'editor_type_plain_text_editor'}
+ `(
+ 'tracks event on form submit',
+ ({ windowLocation, context, localStorageValue, editorType }) => {
+ setWindowLocation(`${TEST_HOST}/${windowLocation}`);
+ localStorage.setItem('gl-markdown-editor-mode', localStorageValue);
+
+ issueDescription.value = 'sample message';
+
+ createIssuable($form);
+
+ $form.submit();
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context,
+ editorType,
+ label: 'editor_tracking',
+ });
+ },
+ );
+
it('prevents form submission when token is present', () => {
issueDescription.value = sensitiveMessage;
diff --git a/spec/frontend/issuable/popover/components/issue_popover_spec.js b/spec/frontend/issuable/popover/components/issue_popover_spec.js
index a7605016039..0596433ce9a 100644
--- a/spec/frontend/issuable/popover/components/issue_popover_spec.js
+++ b/spec/frontend/issuable/popover/components/issue_popover_spec.js
@@ -26,7 +26,7 @@ describe('Issue Popover', () => {
apolloProvider: createMockApollo([[issueQuery, queryResponse]]),
propsData: {
target: document.createElement('a'),
- projectPath: 'foo/bar',
+ namespacePath: 'foo/bar',
iid: '1',
cachedTitle: 'Cached title',
},
diff --git a/spec/frontend/issuable/popover/components/mr_popover_spec.js b/spec/frontend/issuable/popover/components/mr_popover_spec.js
index 5b29ecfc0ba..4ed783da853 100644
--- a/spec/frontend/issuable/popover/components/mr_popover_spec.js
+++ b/spec/frontend/issuable/popover/components/mr_popover_spec.js
@@ -64,7 +64,7 @@ describe('MR Popover', () => {
apolloProvider: createMockApollo([[mergeRequestQuery, queryResponse]]),
propsData: {
target: document.createElement('a'),
- projectPath: 'foo/bar',
+ namespacePath: 'foo/bar',
iid: '1',
cachedTitle: 'Cached Title',
},
diff --git a/spec/frontend/issuable/popover/index_spec.js b/spec/frontend/issuable/popover/index_spec.js
index b1aa7f0f0b0..bf9dce4867f 100644
--- a/spec/frontend/issuable/popover/index_spec.js
+++ b/spec/frontend/issuable/popover/index_spec.js
@@ -1,6 +1,6 @@
import { setHTMLFixture } from 'helpers/fixtures';
import * as createDefaultClient from '~/lib/graphql';
-import initIssuablePopovers from '~/issuable/popover/index';
+import initIssuablePopovers, * as popover from '~/issuable/popover/index';
createDefaultClient.default = jest.fn();
@@ -9,6 +9,7 @@ describe('initIssuablePopovers', () => {
let mr2;
let mr3;
let issue1;
+ let workItem1;
beforeEach(() => {
setHTMLFixture(`
@@ -24,30 +25,69 @@ describe('initIssuablePopovers', () => {
<div id="four" class="gfm-issue" title="title" data-iid="1" data-project-path="group/project" data-reference-type="issue">
MR3
</div>
+ <div id="five" class="gfm-work_item" title="title" data-iid="1" data-project-path="group/project" data-reference-type="work_item">
+ MR3
+ </div>
`);
mr1 = document.querySelector('#one');
mr2 = document.querySelector('#two');
mr3 = document.querySelector('#three');
issue1 = document.querySelector('#four');
-
- mr1.addEventListener = jest.fn();
- mr2.addEventListener = jest.fn();
- mr3.addEventListener = jest.fn();
- issue1.addEventListener = jest.fn();
+ workItem1 = document.querySelector('#five');
});
- it('does not add the same event listener twice', () => {
- initIssuablePopovers([mr1, mr1, mr2, issue1]);
+ describe('init function', () => {
+ beforeEach(() => {
+ mr1.addEventListener = jest.fn();
+ mr2.addEventListener = jest.fn();
+ mr3.addEventListener = jest.fn();
+ issue1.addEventListener = jest.fn();
+ workItem1.addEventListener = jest.fn();
+ });
+
+ it('does not add the same event listener twice', () => {
+ initIssuablePopovers([mr1, mr1, mr2, issue1, workItem1]);
+
+ expect(mr1.addEventListener).toHaveBeenCalledTimes(1);
+ expect(mr2.addEventListener).toHaveBeenCalledTimes(1);
+ expect(issue1.addEventListener).toHaveBeenCalledTimes(1);
+ expect(workItem1.addEventListener).toHaveBeenCalledTimes(1);
+ });
- expect(mr1.addEventListener).toHaveBeenCalledTimes(1);
- expect(mr2.addEventListener).toHaveBeenCalledTimes(1);
- expect(issue1.addEventListener).toHaveBeenCalledTimes(1);
+ it('does not add listener if it does not have the necessary data attributes', () => {
+ initIssuablePopovers([mr1, mr2, mr3]);
+
+ expect(mr3.addEventListener).not.toHaveBeenCalled();
+ });
});
- it('does not add listener if it does not have the necessary data attributes', () => {
- initIssuablePopovers([mr1, mr2, mr3]);
+ describe('mount function', () => {
+ const expectedMountObject = {
+ apolloProvider: expect.anything(),
+ iid: '1',
+ namespacePath: 'group/project',
+ title: 'title',
+ };
+
+ beforeEach(() => {
+ jest.spyOn(popover, 'handleIssuablePopoverMount').mockImplementation(jest.fn());
+ });
+
+ it('calls popover mount function with components for Issue, MR, and Work Item', () => {
+ initIssuablePopovers([mr1, issue1, workItem1], popover.handleIssuablePopoverMount);
+
+ [mr1, issue1, workItem1].forEach(async (el) => {
+ await el.dispatchEvent(new Event('mouseenter', { target: el }));
- expect(mr3.addEventListener).not.toHaveBeenCalled();
+ expect(popover.handleIssuablePopoverMount).toHaveBeenCalledWith(
+ expect.objectContaining({
+ ...expectedMountObject,
+ referenceType: el.dataset.referenceType,
+ target: el,
+ }),
+ );
+ });
+ });
});
});
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 e97c0312181..a24bffdd363 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
import { GlIcon, GlCard } from '@gitlab/ui';
-import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
issuable1,
issuable2,
@@ -14,6 +14,7 @@ import {
linkedIssueTypesTextMap,
PathIdSeparator,
} from '~/related_issues/constants';
+import RelatedIssuesList from '~/related_issues/components/related_issues_list.vue';
describe('RelatedIssuesBlock', () => {
let wrapper;
@@ -21,9 +22,10 @@ describe('RelatedIssuesBlock', () => {
const findToggleButton = () => wrapper.findByTestId('toggle-links');
const findRelatedIssuesBody = () => wrapper.findByTestId('related-issues-body');
const findIssueCountBadgeAddButton = () => wrapper.findByTestId('related-issues-plus-button');
+ const findAllRelatedIssuesList = () => wrapper.findAllComponents(RelatedIssuesList);
+ const findRelatedIssuesList = (index) => findAllRelatedIssuesList().at(index);
const createComponent = ({
- mountFn = mountExtended,
pathIdSeparator = PathIdSeparator.Issue,
issuableType = TYPE_ISSUE,
canAdmin = false,
@@ -35,7 +37,7 @@ describe('RelatedIssuesBlock', () => {
autoCompleteEpics = true,
slots = '',
} = {}) => {
- wrapper = mountFn(RelatedIssuesBlock, {
+ wrapper = shallowMountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator,
issuableType,
@@ -76,7 +78,7 @@ describe('RelatedIssuesBlock', () => {
helpPath: '/help/user/project/issues/related_issues',
});
- expect(wrapper.find('.card-title').text()).toContain(titleText);
+ expect(wrapper.findByTestId('card-title').text()).toContain(titleText);
expect(findIssueCountBadgeAddButton().attributes('aria-label')).toBe(addButtonText);
},
);
@@ -94,12 +96,9 @@ describe('RelatedIssuesBlock', () => {
it('displays header text slot data', () => {
const headerText = '<div>custom header text</div>';
- createComponent({
- mountFn: shallowMountExtended,
- slots: { 'header-text': headerText },
- });
+ createComponent({ slots: { 'header-text': headerText } });
- expect(wrapper.find('.card-title').html()).toContain(headerText);
+ expect(wrapper.findByTestId('card-title').html()).toContain(headerText);
});
});
@@ -107,10 +106,7 @@ describe('RelatedIssuesBlock', () => {
it('displays header actions slot data', () => {
const headerActions = '<button data-testid="custom-button">custom button</button>';
- createComponent({
- mountFn: shallowMountExtended,
- slots: { 'header-actions': headerActions },
- });
+ createComponent({ slots: { 'header-actions': headerActions } });
expect(wrapper.findByTestId('custom-button').html()).toBe(headerActions);
});
@@ -153,10 +149,6 @@ describe('RelatedIssuesBlock', () => {
});
describe('showCategorizedIssues prop', () => {
- const issueList = () => wrapper.findAll('.js-related-issues-token-list-item');
- const categorizedHeadings = () => wrapper.findAll('h4');
- const headingTextAt = (index) => categorizedHeadings().at(index).text();
-
describe('when showCategorizedIssues=true', () => {
beforeEach(() =>
createComponent({
@@ -166,25 +158,25 @@ describe('RelatedIssuesBlock', () => {
);
it('should render issue tokens items', () => {
- expect(issueList()).toHaveLength(3);
+ expect(findAllRelatedIssuesList()).toHaveLength(3);
});
it('shows "Blocks" heading', () => {
- const blocks = linkedIssueTypesTextMap[linkedIssueTypesMap.BLOCKS];
-
- expect(headingTextAt(0)).toBe(blocks);
+ expect(findRelatedIssuesList(0).props('heading')).toBe(
+ linkedIssueTypesTextMap[linkedIssueTypesMap.BLOCKS],
+ );
});
it('shows "Is blocked by" heading', () => {
- const isBlockedBy = linkedIssueTypesTextMap[linkedIssueTypesMap.IS_BLOCKED_BY];
-
- expect(headingTextAt(1)).toBe(isBlockedBy);
+ expect(findRelatedIssuesList(1).props('heading')).toBe(
+ linkedIssueTypesTextMap[linkedIssueTypesMap.IS_BLOCKED_BY],
+ );
});
it('shows "Relates to" heading', () => {
- const relatesTo = linkedIssueTypesTextMap[linkedIssueTypesMap.RELATES_TO];
-
- expect(headingTextAt(2)).toBe(relatesTo);
+ expect(findRelatedIssuesList(2).props('heading')).toBe(
+ linkedIssueTypesTextMap[linkedIssueTypesMap.RELATES_TO],
+ );
});
});
@@ -194,8 +186,9 @@ describe('RelatedIssuesBlock', () => {
showCategorizedIssues: false,
relatedIssues: [issuable1, issuable2, issuable3],
});
- expect(issueList()).toHaveLength(3);
- expect(categorizedHeadings()).toHaveLength(0);
+ expect(findAllRelatedIssuesList()).toHaveLength(1);
+ expect(findRelatedIssuesList(0).props('relatedIssues')).toHaveLength(3);
+ expect(findRelatedIssuesList(0).props('heading')).toBe('');
});
});
});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
index b119c836411..6638f3d6289 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -1,6 +1,5 @@
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import {
defaultProps,
@@ -17,7 +16,6 @@ import {
import { linkedIssueTypesMap } from '~/related_issues/constants';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
-import relatedIssuesService from '~/related_issues/services/related_issues_service';
jest.mock('~/alert');
@@ -37,7 +35,7 @@ describe('RelatedIssuesRoot', () => {
});
const createComponent = ({ props = {}, data = {} } = {}) => {
- wrapper = mount(RelatedIssuesRoot, {
+ wrapper = shallowMount(RelatedIssuesRoot, {
propsData: {
...defaultProps,
...props,
@@ -58,14 +56,13 @@ describe('RelatedIssuesRoot', () => {
describe('when "relatedIssueRemoveRequest" event is emitted', () => {
describe('when emitted value is a numerical issue', () => {
beforeEach(async () => {
- jest
- .spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues')
- .mockReturnValue(Promise.reject());
+ mock.onGet(defaultProps.endpoint).reply(HTTP_STATUS_OK, [issuable1]);
await createComponent();
- wrapper.vm.store.setRelatedIssues([issuable1]);
});
- it('removes related issue on API success', async () => {
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/417177
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('removes related issue on API success', async () => {
mock.onDelete(issuable1.referencePath).reply(HTTP_STATUS_OK, { issues: [] });
findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', issuable1.id);
@@ -91,8 +88,7 @@ describe('RelatedIssuesRoot', () => {
const workItem = `gid://gitlab/WorkItem/${issuable1.id}`;
createComponent({ data: { state: { relatedIssues: [issuable1] } } });
- findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', workItem);
- await nextTick();
+ await findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', workItem);
expect(findRelatedIssuesBlock().props('relatedIssues')).toEqual([]);
});
@@ -103,8 +99,7 @@ describe('RelatedIssuesRoot', () => {
it('toggles related issues form to visible from hidden', async () => {
createComponent();
- findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm');
- await nextTick();
+ await findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm');
expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(true);
});
@@ -112,24 +107,25 @@ describe('RelatedIssuesRoot', () => {
it('toggles related issues form to hidden from visible', async () => {
createComponent({ data: { isFormVisible: true } });
- findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm');
- await nextTick();
+ await findRelatedIssuesBlock().vm.$emit('toggleAddRelatedIssuesForm');
expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(false);
});
});
describe('when "pendingIssuableRemoveRequest" event is emitted', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
- wrapper.vm.store.setPendingReferences([issuable1.reference]);
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ untouchedRawReferences: [issuable1.reference],
+ touchedReference: '',
+ });
});
it('removes pending related issue', async () => {
expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(1);
- findRelatedIssuesBlock().vm.$emit('pendingIssuableRemoveRequest', 0);
- await nextTick();
+ await findRelatedIssuesBlock().vm.$emit('pendingIssuableRemoveRequest', 0);
expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
});
@@ -137,33 +133,24 @@ describe('RelatedIssuesRoot', () => {
describe('when "addIssuableFormSubmit" event is emitted', () => {
beforeEach(async () => {
- jest
- .spyOn(relatedIssuesService.prototype, 'fetchRelatedIssues')
- .mockReturnValue(Promise.reject());
await createComponent();
- jest.spyOn(wrapper.vm, 'processAllReferences');
- jest.spyOn(wrapper.vm.service, 'addRelatedIssues');
createAlert.mockClear();
});
- it('processes references before submitting', () => {
+ it('processes references before submitting', async () => {
const input = '#123';
const linkedIssueType = linkedIssueTypesMap.RELATES_TO;
const emitObj = {
pendingReferences: input,
linkedIssueType,
};
-
- findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', emitObj);
-
- expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input);
- expect(wrapper.vm.service.addRelatedIssues).toHaveBeenCalledWith([input], linkedIssueType);
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', emitObj);
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input]);
});
- it('submits zero pending issues as related issue', () => {
- wrapper.vm.store.setPendingReferences([]);
-
- findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+ it('submits zero pending issues as related issue', async () => {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+ await waitForPromises();
expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
expect(findRelatedIssuesBlock().props('relatedIssues')).toHaveLength(0);
@@ -177,9 +164,11 @@ describe('RelatedIssuesRoot', () => {
status: 'success',
},
});
- wrapper.vm.store.setPendingReferences([issuable1.reference]);
-
- findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ untouchedRawReferences: [issuable1],
+ touchedReference: '',
+ });
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
await waitForPromises();
expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
@@ -196,9 +185,11 @@ describe('RelatedIssuesRoot', () => {
status: 'success',
},
});
- wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
-
- findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ untouchedRawReferences: [issuable1.reference, issuable2.reference],
+ touchedReference: '',
+ });
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', {});
await waitForPromises();
expect(findRelatedIssuesBlock().props('pendingReferences')).toHaveLength(0);
@@ -212,12 +203,15 @@ describe('RelatedIssuesRoot', () => {
const input = '#123';
const message = 'error';
mock.onPost(defaultProps.endpoint).reply(HTTP_STATUS_CONFLICT, { message });
- wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ untouchedRawReferences: [issuable1.reference, issuable2.reference],
+ touchedReference: '',
+ });
expect(findRelatedIssuesBlock().props('hasError')).toBe(false);
expect(findRelatedIssuesBlock().props('itemAddFailureMessage')).toBe(null);
- findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', input);
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormSubmit', input);
await waitForPromises();
expect(findRelatedIssuesBlock().props('hasError')).toBe(true);
@@ -229,8 +223,7 @@ describe('RelatedIssuesRoot', () => {
beforeEach(() => createComponent({ data: { isFormVisible: true, inputValue: 'foo' } }));
it('hides form and resets input', async () => {
- findRelatedIssuesBlock().vm.$emit('addIssuableFormCancel');
- await nextTick();
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormCancel');
expect(findRelatedIssuesBlock().props('isFormVisible')).toBe(false);
expect(findRelatedIssuesBlock().props('inputValue')).toBe('');
@@ -243,11 +236,10 @@ describe('RelatedIssuesRoot', () => {
const input = '#123 ';
createComponent();
- findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: [input.trim()],
touchedReference: input,
});
- await nextTick();
expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input.trim()]);
});
@@ -256,11 +248,10 @@ describe('RelatedIssuesRoot', () => {
const input = 'asdf/qwer#444 ';
createComponent();
- findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: [input.trim()],
touchedReference: input,
});
- await nextTick();
expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input.trim()]);
});
@@ -270,11 +261,10 @@ describe('RelatedIssuesRoot', () => {
const input = `${link} `;
createComponent();
- findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: [input.trim()],
touchedReference: input,
});
- await nextTick();
expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([link]);
});
@@ -283,11 +273,10 @@ describe('RelatedIssuesRoot', () => {
const input = 'asdf/qwer#444 #12 ';
createComponent();
- findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: input.trim().split(/\s/),
touchedReference: '2',
});
- await nextTick();
expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([
'asdf/qwer#444',
@@ -299,11 +288,10 @@ describe('RelatedIssuesRoot', () => {
const input = 'something random ';
createComponent();
- findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: input.trim().split(/\s/),
touchedReference: '2',
});
- await nextTick();
expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([
'something',
@@ -317,11 +305,10 @@ describe('RelatedIssuesRoot', () => {
const input = '23';
createComponent({ props: { pathIdSeparator } });
- findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormInput', {
untouchedRawReferences: input.trim().split(/\s/),
touchedReference: input,
});
- await nextTick();
expect(findRelatedIssuesBlock().props('inputValue')).toBe(`${pathIdSeparator}${input}`);
},
@@ -331,15 +318,13 @@ describe('RelatedIssuesRoot', () => {
describe('when "addIssuableFormBlur" event is emitted', () => {
beforeEach(() => {
createComponent();
- jest.spyOn(wrapper.vm, 'processAllReferences').mockImplementation(() => {});
});
- it('adds any references to pending when blurring', () => {
+ it('adds any references to pending when blurring', async () => {
const input = '#123';
-
- findRelatedIssuesBlock().vm.$emit('addIssuableFormBlur', input);
-
- expect(wrapper.vm.processAllReferences).toHaveBeenCalledWith(input);
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([]);
+ await findRelatedIssuesBlock().vm.$emit('addIssuableFormBlur', input);
+ expect(findRelatedIssuesBlock().props('pendingReferences')).toEqual([input]);
});
});
});
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
index c152a5ef9a8..148c6230b9f 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -54,6 +54,7 @@ describe('IssuesDashboardApp component', () => {
const defaultProvide = {
autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
+ autocompleteUsersPath: 'autocomplete/users.json',
calendarPath: 'calendar/path',
dashboardLabelsPath: 'dashboard/labels/path',
dashboardMilestonesPath: 'dashboard/milestones/path',
@@ -120,7 +121,7 @@ describe('IssuesDashboardApp component', () => {
await waitForPromises();
});
- // https://gitlab.com/gitlab-org/gitlab/-/issues/391722
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/391722
// eslint-disable-next-line jest/no-disabled-tests
it.skip('renders IssuableList component', () => {
expect(findIssuableList().props()).toMatchObject({
diff --git a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
index a61e7ed1e86..8e69213ebba 100644
--- a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
+++ b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
@@ -23,6 +23,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
newProjectPath: 'new/project/path',
showNewIssueLink: false,
signInPath: 'sign/in/path',
+ groupId: '',
};
const findCsvImportExportButtons = () => wrapper.findComponent(CsvImportExportButtons);
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 0e87e5e6595..72bf4826056 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -115,6 +115,7 @@ describe('CE IssuesListApp component', () => {
rssPath: 'rss/path',
showNewIssueLink: true,
signInPath: 'sign/in/path',
+ groupId: '',
};
let defaultQueryResponse = getIssuesQueryResponse;
diff --git a/spec/frontend/issues/show/components/delete_issue_modal_spec.js b/spec/frontend/issues/show/components/delete_issue_modal_spec.js
index b8adeb24005..f122180a403 100644
--- a/spec/frontend/issues/show/components/delete_issue_modal_spec.js
+++ b/spec/frontend/issues/show/components/delete_issue_modal_spec.js
@@ -37,11 +37,14 @@ describe('DeleteIssueModal component', () => {
});
describe('when "primary" event is emitted', () => {
- let formSubmitSpy;
+ const submitMock = jest.fn();
+ // Mock the form submit method
+ Object.defineProperty(HTMLFormElement.prototype, 'submit', {
+ value: submitMock,
+ });
beforeEach(() => {
wrapper = mountComponent();
- formSubmitSpy = jest.spyOn(wrapper.vm.$refs.form, 'submit');
findModal().vm.$emit('primary');
});
@@ -50,7 +53,7 @@ describe('DeleteIssueModal component', () => {
});
it('submits the form', () => {
- expect(formSubmitSpy).toHaveBeenCalled();
+ expect(submitMock).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index c7116f380a1..5e329d44acb 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -3,20 +3,19 @@ import DescriptionField from '~/issues/show/components/fields/description.vue';
import eventHub from '~/issues/show/event_hub';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import { mockTracking } from 'helpers/tracking_helper';
describe('Description field component', () => {
let wrapper;
+ let trackingSpy;
- const findTextarea = () => wrapper.findComponent({ ref: 'textarea' });
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
-
- const mountComponent = ({ description = 'test', contentEditorOnIssues = false } = {}) =>
- shallowMount(DescriptionField, {
+ const mountComponent = ({ description = 'test', contentEditorOnIssues = false } = {}) => {
+ wrapper = shallowMount(DescriptionField, {
attachTo: document.body,
propsData: {
markdownPreviewPath: '/',
markdownDocsPath: '/',
- quickActionsDocsPath: '/',
value: description,
},
provide: {
@@ -28,90 +27,66 @@ describe('Description field component', () => {
MarkdownField,
},
});
+ };
beforeEach(() => {
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
jest.spyOn(eventHub, '$emit');
- });
-
- it('renders markdown field with description', () => {
- wrapper = mountComponent();
-
- expect(findTextarea().element.value).toBe('test');
- });
-
- it('renders markdown field with a markdown description', () => {
- const markdown = '**test**';
-
- wrapper = mountComponent({ description: markdown });
- expect(findTextarea().element.value).toBe(markdown);
+ mountComponent({ contentEditorOnIssues: true });
});
- it('focuses field when mounted', () => {
- wrapper = mountComponent();
+ it('passes feature flag to the MarkdownEditorComponent', () => {
+ expect(findMarkdownEditor().props('enableContentEditor')).toBe(true);
- expect(document.activeElement).toBe(findTextarea().element);
- });
-
- it('triggers update with meta+enter', () => {
- wrapper = mountComponent();
+ mountComponent({ contentEditorOnIssues: false });
- findTextarea().trigger('keydown.enter', { metaKey: true });
-
- expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
+ expect(findMarkdownEditor().props('enableContentEditor')).toBe(false);
});
- it('triggers update with ctrl+enter', () => {
- wrapper = mountComponent();
-
- findTextarea().trigger('keydown.enter', { ctrlKey: true });
-
- expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
+ it('uses the MarkdownEditor component to edit markdown', () => {
+ expect(findMarkdownEditor().props()).toMatchObject({
+ value: 'test',
+ renderMarkdownPath: '/',
+ autofocus: true,
+ supportsQuickActions: true,
+ markdownDocsPath: '/',
+ enableAutocomplete: true,
+ });
});
- describe('when contentEditorOnIssues feature flag is on', () => {
+ describe.each`
+ testDescription | metaKey | ctrlKey
+ ${'when meta+enter is pressed'} | ${true} | ${false}
+ ${'when ctrl+enter is pressed'} | ${false} | ${true}
+ `('$testDescription', ({ metaKey, ctrlKey }) => {
beforeEach(() => {
- wrapper = mountComponent({ contentEditorOnIssues: true });
- });
-
- it('uses the MarkdownEditor component to edit markdown', () => {
- expect(findMarkdownEditor().props()).toMatchObject({
- value: 'test',
- renderMarkdownPath: '/',
- autofocus: true,
- supportsQuickActions: true,
- quickActionsDocsPath: expect.any(String),
- markdownDocsPath: '/',
- enableAutocomplete: true,
- });
- });
-
- it('triggers update with meta+enter', () => {
findMarkdownEditor().vm.$emit('keydown', {
type: 'keydown',
keyCode: 13,
- metaKey: true,
+ metaKey,
+ ctrlKey,
});
+ });
+ it('triggers update', () => {
expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
});
- it('triggers update with ctrl+enter', () => {
- findMarkdownEditor().vm.$emit('keydown', {
- type: 'keydown',
- keyCode: 13,
- ctrlKey: true,
+ it('tracks event', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context: 'Issue',
+ editorType: 'editor_type_plain_text_editor',
+ label: 'editor_tracking',
});
-
- expect(eventHub.$emit).toHaveBeenCalledWith('update.issuable');
});
+ });
- it('emits input event when MarkdownEditor emits input event', () => {
- const markdown = 'markdown';
+ it('emits input event when MarkdownEditor emits input event', () => {
+ const markdown = 'markdown';
- findMarkdownEditor().vm.$emit('input', markdown);
+ findMarkdownEditor().vm.$emit('input', markdown);
- expect(wrapper.emitted('input')).toEqual([[markdown]]);
- });
+ expect(wrapper.emitted('input')).toEqual([[markdown]]);
});
});
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 9a503a2d882..8a98b2b702a 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -1,12 +1,20 @@
import Vue, { nextTick } from 'vue';
-import { GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui';
+import { GlDropdown, GlDropdownItem, GlLink, GlModal, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import { mockTracking } from 'helpers/tracking_helper';
import { createAlert, VARIANT_SUCCESS } from '~/alert';
-import { STATUS_CLOSED, STATUS_OPEN, TYPE_INCIDENT, TYPE_ISSUE } from '~/issues/constants';
+import {
+ STATUS_CLOSED,
+ STATUS_OPEN,
+ TYPE_INCIDENT,
+ TYPE_ISSUE,
+ TYPE_TEST_CASE,
+ TYPE_ALERT,
+ TYPE_MERGE_REQUEST,
+} from '~/issues/constants';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import HeaderActions from '~/issues/show/components/header_actions.vue';
@@ -14,6 +22,7 @@ import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show
import issuesEventHub from '~/issues/show/event_hub';
import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutation.graphql';
import * as urlUtility from '~/lib/utils/url_utility';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
import eventHub from '~/notes/event_hub';
import createStore from '~/notes/stores';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -690,4 +699,27 @@ describe('HeaderActions component', () => {
},
);
});
+
+ describe('issue type text', () => {
+ it.each`
+ issueType | expectedText
+ ${TYPE_ISSUE} | ${'issue'}
+ ${TYPE_INCIDENT} | ${'incident'}
+ ${TYPE_MERGE_REQUEST} | ${'merge request'}
+ ${TYPE_ALERT} | ${'alert'}
+ ${TYPE_TEST_CASE} | ${'test case'}
+ ${'unknown'} | ${'unknown'}
+ `('$issueType', ({ issueType, expectedText }) => {
+ wrapper = mountComponent({
+ movedMrSidebarEnabled: true,
+ props: { issueType, issuableEmailAddress: 'mock-email-address' },
+ });
+
+ expect(wrapper.findComponent(GlDropdown).attributes('text')).toBe(
+ `${capitalizeFirstCharacter(expectedText)} actions`,
+ );
+ expect(findDropdownBy('copy-email').text()).toBe(`Copy ${expectedText} email address`);
+ expect(findDesktopDropdownItems().at(0).text()).toBe(`New related ${expectedText}`);
+ });
+ });
});
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 24653a23036..2500c808073 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,5 +1,5 @@
import timezoneMock from 'timezone-mock';
-import { GlIcon, GlDropdown, GlBadge } from '@gitlab/ui';
+import { GlIcon, GlDisclosureDropdown, GlBadge } from '@gitlab/ui';
import { nextTick } from 'vue';
import { timelineItemI18n } from '~/issues/show/components/incidents/constants';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -28,7 +28,7 @@ describe('IncidentTimelineEventList', () => {
const findCommentIcon = () => wrapper.findComponent(GlIcon);
const findEventTime = () => wrapper.findByTestId('event-time');
const findEventTags = () => wrapper.findAllComponents(GlBadge);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findGlDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
const findDeleteButton = () => wrapper.findByText(timelineItemI18n.delete);
const findEditButton = () => wrapper.findByText(timelineItemI18n.edit);
@@ -85,7 +85,7 @@ describe('IncidentTimelineEventList', () => {
describe('action dropdown', () => {
it('does not show the action dropdown by default', () => {
- expect(findDropdown().exists()).toBe(false);
+ expect(findGlDropdown().exists()).toBe(false);
expect(findDeleteButton().exists()).toBe(false);
});
@@ -100,14 +100,14 @@ describe('IncidentTimelineEventList', () => {
mockEvent: systemGeneratedMockEvent,
});
- expect(findDropdown().exists()).toBe(true);
+ expect(findGlDropdown().exists()).toBe(true);
expect(findEditButton().exists()).toBe(false);
});
it('shows dropdown and delete item when user has update permission', () => {
mountComponent({ provide: { canUpdateTimelineEvent: true } });
- expect(findDropdown().exists()).toBe(true);
+ expect(findGlDropdown().exists()).toBe(true);
expect(findDeleteButton().exists()).toBe(true);
});
diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
index 0b3ff0667b1..93cb7b5ae16 100644
--- a/spec/frontend/issues/show/components/task_list_item_actions_spec.js
+++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
@@ -3,6 +3,8 @@ import { shallowMount } from '@vue/test-utils';
import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
import eventHub from '~/issues/show/event_hub';
+jest.mock('~/issues/show/event_hub');
+
describe('TaskListItemActions component', () => {
let wrapper;
@@ -37,16 +39,12 @@ describe('TaskListItemActions component', () => {
});
it('emits event when `Convert to task` dropdown item is clicked', () => {
- jest.spyOn(eventHub, '$emit');
-
findConvertToTaskItem().vm.$emit('action');
expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
});
it('emits event when `Delete` dropdown item is clicked', () => {
- jest.spyOn(eventHub, '$emit');
-
findDeleteItem().vm.$emit('action');
expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');
diff --git a/spec/frontend/issues/show/issue_spec.js b/spec/frontend/issues/show/issue_spec.js
index 2980a6c33ee..561035242eb 100644
--- a/spec/frontend/issues/show/issue_spec.js
+++ b/spec/frontend/issues/show/issue_spec.js
@@ -19,7 +19,7 @@ const setupHTML = (initialData) => {
describe('Issue show index', () => {
describe('initIssueApp', () => {
- // https://gitlab.com/gitlab-org/gitlab/-/issues/390368
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/390368
// eslint-disable-next-line jest/no-disabled-tests
it.skip('should initialize app with no potential XSS attack', async () => {
const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
diff --git a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
index 0a887efee4b..f4f4936a134 100644
--- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
@@ -137,7 +137,6 @@ describe('ProjectDropdown', () => {
describe('when searching branches', () => {
it('triggers a refetch', async () => {
createComponent({ mountFn: mount });
- jest.clearAllMocks();
const mockSearchTerm = 'gitl';
await findDropdown().vm.$emit('search', mockSearchTerm);
diff --git a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
index a3bc8e861b2..cf2dacb50d8 100644
--- a/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/source_branch_dropdown_spec.js
@@ -104,7 +104,6 @@ describe('SourceBranchDropdown', () => {
it('triggers a refetch', async () => {
createComponent({ mountFn: mount, props: { selectedProject: mockSelectedProject } });
await waitForPromises();
- jest.clearAllMocks();
const mockSearchTerm = 'mai';
await findListbox().vm.$emit('search', mockSearchTerm);
diff --git a/spec/frontend/jira_connect/subscriptions/components/app_spec.js b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
index 26a9d07321c..ea578836a12 100644
--- a/spec/frontend/jira_connect/subscriptions/components/app_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/app_spec.js
@@ -7,6 +7,7 @@ import SignInPage from '~/jira_connect/subscriptions/pages/sign_in/sign_in_page.
import SubscriptionsPage from '~/jira_connect/subscriptions/pages/subscriptions_page.vue';
import UserLink from '~/jira_connect/subscriptions/components/user_link.vue';
import BrowserSupportAlert from '~/jira_connect/subscriptions/components/browser_support_alert.vue';
+import FeedbackBanner from '~/jira_connect/subscriptions/components/feedback_banner.vue';
import createStore from '~/jira_connect/subscriptions/store';
import { SET_ALERT } from '~/jira_connect/subscriptions/store/mutation_types';
import { I18N_DEFAULT_SIGN_IN_ERROR_MESSAGE } from '~/jira_connect/subscriptions/constants';
@@ -31,6 +32,7 @@ describe('JiraConnectApp', () => {
const findSubscriptionsPage = () => wrapper.findComponent(SubscriptionsPage);
const findUserLink = () => wrapper.findComponent(UserLink);
const findBrowserSupportAlert = () => wrapper.findComponent(BrowserSupportAlert);
+ const findFeedbackBanner = () => wrapper.findComponent(FeedbackBanner);
const createComponent = ({ provide, initialState = {} } = {}) => {
store = createStore({ ...initialState, subscriptions: [mockSubscription] });
@@ -66,6 +68,12 @@ describe('JiraConnectApp', () => {
expect(findJiraConnectApp().exists()).toBe(false);
});
+ it('renders FeedbackBanner', () => {
+ createComponent();
+
+ expect(findFeedbackBanner().exists()).toBe(true);
+ });
+
describe.each`
scenario | currentUser | expectUserLink | expectSignInPage | expectSubscriptionsPage
${'user is not signed in'} | ${undefined} | ${false} | ${true} | ${false}
diff --git a/spec/frontend/jira_connect/subscriptions/components/feedback_banner_spec.js b/spec/frontend/jira_connect/subscriptions/components/feedback_banner_spec.js
new file mode 100644
index 00000000000..8debfaad5bb
--- /dev/null
+++ b/spec/frontend/jira_connect/subscriptions/components/feedback_banner_spec.js
@@ -0,0 +1,45 @@
+import { GlBanner } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import FeedbackBanner from '~/jira_connect/subscriptions/components/feedback_banner.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+
+describe('FeedbackBanner', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(FeedbackBanner);
+ };
+
+ const findBanner = () => wrapper.findComponent(GlBanner);
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders a banner with button', () => {
+ expect(findBanner().props()).toMatchObject({
+ title: FeedbackBanner.i18n.title,
+ buttonText: FeedbackBanner.i18n.buttonText,
+ buttonLink: FeedbackBanner.feedbackIssueUrl,
+ });
+ });
+
+ it('uses localStorage with default value as false', () => {
+ expect(findLocalStorageSync().props().value).toBe(false);
+ });
+
+ describe('when banner is dimsissed', () => {
+ beforeEach(() => {
+ findBanner().vm.$emit('close');
+ });
+
+ it('hides the banner', () => {
+ expect(findBanner().exists()).toBe(false);
+ });
+
+ it('updates localStorage value to true', () => {
+ expect(findLocalStorageSync().props().value).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/jobs/components/job/job_app_spec.js b/spec/frontend/jobs/components/job/job_app_spec.js
index 394fc8ad43c..c925131dd9c 100644
--- a/spec/frontend/jobs/components/job/job_app_spec.js
+++ b/spec/frontend/jobs/components/job/job_app_spec.js
@@ -9,7 +9,7 @@ import EnvironmentsBlock from '~/jobs/components/job/environments_block.vue';
import ErasedBlock from '~/jobs/components/job/erased_block.vue';
import JobApp from '~/jobs/components/job/job_app.vue';
import JobLog from '~/jobs/components/log/log.vue';
-import JobLogTopBar from '~/jobs/components/job/job_log_controllers.vue';
+import JobLogTopBar from 'ee_else_ce/jobs/components/job/job_log_controllers.vue';
import Sidebar from '~/jobs/components/job/sidebar/sidebar.vue';
import StuckBlock from '~/jobs/components/job/stuck_block.vue';
import UnmetPrerequisitesBlock from '~/jobs/components/job/unmet_prerequisites_block.vue';
diff --git a/spec/frontend/jobs/components/job/job_container_item_spec.js b/spec/frontend/jobs/components/job/job_container_item_spec.js
index 8121aa1172f..39782130d38 100644
--- a/spec/frontend/jobs/components/job/job_container_item_spec.js
+++ b/spec/frontend/jobs/components/job/job_container_item_spec.js
@@ -9,8 +9,8 @@ import job from '../../mock_data';
describe('JobContainerItem', () => {
let wrapper;
- const findCiIconComponent = () => wrapper.findComponent(CiIcon);
- const findGlIconComponent = () => wrapper.findComponent(GlIcon);
+ const findCiIcon = () => wrapper.findComponent(CiIcon);
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
function createComponent(jobData = {}, props = { isActive: false, retried: false }) {
wrapper = shallowMount(JobContainerItem, {
@@ -30,9 +30,7 @@ describe('JobContainerItem', () => {
});
it('displays a status icon', () => {
- const ciIcon = findCiIconComponent();
-
- expect(ciIcon.props('status')).toBe(job.status);
+ expect(findCiIcon().props('status')).toBe(job.status);
});
it('displays the job name', () => {
@@ -52,9 +50,7 @@ describe('JobContainerItem', () => {
});
it('displays an arrow sprite icon', () => {
- const icon = findGlIconComponent();
-
- expect(icon.props('name')).toBe('arrow-right');
+ expect(findGlIcon().props('name')).toBe('arrow-right');
});
});
@@ -64,9 +60,7 @@ describe('JobContainerItem', () => {
});
it('displays a retry icon', () => {
- const icon = findGlIconComponent();
-
- expect(icon.props('name')).toBe('retry');
+ expect(findGlIcon().props('name')).toBe('retry');
});
});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index b4ec00ab766..444d4a96f9c 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1140,4 +1140,38 @@ describe('common_utils', () => {
expect(result).toEqual([{ hello: '' }, { helloWorld: '' }]);
});
});
+
+ describe('isCurrentUser', () => {
+ describe('when user is not signed in', () => {
+ it('returns `false`', () => {
+ window.gon.current_user_id = null;
+
+ expect(commonUtils.isCurrentUser(1)).toBe(false);
+ });
+ });
+
+ describe('when current user id does not match the provided user id', () => {
+ it('returns `false`', () => {
+ window.gon.current_user_id = 2;
+
+ expect(commonUtils.isCurrentUser(1)).toBe(false);
+ });
+ });
+
+ describe('when current user id matches the provided user id', () => {
+ it('returns `true`', () => {
+ window.gon.current_user_id = 1;
+
+ expect(commonUtils.isCurrentUser(1)).toBe(true);
+ });
+ });
+
+ describe('when provided user id is a string and it matches current user id', () => {
+ it('returns `true`', () => {
+ window.gon.current_user_id = 1;
+
+ expect(commonUtils.isCurrentUser('1')).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
index e7a6367eeac..65018fe1625 100644
--- a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
@@ -152,3 +152,18 @@ describe('formatUtcOffset', () => {
expect(utils.formatUtcOffset(offset)).toEqual(expected);
});
});
+
+describe('humanTimeframe', () => {
+ it.each`
+ startDate | dueDate | returnValue
+ ${'2021-1-1'} | ${'2021-2-28'} | ${'Jan 1 – Feb 28, 2021'}
+ ${'2021-1-1'} | ${'2022-2-28'} | ${'Jan 1, 2021 – Feb 28, 2022'}
+ ${'2021-1-1'} | ${null} | ${'Jan 1, 2021 – No due date'}
+ ${null} | ${'2021-2-28'} | ${'No start date – Feb 28, 2021'}
+ `(
+ 'returns string "$returnValue" when startDate is $startDate and dueDate is $dueDate',
+ ({ startDate, dueDate, returnValue }) => {
+ expect(utils.humanTimeframe(startDate, dueDate)).toBe(returnValue);
+ },
+ );
+});
diff --git a/spec/frontend/lib/utils/downloader_spec.js b/spec/frontend/lib/utils/downloader_spec.js
index c14cba3a62b..a95b46d1440 100644
--- a/spec/frontend/lib/utils/downloader_spec.js
+++ b/spec/frontend/lib/utils/downloader_spec.js
@@ -8,10 +8,6 @@ describe('Downloader', () => {
jest.spyOn(document, 'createElement').mockImplementation(() => a);
});
- afterEach(() => {
- jest.clearAllMocks();
- });
-
describe('when inline file content is provided', () => {
const fileData = 'inline content';
const fileName = 'test.csv';
diff --git a/spec/frontend/lib/utils/forms_spec.js b/spec/frontend/lib/utils/forms_spec.js
index 2f71b26b29a..b97f5bf3c51 100644
--- a/spec/frontend/lib/utils/forms_spec.js
+++ b/spec/frontend/lib/utils/forms_spec.js
@@ -1,7 +1,12 @@
import {
serializeForm,
serializeFormObject,
+ safeTrim,
isEmptyValue,
+ hasMinimumLength,
+ isParseableAsInteger,
+ isIntegerGreaterThan,
+ isEmail,
parseRailsFormFields,
} from '~/lib/utils/forms';
@@ -99,6 +104,22 @@ describe('lib/utils/forms', () => {
});
});
+ describe('safeTrim', () => {
+ it.each`
+ input | returnValue
+ ${''} | ${''}
+ ${[]} | ${[]}
+ ${null} | ${null}
+ ${undefined} | ${undefined}
+ ${' '} | ${''}
+ ${'hello '} | ${'hello'}
+ ${'hello'} | ${'hello'}
+ ${0} | ${0}
+ `('returns $returnValue for value $input', ({ input, returnValue }) => {
+ expect(safeTrim(input)).toEqual(returnValue);
+ });
+ });
+
describe('isEmptyValue', () => {
it.each`
input | returnValue
@@ -106,14 +127,102 @@ describe('lib/utils/forms', () => {
${[]} | ${true}
${null} | ${true}
${undefined} | ${true}
+ ${' '} | ${true}
${'hello'} | ${false}
- ${' '} | ${false}
${0} | ${false}
`('returns $returnValue for value $input', ({ input, returnValue }) => {
expect(isEmptyValue(input)).toBe(returnValue);
});
});
+ describe('hasMinimumLength', () => {
+ it.each`
+ input | minLength | returnValue
+ ${['o', 't']} | ${1} | ${true}
+ ${'hello'} | ${3} | ${true}
+ ${' '} | ${2} | ${false}
+ ${''} | ${0} | ${false}
+ ${''} | ${8} | ${false}
+ ${[]} | ${0} | ${false}
+ ${null} | ${8} | ${false}
+ ${undefined} | ${8} | ${false}
+ ${'hello'} | ${8} | ${false}
+ ${0} | ${8} | ${false}
+ ${4} | ${1} | ${false}
+ `(
+ 'returns $returnValue for value $input and minLength $minLength',
+ ({ input, minLength, returnValue }) => {
+ expect(hasMinimumLength(input, minLength)).toBe(returnValue);
+ },
+ );
+ });
+
+ describe('isPareseableInteger', () => {
+ it.each`
+ input | returnValue
+ ${'0'} | ${true}
+ ${'12'} | ${true}
+ ${''} | ${false}
+ ${[]} | ${false}
+ ${null} | ${false}
+ ${undefined} | ${false}
+ ${'hello'} | ${false}
+ ${' '} | ${false}
+ ${'12.4'} | ${false}
+ ${'12ef'} | ${false}
+ `('returns $returnValue for value $input', ({ input, returnValue }) => {
+ expect(isParseableAsInteger(input)).toBe(returnValue);
+ });
+ });
+
+ describe('isIntegerGreaterThan', () => {
+ it.each`
+ input | greaterThan | returnValue
+ ${25} | ${8} | ${true}
+ ${'25'} | ${8} | ${true}
+ ${'4'} | ${1} | ${true}
+ ${'4'} | ${8} | ${false}
+ ${'9.5'} | ${8} | ${false}
+ ${'9.5e'} | ${8} | ${false}
+ ${['o', 't']} | ${0} | ${false}
+ ${'hello'} | ${0} | ${false}
+ ${' '} | ${0} | ${false}
+ ${''} | ${0} | ${false}
+ ${''} | ${8} | ${false}
+ ${[]} | ${0} | ${false}
+ ${null} | ${0} | ${false}
+ ${undefined} | ${0} | ${false}
+ ${'hello'} | ${0} | ${false}
+ ${0} | ${0} | ${false}
+ `(
+ 'returns $returnValue for value $input and greaterThan $greaterThan',
+ ({ input, greaterThan, returnValue }) => {
+ expect(isIntegerGreaterThan(input, greaterThan)).toBe(returnValue);
+ },
+ );
+ });
+
+ describe('isEmail', () => {
+ it.each`
+ input | returnValue
+ ${'user-with_special-chars@example.com'} | ${true}
+ ${'user@subdomain.example.com'} | ${true}
+ ${'user@example.com'} | ${true}
+ ${'user@example.co'} | ${true}
+ ${'user@example.c'} | ${false}
+ ${'user@example'} | ${false}
+ ${''} | ${false}
+ ${[]} | ${false}
+ ${null} | ${false}
+ ${undefined} | ${false}
+ ${'hello'} | ${false}
+ ${' '} | ${false}
+ ${'12'} | ${false}
+ `('returns $returnValue for value $input', ({ input, returnValue }) => {
+ expect(isEmail(input)).toBe(returnValue);
+ });
+ });
+
describe('serializeFormObject', () => {
it('returns an serialized object', () => {
const form = {
diff --git a/spec/frontend/lib/utils/ref_validator_spec.js b/spec/frontend/lib/utils/ref_validator_spec.js
index 7185ebf0a24..97896d74dff 100644
--- a/spec/frontend/lib/utils/ref_validator_spec.js
+++ b/spec/frontend/lib/utils/ref_validator_spec.js
@@ -65,9 +65,6 @@ describe('~/lib/utils/ref_validator', () => {
['foo.123.', validationMessages.DisallowedSequencePostfixesValidationMessage],
['foo/', validationMessages.DisallowedPostfixesValidationMessage],
-
- ['control-character\x7f', validationMessages.ControlCharactersValidationMessage],
- ['control-character\x15', validationMessages.ControlCharactersValidationMessage],
])('tag with name "%s"', (tagName, validationMessage) => {
it(`should be invalid with validation message "${validationMessage}"`, () => {
const result = validateTag(tagName);
@@ -75,5 +72,25 @@ describe('~/lib/utils/ref_validator', () => {
expect(result.validationErrors).toContain(validationMessage);
});
});
+
+ // NOTE: control characters cannot be used in test names because they cause test report XML parsing errors
+ describe.each([
+ [
+ 'control-character x7f',
+ 'control-character\x7f',
+ validationMessages.ControlCharactersValidationMessage,
+ ],
+ [
+ 'control-character x15',
+ 'control-character\x15',
+ validationMessages.ControlCharactersValidationMessage,
+ ],
+ ])('tag with name "%s"', (_, tagName, validationMessage) => {
+ it(`should be invalid with validation message "${validationMessage}"`, () => {
+ const result = validateTag(tagName);
+ expect(result.isValid).toBe(false);
+ expect(result.validationErrors).toContain(validationMessage);
+ });
+ });
});
});
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index e3c89bfed53..efc8c9b4459 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -221,9 +221,11 @@ describe('MembersTable', () => {
'col-actions',
'gl-display-none!',
'gl-lg-display-table-cell!',
+ 'gl-vertical-align-middle!',
]);
expect(findTableCellByMemberId('Actions', members[1].id).classes()).toStrictEqual([
'col-actions',
+ 'gl-vertical-align-middle!',
]);
});
});
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index 1285404fd9f..fa188f50d54 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -238,6 +238,16 @@ describe('RoleDropdown', () => {
it('does not call updateMemberRole', () => {
expect(actions.updateMemberRole).not.toHaveBeenCalled();
});
+
+ it('re-enables dropdown', async () => {
+ await waitForPromises();
+
+ expect(findListbox().props('disabled')).toBe(false);
+ });
+
+ it('resets selected dropdown item', () => {
+ expect(findListbox().props('selected')).toBe(member.validRoles.Owner);
+ });
});
});
});
diff --git a/spec/frontend/merge_requests/generated_content_spec.js b/spec/frontend/merge_requests/generated_content_spec.js
new file mode 100644
index 00000000000..f56a67ec466
--- /dev/null
+++ b/spec/frontend/merge_requests/generated_content_spec.js
@@ -0,0 +1,310 @@
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+
+import { MergeRequestGeneratedContent } from '~/merge_requests/generated_content';
+
+function findWarningElement() {
+ return document.querySelector('.js-ai-description-warning');
+}
+
+function findCloseButton() {
+ return findWarningElement()?.querySelector('.js-close-btn');
+}
+
+function findApprovalButton() {
+ return findWarningElement()?.querySelector('.js-ai-override-description');
+}
+
+function findCancelButton() {
+ return findWarningElement()?.querySelector('.js-cancel-btn');
+}
+
+function clickButton(button) {
+ button.dispatchEvent(new Event('click'));
+}
+
+describe('MergeRequestGeneratedContent', () => {
+ const warningDOM = `
+
+<div class="js-ai-description-warning hidden">
+ <button class="js-close-btn">X</button>
+ <button class="js-ai-override-description">Do AI</button>
+ <button class="js-cancel-btn">Cancel</button>
+</div>
+
+`;
+
+ describe('class basics', () => {
+ let gen;
+
+ beforeEach(() => {
+ gen = new MergeRequestGeneratedContent();
+ });
+
+ it.each`
+ description | property
+ ${'with no editor'} | ${'hasEditor'}
+ ${'with no warning'} | ${'hasWarning'}
+ ${'unable to replace the content'} | ${'canReplaceContent'}
+ `('begins $description', ({ property }) => {
+ expect(gen[property]).toBe(false);
+ });
+ });
+
+ describe('the internal editor representation', () => {
+ let gen;
+
+ it('accepts an editor during construction', () => {
+ gen = new MergeRequestGeneratedContent({ editor: {} });
+
+ expect(gen.hasEditor).toBe(true);
+ });
+
+ it('allows adding an editor through a public API after construction', () => {
+ gen = new MergeRequestGeneratedContent();
+
+ expect(gen.hasEditor).toBe(false);
+
+ gen.setEditor({});
+
+ expect(gen.hasEditor).toBe(true);
+ });
+ });
+
+ describe('generated content', () => {
+ let gen;
+
+ beforeEach(() => {
+ gen = new MergeRequestGeneratedContent();
+ });
+
+ it('can be provided to the instance through a public API', () => {
+ expect(gen.generatedContent).toBe(null);
+
+ gen.setGeneratedContent('generated content');
+
+ expect(gen.generatedContent).toBe('generated content');
+ });
+
+ it('can be cleared from the instance through a public API', () => {
+ gen.setGeneratedContent('generated content');
+
+ expect(gen.generatedContent).toBe('generated content');
+
+ gen.clearGeneratedContent();
+
+ expect(gen.generatedContent).toBe(null);
+ });
+ });
+
+ describe('warning element', () => {
+ let gen;
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it.each`
+ presence | withFixture
+ ${'is'} | ${true}
+ ${'is not'} | ${false}
+ `('`.hasWarning` is $withFixture when the element $presence in the DOM', ({ withFixture }) => {
+ if (withFixture) {
+ setHTMLFixture(warningDOM);
+ }
+
+ gen = new MergeRequestGeneratedContent();
+
+ expect(gen.hasWarning).toBe(withFixture);
+ });
+ });
+
+ describe('special cases', () => {
+ it.each`
+ description | value | props
+ ${'there is no internal editor representation, and no generated content'} | ${false} | ${{}}
+ ${'there is an internal editor representation, but no generated content'} | ${false} | ${{ editor: {} }}
+ ${'there is no internal editor representation, but there is generated content'} | ${false} | ${{ content: 'generated content' }}
+ ${'there is an internal editor representation, and there is generated content'} | ${true} | ${{ editor: {}, content: 'generated content' }}
+ `('`.canReplaceContent` is $value when $description', ({ value, props }) => {
+ const gen = new MergeRequestGeneratedContent();
+
+ if (props.editor) {
+ gen.setEditor(props.editor);
+ }
+ if (props.content) {
+ gen.setGeneratedContent(props.content);
+ }
+
+ expect(gen.canReplaceContent).toBe(value);
+ });
+ });
+
+ describe('behaviors', () => {
+ describe('UI', () => {
+ describe('warning element', () => {
+ let gen;
+
+ beforeEach(() => {
+ setHTMLFixture(warningDOM);
+ gen = new MergeRequestGeneratedContent({ editor: {} });
+
+ gen.setGeneratedContent('generated content');
+ });
+
+ describe('#showWarning', () => {
+ it("shows the warning if it exists in the DOM and if it's possible to replace the description", () => {
+ gen.showWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(false);
+ });
+
+ it("does nothing if the warning doesn't exist or if it's not possible to replace the description", () => {
+ gen.setEditor(null);
+
+ gen.showWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(true);
+
+ gen.setEditor({});
+ gen.setGeneratedContent(null);
+
+ gen.showWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(true);
+
+ resetHTMLFixture();
+ gen = new MergeRequestGeneratedContent({ editor: {} });
+ gen.setGeneratedContent('generated content');
+
+ expect(() => gen.showWarning()).not.toThrow();
+ expect(findWarningElement()).toBe(null);
+ });
+ });
+
+ describe('#hideWarning', () => {
+ it('hides the warning', () => {
+ findWarningElement().classList.remove('hidden');
+
+ gen.hideWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(true);
+ });
+
+ it("does nothing if there's no warning element", () => {
+ resetHTMLFixture();
+ gen = new MergeRequestGeneratedContent();
+
+ expect(() => gen.hideWarning()).not.toThrow();
+ expect(findWarningElement()).toBe(null);
+ });
+ });
+ });
+ });
+
+ describe('content', () => {
+ const editor = {};
+ let gen;
+
+ beforeEach(() => {
+ editor.setValue = jest.fn();
+ gen = new MergeRequestGeneratedContent({ editor });
+ });
+
+ describe('#replaceDescription', () => {
+ it("sets the instance's generated content value to the internal representation of the editor", () => {
+ gen.setGeneratedContent('generated content');
+
+ gen.replaceDescription();
+
+ expect(editor.setValue).toHaveBeenCalledWith('generated content');
+ });
+
+ it("does nothing if there's no editor or no generated content", () => {
+ // Starts with editor, but no content
+ gen.replaceDescription();
+
+ expect(editor.setValue).not.toHaveBeenCalled();
+
+ gen.setGeneratedContent('generated content');
+ gen.setEditor(null);
+
+ gen.replaceDescription();
+
+ expect(editor.setValue).not.toHaveBeenCalled();
+ });
+
+ it("clears the generated content so the warning can't be re-shown with stale content", () => {
+ gen.setGeneratedContent('generated content');
+
+ gen.replaceDescription();
+
+ expect(editor.setValue).toHaveBeenCalledWith('generated content');
+ expect(gen.hasEditor).toBe(true);
+ expect(gen.canReplaceContent).toBe(false);
+ expect(gen.generatedContent).toBe(null);
+ });
+ });
+ });
+ });
+
+ describe('events', () => {
+ describe('UI clicks', () => {
+ const editor = {};
+ let gen;
+
+ beforeEach(() => {
+ setHTMLFixture(warningDOM);
+ editor.setValue = jest.fn();
+ gen = new MergeRequestGeneratedContent({ editor });
+
+ gen.setGeneratedContent('generated content');
+ });
+
+ describe('banner close button', () => {
+ it('hides the warning element', () => {
+ const close = findCloseButton();
+
+ gen.showWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(false);
+
+ clickButton(close);
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(true);
+ });
+ });
+
+ describe('banner approval button', () => {
+ it('sends the generated content to the editor, clears the internal generated content, and hides the warning', () => {
+ const approve = findApprovalButton();
+
+ gen.showWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(false);
+ expect(gen.generatedContent).toBe('generated content');
+ expect(editor.setValue).not.toHaveBeenCalled();
+
+ clickButton(approve);
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(true);
+ expect(gen.generatedContent).toBe(null);
+ expect(editor.setValue).toHaveBeenCalledWith('generated content');
+ });
+ });
+
+ describe('banner cancel button', () => {
+ it('hides the warning element', () => {
+ const cancel = findCancelButton();
+
+ gen.showWarning();
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(false);
+
+ clickButton(cancel);
+
+ expect(findWarningElement().classList.contains('hidden')).toBe(true);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js b/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
new file mode 100644
index 00000000000..d1715ccd8f1
--- /dev/null
+++ b/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
@@ -0,0 +1,39 @@
+import { GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import MlModelsIndexApp from '~/ml/model_registry/routes/models/index';
+import { TITLE_LABEL } from '~/ml/model_registry/routes/models/index/translations';
+import { mockModels } from './mock_data';
+
+let wrapper;
+const createWrapper = (models = mockModels) => {
+ wrapper = shallowMountExtended(MlModelsIndexApp, {
+ propsData: { models },
+ });
+};
+
+const findModelLink = (index) => wrapper.findAllComponents(GlLink).at(index);
+const modelLinkText = (index) => findModelLink(index).text();
+const modelLinkHref = (index) => findModelLink(index).attributes('href');
+const findTitle = () => wrapper.findByText(TITLE_LABEL);
+
+describe('MlModelsIndex', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ describe('header', () => {
+ it('displays the title', () => {
+ expect(findTitle().exists()).toBe(true);
+ });
+ });
+
+ describe('model list', () => {
+ it('displays the models', () => {
+ expect(modelLinkHref(0)).toBe(mockModels[0].path);
+ expect(modelLinkText(0)).toBe(`${mockModels[0].name} / ${mockModels[0].version}`);
+
+ expect(modelLinkHref(1)).toBe(mockModels[1].path);
+ expect(modelLinkText(1)).toBe(`${mockModels[1].name} / ${mockModels[1].version}`);
+ });
+ });
+});
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js b/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js
new file mode 100644
index 00000000000..b8a999abbbd
--- /dev/null
+++ b/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js
@@ -0,0 +1,12 @@
+export const mockModels = [
+ {
+ name: 'model_1',
+ version: '1.0',
+ path: 'path/to/model_1',
+ },
+ {
+ name: 'model_2',
+ version: '1.0',
+ path: 'path/to/model_2',
+ },
+];
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
deleted file mode 100644
index 3b4554700b4..00000000000
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ /dev/null
@@ -1,155 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Dashboard template matches the default snapshot 1`] = `
-<div
- class="prometheus-graphs"
- data-testid="prometheus-graphs"
- environmentstate="available"
- metricsdashboardbasepath="/monitoring/monitor-project/-/metrics?environment=1"
- metricsendpoint="/monitoring/monitor-project/-/environments/1/additional_metrics.json"
->
- <div>
- <gl-alert-stub
- class="mb-3"
- dismissible="true"
- dismisslabel="Dismiss"
- primarybuttonlink=""
- primarybuttontext=""
- secondarybuttonlink=""
- secondarybuttontext=""
- showicon="true"
- title="Feature deprecation"
- variant="warning"
- >
- <gl-sprintf-stub
- message="The metrics feature was deprecated in GitLab 14.7."
- />
-
- <gl-sprintf-stub
- message="For information on a possible replacement %{epicStart} learn more about Opstrace %{epicEnd}."
- />
- </gl-alert-stub>
- </div>
-
- <div
- class="prometheus-graphs-header d-sm-flex flex-sm-wrap pt-2 pr-1 pb-0 pl-2 border-bottom bg-gray-light"
- >
- <div
- class="gl-mb-3 gl-mr-3 gl-display-flex gl-sm-display-block"
- >
- <dashboards-dropdown-stub
- class="flex-grow-1"
- defaultbranch="master"
- id="monitor-dashboards-dropdown"
- toggle-class="dropdown-menu-toggle"
- />
- </div>
-
- <span
- aria-hidden="true"
- class="gl-pl-3 border-left gl-mb-3 d-none d-sm-block"
- />
-
- <div
- class="mb-2 pr-2 d-flex d-sm-block"
- >
- <gl-dropdown-stub
- category="primary"
- class="flex-grow-1"
- clearalltext="Clear all"
- clearalltextclass="gl-px-5"
- data-testid="environments-dropdown"
- headertext=""
- hideheaderborder="true"
- highlighteditemstitle="Selected"
- highlighteditemstitleclass="gl-px-5"
- id="monitor-environments-dropdown"
- menu-class="monitor-environment-dropdown-menu"
- size="medium"
- text="production"
- toggleclass="dropdown-menu-toggle"
- variant="default"
- >
- <div
- class="d-flex flex-column overflow-hidden"
- >
- <gl-dropdown-section-header-stub>
- Environment
- </gl-dropdown-section-header-stub>
-
- <gl-search-box-by-type-stub
- clearbuttontitle="Clear"
- value=""
- />
-
- <div
- class="flex-fill overflow-auto"
- />
-
- <div
- class="text-secondary no-matches-message"
- >
-
- No matching results
-
- </div>
- </div>
- </gl-dropdown-stub>
- </div>
-
- <div
- class="mb-2 pr-2 d-flex d-sm-block"
- >
- <date-time-picker-stub
- class="flex-grow-1 show-last-dropdown"
- customenabled="true"
- options="[object Object],[object Object],[object Object],[object Object],[object Object],[object Object],[object Object]"
- value="[object Object]"
- />
- </div>
-
- <div
- class="mb-2 pr-2 d-flex d-sm-block"
- >
- <refresh-button-stub />
- </div>
-
- <div
- class="flex-grow-1"
- />
-
- <div
- class="d-sm-flex"
- >
- <!---->
-
- <!---->
-
- <div
- class="gl-mb-3 gl-mr-3 d-flex d-sm-block"
- >
- <actions-menu-stub
- custommetricspath="/monitoring/monitor-project/prometheus/metrics"
- defaultbranch="master"
- isootbdashboard="true"
- validatequerypath="/monitoring/monitor-project/prometheus/metrics/validate_query"
- />
- </div>
-
- <!---->
- </div>
- </div>
-
- <empty-state-stub
- clusterspath="/monitoring/monitor-project/-/clusters"
- documentationpath="/help/administration/monitoring/prometheus/index.md"
- emptygettingstartedsvgpath="/images/illustrations/monitoring/getting_started.svg"
- emptyloadingsvgpath="/images/illustrations/monitoring/loading.svg"
- emptynodatasmallsvgpath="/images/illustrations/chart-empty-state-small.svg"
- emptynodatasvgpath="/images/illustrations/monitoring/no_data.svg"
- emptyunabletoconnectsvgpath="/images/illustrations/monitoring/unable_to_connect.svg"
- selectedstate="gettingStarted"
- settingspath="/monitoring/monitor-project/-/settings/integrations/prometheus/edit"
- />
-</div>
-`;
diff --git a/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
deleted file mode 100644
index 4483c9fd39f..00000000000
--- a/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
+++ /dev/null
@@ -1,55 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`EmptyState shows gettingStarted state 1`] = `
-<div>
- <!---->
-
- <gl-empty-state-stub
- contentclass=""
- description="Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments."
- invertindarkmode="true"
- primarybuttonlink="/clustersPath"
- primarybuttontext="Install on clusters"
- secondarybuttonlink="/settingsPath"
- secondarybuttontext="Configure existing installation"
- svgpath="/path/to/getting-started.svg"
- title="Get started with performance monitoring"
- />
-</div>
-`;
-
-exports[`EmptyState shows noData state 1`] = `
-<div>
- <!---->
-
- <gl-empty-state-stub
- contentclass=""
- description="You are connected to the Prometheus server, but there is currently no data to display."
- invertindarkmode="true"
- primarybuttonlink="/settingsPath"
- primarybuttontext="Configure Prometheus"
- secondarybuttonlink=""
- secondarybuttontext=""
- svgpath="/path/to/no-data.svg"
- title="No data found"
- />
-</div>
-`;
-
-exports[`EmptyState shows unableToConnect state 1`] = `
-<div>
- <!---->
-
- <gl-empty-state-stub
- contentclass=""
- description="Ensure connectivity is available from the GitLab server to the Prometheus server"
- invertindarkmode="true"
- primarybuttonlink="/documentationPath"
- primarybuttontext="View documentation"
- secondarybuttonlink="/settingsPath"
- secondarybuttontext="Configure Prometheus"
- svgpath="/path/to/unable-to-connect.svg"
- title="Unable to connect to Prometheus server"
- />
-</div>
-`;
diff --git a/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
deleted file mode 100644
index 42a16a39dfd..00000000000
--- a/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
+++ /dev/null
@@ -1,160 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`GroupEmptyState given state BAD_QUERY passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": null,
- "invertInDarkMode": true,
- "primaryButtonLink": "/path/to/settings",
- "primaryButtonText": "Verify configuration",
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "Query cannot be processed",
-}
-`;
-
-exports[`GroupEmptyState given state BAD_QUERY renders the slotted content 1`] = `
-<div>
- <div>
- The Prometheus server responded with "bad request". Please check your queries are correct and are supported in your Prometheus version.
- <a
- href="/path/to/docs"
- >
- More information
- </a>
- </div>
-</div>
-`;
-
-exports[`GroupEmptyState given state CONNECTION_FAILED passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": "We couldn't reach the Prometheus server. Either the server no longer exists or the configuration details need updating.",
- "invertInDarkMode": true,
- "primaryButtonLink": "/path/to/settings",
- "primaryButtonText": "Verify configuration",
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "Connection failed",
-}
-`;
-
-exports[`GroupEmptyState given state CONNECTION_FAILED renders the slotted content 1`] = `<div />`;
-
-exports[`GroupEmptyState given state FOO STATE passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": "An error occurred while loading the data. Please try again.",
- "invertInDarkMode": true,
- "primaryButtonLink": null,
- "primaryButtonText": null,
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "An error has occurred",
-}
-`;
-
-exports[`GroupEmptyState given state FOO STATE renders the slotted content 1`] = `<div />`;
-
-exports[`GroupEmptyState given state LOADING passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": "Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.",
- "invertInDarkMode": true,
- "primaryButtonLink": null,
- "primaryButtonText": null,
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "Waiting for performance data",
-}
-`;
-
-exports[`GroupEmptyState given state LOADING renders the slotted content 1`] = `<div />`;
-
-exports[`GroupEmptyState given state NO_DATA passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": null,
- "invertInDarkMode": true,
- "primaryButtonLink": null,
- "primaryButtonText": null,
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "No data to display",
-}
-`;
-
-exports[`GroupEmptyState given state NO_DATA renders the slotted content 1`] = `
-<div>
- <div>
- The data source is connected, but there is no data to display.
- <a
- href="/path/to/docs"
- >
- More information
- </a>
- </div>
-</div>
-`;
-
-exports[`GroupEmptyState given state TIMEOUT passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": null,
- "invertInDarkMode": true,
- "primaryButtonLink": null,
- "primaryButtonText": null,
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "Connection timed out",
-}
-`;
-
-exports[`GroupEmptyState given state TIMEOUT renders the slotted content 1`] = `
-<div>
- <div>
- Charts can't be displayed as the request for data has timed out.
- <a
- href="/path/to/docs"
- >
- More information
- </a>
- </div>
-</div>
-`;
-
-exports[`GroupEmptyState given state UNKNOWN_ERROR passes the expected props to GlEmptyState 1`] = `
-Object {
- "compact": true,
- "contentClass": Array [],
- "description": "An error occurred while loading the data. Please try again.",
- "invertInDarkMode": true,
- "primaryButtonLink": null,
- "primaryButtonText": null,
- "secondaryButtonLink": null,
- "secondaryButtonText": null,
- "svgHeight": null,
- "svgPath": "/path/to/empty-group-illustration.svg",
- "title": "An error has occurred",
-}
-`;
-
-exports[`GroupEmptyState given state UNKNOWN_ERROR renders the slotted content 1`] = `<div />`;
diff --git a/spec/frontend/monitoring/components/charts/annotations_spec.js b/spec/frontend/monitoring/components/charts/annotations_spec.js
deleted file mode 100644
index 1eac0935fe4..00000000000
--- a/spec/frontend/monitoring/components/charts/annotations_spec.js
+++ /dev/null
@@ -1,95 +0,0 @@
-import { generateAnnotationsSeries } from '~/monitoring/components/charts/annotations';
-import { deploymentData, annotationsData } from '../../mock_data';
-
-describe('annotations spec', () => {
- describe('generateAnnotationsSeries', () => {
- it('with default options', () => {
- const annotations = generateAnnotationsSeries();
-
- expect(annotations).toEqual(
- expect.objectContaining({
- type: 'scatter',
- yAxisIndex: 1,
- data: [],
- markLine: {
- data: [],
- symbol: 'none',
- silent: true,
- },
- }),
- );
- });
-
- it('when only deployments data is passed', () => {
- const annotations = generateAnnotationsSeries({ deployments: deploymentData });
-
- expect(annotations).toEqual(
- expect.objectContaining({
- type: 'scatter',
- yAxisIndex: 1,
- data: expect.any(Array),
- markLine: {
- data: [],
- symbol: 'none',
- silent: true,
- },
- }),
- );
-
- annotations.data.forEach((annotation) => {
- expect(annotation).toEqual(expect.any(Object));
- });
-
- expect(annotations.data).toHaveLength(deploymentData.length);
- });
-
- it('when only annotations data is passed', () => {
- const annotations = generateAnnotationsSeries({
- annotations: annotationsData,
- });
-
- expect(annotations).toEqual(
- expect.objectContaining({
- type: 'scatter',
- yAxisIndex: 1,
- data: expect.any(Array),
- markLine: expect.any(Object),
- markPoint: expect.any(Object),
- }),
- );
-
- annotations.markLine.data.forEach((annotation) => {
- expect(annotation).toEqual(expect.any(Object));
- });
-
- expect(annotations.data).toHaveLength(0);
- expect(annotations.markLine.data).toHaveLength(annotationsData.length);
- expect(annotations.markPoint.data).toHaveLength(annotationsData.length);
- });
-
- it('when deployments and annotations data is passed', () => {
- const annotations = generateAnnotationsSeries({
- deployments: deploymentData,
- annotations: annotationsData,
- });
-
- expect(annotations).toEqual(
- expect.objectContaining({
- type: 'scatter',
- yAxisIndex: 1,
- data: expect.any(Array),
- markLine: expect.any(Object),
- markPoint: expect.any(Object),
- }),
- );
-
- annotations.markLine.data.forEach((annotation) => {
- expect(annotation).toEqual(expect.any(Object));
- });
-
- expect(annotations.data).toHaveLength(deploymentData.length);
- expect(annotations.markLine.data).toHaveLength(annotationsData.length);
- expect(annotations.markPoint.data).toHaveLength(annotationsData.length);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js
deleted file mode 100644
index 3674a49f42c..00000000000
--- a/spec/frontend/monitoring/components/charts/anomaly_spec.js
+++ /dev/null
@@ -1,304 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { TEST_HOST } from 'helpers/test_constants';
-import Anomaly from '~/monitoring/components/charts/anomaly.vue';
-
-import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
-import { colorValues } from '~/monitoring/constants';
-import { anomalyGraphData } from '../../graph_data';
-import { anomalyDeploymentData, mockProjectDir } from '../../mock_data';
-
-const mockProjectPath = `${TEST_HOST}${mockProjectDir}`;
-
-const TEST_UPPER = 11;
-const TEST_LOWER = 9;
-
-describe('Anomaly chart component', () => {
- let wrapper;
-
- const setupAnomalyChart = (props) => {
- wrapper = shallowMount(Anomaly, {
- propsData: { ...props },
- });
- };
- const findTimeSeries = () => wrapper.findComponent(MonitorTimeSeriesChart);
- const getTimeSeriesProps = () => findTimeSeries().props();
-
- describe('wrapped monitor-time-series-chart component', () => {
- const mockValues = ['10', '10', '10'];
-
- const mockGraphData = anomalyGraphData(
- {},
- {
- upper: mockValues.map(() => String(TEST_UPPER)),
- values: mockValues,
- lower: mockValues.map(() => String(TEST_LOWER)),
- },
- );
-
- const inputThresholds = ['some threshold'];
-
- beforeEach(() => {
- setupAnomalyChart({
- graphData: mockGraphData,
- deploymentData: anomalyDeploymentData,
- thresholds: inputThresholds,
- projectPath: mockProjectPath,
- });
- });
-
- it('renders correctly', () => {
- expect(findTimeSeries().exists()).toBe(true);
- });
-
- describe('receives props correctly', () => {
- describe('graph-data', () => {
- it('receives a single "metric" series', () => {
- const { graphData } = getTimeSeriesProps();
- expect(graphData.metrics.length).toBe(1);
- });
-
- it('receives "metric" with all data', () => {
- const { graphData } = getTimeSeriesProps();
- const metric = graphData.metrics[0];
- const expectedMetric = mockGraphData.metrics[0];
- expect(metric).toEqual(expectedMetric);
- });
-
- it('receives the "metric" results', () => {
- const { graphData } = getTimeSeriesProps();
- const { result } = graphData.metrics[0];
- const { values } = result[0];
-
- expect(values).toEqual([
- [expect.any(String), 10],
- [expect.any(String), 10],
- [expect.any(String), 10],
- ]);
- });
- });
-
- describe('option', () => {
- let option;
- let series;
-
- beforeEach(() => {
- ({ option } = getTimeSeriesProps());
- ({ series } = option);
- });
-
- it('contains a boundary band', () => {
- expect(series).toEqual(expect.any(Array));
- expect(series.length).toEqual(2); // 1 upper + 1 lower boundaries
- expect(series[0].stack).toEqual(series[1].stack);
-
- series.forEach((s) => {
- expect(s.type).toBe('line');
- expect(s.lineStyle.width).toBe(0);
- expect(s.lineStyle.color).toMatch(/rgba\(.+\)/);
- expect(s.lineStyle.color).toMatch(s.color);
- expect(s.symbol).toEqual('none');
- });
- });
-
- it('upper boundary values are stacked on top of lower boundary', () => {
- const [lowerSeries, upperSeries] = series;
-
- lowerSeries.data.forEach(([, y]) => {
- expect(y).toBeCloseTo(TEST_LOWER);
- });
-
- upperSeries.data.forEach(([, y]) => {
- expect(y).toBeCloseTo(TEST_UPPER - TEST_LOWER);
- });
- });
- });
-
- describe('series-config', () => {
- let seriesConfig;
-
- beforeEach(() => {
- ({ seriesConfig } = getTimeSeriesProps());
- });
-
- it('display symbols is enabled', () => {
- expect(seriesConfig).toEqual(
- expect.objectContaining({
- type: 'line',
- symbol: 'circle',
- showSymbol: true,
- symbolSize: expect.any(Function),
- itemStyle: {
- color: expect.any(Function),
- },
- }),
- );
- });
-
- it('does not display anomalies', () => {
- const { symbolSize, itemStyle } = seriesConfig;
- mockValues.forEach((v, dataIndex) => {
- const size = symbolSize(null, { dataIndex });
- const color = itemStyle.color({ dataIndex });
-
- // normal color and small size
- expect(size).toBeCloseTo(0);
- expect(color).toBe(colorValues.primaryColor);
- });
- });
-
- it('can format y values (to use in tooltips)', () => {
- mockValues.forEach((v, dataIndex) => {
- const formatted = wrapper.vm.yValueFormatted(0, dataIndex);
- expect(parseFloat(formatted)).toEqual(parseFloat(v));
- });
- });
- });
-
- describe('inherited properties', () => {
- it('"deployment-data" keeps the same value', () => {
- const { deploymentData } = getTimeSeriesProps();
- expect(deploymentData).toEqual(anomalyDeploymentData);
- });
- it('"projectPath" keeps the same value', () => {
- const { projectPath } = getTimeSeriesProps();
- expect(projectPath).toEqual(mockProjectPath);
- });
- });
- });
- });
-
- describe('with no boundary data', () => {
- const noBoundaryData = anomalyGraphData(
- {},
- {
- upper: [],
- values: ['10', '10', '10'],
- lower: [],
- },
- );
-
- beforeEach(() => {
- setupAnomalyChart({
- graphData: noBoundaryData,
- deploymentData: anomalyDeploymentData,
- });
- });
-
- describe('option', () => {
- let option;
- let series;
-
- beforeEach(() => {
- ({ option } = getTimeSeriesProps());
- ({ series } = option);
- });
-
- it('does not display a boundary band', () => {
- expect(series).toEqual(expect.any(Array));
- expect(series.length).toEqual(0); // no boundaries
- });
-
- it('can format y values (to use in tooltips)', () => {
- expect(parseFloat(wrapper.vm.yValueFormatted(0, 0))).toEqual(10);
- expect(wrapper.vm.yValueFormatted(1, 0)).toBe(''); // missing boundary
- expect(wrapper.vm.yValueFormatted(2, 0)).toBe(''); // missing boundary
- });
- });
- });
-
- describe('with one anomaly', () => {
- const mockValues = ['10', '20', '10'];
-
- const oneAnomalyData = anomalyGraphData(
- {},
- {
- upper: mockValues.map(() => TEST_UPPER),
- values: mockValues,
- lower: mockValues.map(() => TEST_LOWER),
- },
- );
-
- beforeEach(() => {
- setupAnomalyChart({
- graphData: oneAnomalyData,
- deploymentData: anomalyDeploymentData,
- });
- });
-
- describe('series-config', () => {
- it('displays one anomaly', () => {
- const { seriesConfig } = getTimeSeriesProps();
- const { symbolSize, itemStyle } = seriesConfig;
-
- const bigDots = mockValues.filter((v, dataIndex) => {
- const size = symbolSize(null, { dataIndex });
- return size > 0.1;
- });
- const redDots = mockValues.filter((v, dataIndex) => {
- const color = itemStyle.color({ dataIndex });
- return color === colorValues.anomalySymbol;
- });
-
- expect(bigDots.length).toBe(1);
- expect(redDots.length).toBe(1);
- });
- });
- });
-
- describe('with offset', () => {
- const mockValues = ['10', '11', '12'];
- const mockUpper = ['20', '20', '20'];
- const mockLower = ['-1', '-2', '-3.70'];
- const expectedOffset = 4; // Lowest point in mock data is -3.70, it gets rounded
-
- beforeEach(() => {
- setupAnomalyChart({
- graphData: anomalyGraphData(
- {},
- {
- upper: mockUpper,
- values: mockValues,
- lower: mockLower,
- },
- ),
- deploymentData: anomalyDeploymentData,
- });
- });
-
- describe('receives props correctly', () => {
- describe('graph-data', () => {
- it('receives a single "metric" series', () => {
- const { graphData } = getTimeSeriesProps();
- expect(graphData.metrics.length).toBe(1);
- });
-
- it('receives "metric" results and applies the offset to them', () => {
- const { graphData } = getTimeSeriesProps();
- const { result } = graphData.metrics[0];
- const { values } = result[0];
-
- expect(values).toEqual(expect.any(Array));
-
- values.forEach(([, y], index) => {
- expect(y).toBeCloseTo(parseFloat(mockValues[index]) + expectedOffset);
- });
- });
- });
- });
-
- describe('option', () => {
- it('upper boundary values are stacked on top of lower boundary, plus the offset', () => {
- const { option } = getTimeSeriesProps();
- const { series } = option;
- const [lowerSeries, upperSeries] = series;
- lowerSeries.data.forEach(([, y], i) => {
- expect(y).toBeCloseTo(parseFloat(mockLower[i]) + expectedOffset);
- });
-
- upperSeries.data.forEach(([, y], i) => {
- expect(y).toBeCloseTo(parseFloat(mockUpper[i] - mockLower[i]));
- });
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/bar_spec.js b/spec/frontend/monitoring/components/charts/bar_spec.js
deleted file mode 100644
index 5339a7a525b..00000000000
--- a/spec/frontend/monitoring/components/charts/bar_spec.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import { GlBarChart } from '@gitlab/ui/dist/charts';
-import { shallowMount } from '@vue/test-utils';
-import Bar from '~/monitoring/components/charts/bar.vue';
-import { barGraphData } from '../../graph_data';
-
-jest.mock('~/lib/utils/icon_utils', () => ({
- getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'),
-}));
-
-describe('Bar component', () => {
- let barChart;
- let store;
- let graphData;
-
- beforeEach(() => {
- graphData = barGraphData();
-
- barChart = shallowMount(Bar, {
- propsData: {
- graphData,
- },
- store,
- });
- });
-
- afterEach(() => {
- barChart.destroy();
- });
-
- describe('wrapped components', () => {
- describe('GitLab UI bar chart', () => {
- let glbarChart;
- let chartData;
-
- beforeEach(() => {
- glbarChart = barChart.findComponent(GlBarChart);
- chartData = barChart.vm.chartData[graphData.metrics[0].label];
- });
-
- it('should display a label on the x axis', () => {
- expect(glbarChart.props('xAxisTitle')).toBe(graphData.xLabel);
- });
-
- it('should return chartData as array of arrays', () => {
- expect(chartData).toBeInstanceOf(Array);
-
- chartData.forEach((item) => {
- expect(item).toBeInstanceOf(Array);
- });
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/column_spec.js b/spec/frontend/monitoring/components/charts/column_spec.js
deleted file mode 100644
index cc38a3fd8a1..00000000000
--- a/spec/frontend/monitoring/components/charts/column_spec.js
+++ /dev/null
@@ -1,118 +0,0 @@
-import { GlColumnChart } from '@gitlab/ui/dist/charts';
-import { shallowMount } from '@vue/test-utils';
-import timezoneMock from 'timezone-mock';
-import ColumnChart from '~/monitoring/components/charts/column.vue';
-
-jest.mock('~/lib/utils/icon_utils', () => ({
- getSvgIconPathContent: jest.fn().mockResolvedValue('mockSvgPathContent'),
-}));
-
-const yAxisName = 'Y-axis mock name';
-const yAxisFormat = 'bytes';
-const yAxisPrecistion = 3;
-const dataValues = [
- [1495700554.925, '8.0390625'],
- [1495700614.925, '8.0390625'],
- [1495700674.925, '8.0390625'],
-];
-
-describe('Column component', () => {
- let wrapper;
-
- const createWrapper = (props = {}) => {
- wrapper = shallowMount(ColumnChart, {
- propsData: {
- graphData: {
- yAxis: {
- name: yAxisName,
- format: yAxisFormat,
- precision: yAxisPrecistion,
- },
- metrics: [
- {
- label: 'Mock data',
- result: [
- {
- metric: {},
- values: dataValues,
- },
- ],
- },
- ],
- },
- ...props,
- },
- });
- };
- const findChart = () => wrapper.findComponent(GlColumnChart);
- const chartProps = (prop) => findChart().props(prop);
-
- beforeEach(() => {
- createWrapper();
- });
-
- describe('xAxisLabel', () => {
- const mockDate = Date.UTC(2020, 4, 26, 20); // 8:00 PM in GMT
-
- const useXAxisFormatter = (date) => {
- const { xAxis } = chartProps('option');
- const { formatter } = xAxis.axisLabel;
- return formatter(date);
- };
-
- it('x-axis is formatted correctly in m/d h:MM TT format', () => {
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 8:00 PM');
- });
-
- describe('when in PT timezone', () => {
- beforeAll(() => {
- timezoneMock.register('US/Pacific');
- });
-
- afterAll(() => {
- timezoneMock.unregister();
- });
-
- it('by default, values are formatted in PT', () => {
- createWrapper();
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 1:00 PM');
- });
-
- it('when the chart uses local timezone, y-axis is formatted in PT', () => {
- createWrapper({ timezone: 'LOCAL' });
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 1:00 PM');
- });
-
- it('when the chart uses UTC, y-axis is formatted in UTC', () => {
- createWrapper({ timezone: 'UTC' });
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 8:00 PM');
- });
- });
- });
-
- describe('wrapped components', () => {
- describe('GitLab UI column chart', () => {
- it('receives data properties needed for proper chart render', () => {
- expect(chartProps('bars')).toEqual([{ name: 'Mock data', data: dataValues }]);
- });
-
- it('passes the y axis name correctly', () => {
- expect(chartProps('yAxisTitle')).toBe(yAxisName);
- });
-
- it('passes the y axis configuration correctly', () => {
- expect(chartProps('option').yAxis).toMatchObject({
- name: yAxisName,
- axisLabel: {
- formatter: expect.any(Function),
- },
- scale: false,
- });
- });
-
- it('passes a dataZoom configuration', () => {
- expect(chartProps('option').dataZoom).toBeDefined();
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/empty_chart_spec.js b/spec/frontend/monitoring/components/charts/empty_chart_spec.js
deleted file mode 100644
index d755ed7c104..00000000000
--- a/spec/frontend/monitoring/components/charts/empty_chart_spec.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import EmptyChart from '~/monitoring/components/charts/empty_chart.vue';
-
-describe('Empty Chart component', () => {
- let emptyChart;
- const graphTitle = 'Memory Usage';
-
- beforeEach(() => {
- emptyChart = shallowMount(EmptyChart, {
- propsData: {
- graphTitle,
- },
- });
- });
-
- describe('Computed props', () => {
- it('sets the height for the svg container', () => {
- expect(emptyChart.vm.svgContainerStyle.height).toBe('300px');
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/gauge_spec.js b/spec/frontend/monitoring/components/charts/gauge_spec.js
deleted file mode 100644
index 33ea5e83598..00000000000
--- a/spec/frontend/monitoring/components/charts/gauge_spec.js
+++ /dev/null
@@ -1,210 +0,0 @@
-import { GlGaugeChart } from '@gitlab/ui/dist/charts';
-import { shallowMount } from '@vue/test-utils';
-import GaugeChart from '~/monitoring/components/charts/gauge.vue';
-import { gaugeChartGraphData } from '../../graph_data';
-
-describe('Gauge Chart component', () => {
- const defaultGraphData = gaugeChartGraphData();
-
- let wrapper;
-
- const findGaugeChart = () => wrapper.findComponent(GlGaugeChart);
-
- const createWrapper = ({ ...graphProps } = {}) => {
- wrapper = shallowMount(GaugeChart, {
- propsData: {
- graphData: {
- ...defaultGraphData,
- ...graphProps,
- },
- },
- });
- };
-
- describe('chart component', () => {
- it('is rendered when props are passed', () => {
- createWrapper();
-
- expect(findGaugeChart().exists()).toBe(true);
- });
- });
-
- describe('min and max', () => {
- const MIN_DEFAULT = 0;
- const MAX_DEFAULT = 100;
-
- it('are passed to chart component', () => {
- createWrapper();
-
- expect(findGaugeChart().props('min')).toBe(100);
- expect(findGaugeChart().props('max')).toBe(1000);
- });
-
- const invalidCases = [undefined, NaN, 'a string'];
-
- it.each(invalidCases)(
- 'if min has invalid value, defaults are used for both min and max',
- (invalidValue) => {
- createWrapper({ minValue: invalidValue });
-
- expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT);
- expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT);
- },
- );
-
- it.each(invalidCases)(
- 'if max has invalid value, defaults are used for both min and max',
- (invalidValue) => {
- createWrapper({ minValue: invalidValue });
-
- expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT);
- expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT);
- },
- );
-
- it('if min is bigger than max, defaults are used for both min and max', () => {
- createWrapper({ minValue: 100, maxValue: 0 });
-
- expect(findGaugeChart().props('min')).toBe(MIN_DEFAULT);
- expect(findGaugeChart().props('max')).toBe(MAX_DEFAULT);
- });
- });
-
- describe('thresholds', () => {
- it('thresholds are set on chart', () => {
- createWrapper();
-
- expect(findGaugeChart().props('thresholds')).toEqual([500, 800]);
- });
-
- it('when no thresholds are defined, a default threshold is defined at 95% of max_value', () => {
- createWrapper({
- minValue: 0,
- maxValue: 100,
- thresholds: {},
- });
-
- expect(findGaugeChart().props('thresholds')).toEqual([95]);
- });
-
- it('when out of min-max bounds thresholds are defined, a default threshold is defined at 95% of the range between min_value and max_value', () => {
- createWrapper({
- thresholds: {
- values: [-10, 1500],
- },
- });
-
- expect(findGaugeChart().props('thresholds')).toEqual([855]);
- });
-
- describe('when mode is absolute', () => {
- it('only valid threshold values are used', () => {
- createWrapper({
- thresholds: {
- mode: 'absolute',
- values: [undefined, 10, 110, NaN, 'a string', 400],
- },
- });
-
- expect(findGaugeChart().props('thresholds')).toEqual([110, 400]);
- });
-
- it('if all threshold values are invalid, a default threshold is defined at 95% of the range between min_value and max_value', () => {
- createWrapper({
- thresholds: {
- mode: 'absolute',
- values: [NaN, undefined, 'a string', 1500],
- },
- });
-
- expect(findGaugeChart().props('thresholds')).toEqual([855]);
- });
- });
-
- describe('when mode is percentage', () => {
- it('when values outside of 0-100 bounds are used, a default threshold is defined at 95% of max_value', () => {
- createWrapper({
- thresholds: {
- mode: 'percentage',
- values: [110],
- },
- });
-
- expect(findGaugeChart().props('thresholds')).toEqual([855]);
- });
-
- it('if all threshold values are invalid, a default threshold is defined at 95% of max_value', () => {
- createWrapper({
- thresholds: {
- mode: 'percentage',
- values: [NaN, undefined, 'a string', 1500],
- },
- });
-
- expect(findGaugeChart().props('thresholds')).toEqual([855]);
- });
- });
- });
-
- describe('split (the number of ticks on the chart arc)', () => {
- const SPLIT_DEFAULT = 10;
-
- it('is passed to chart as prop', () => {
- createWrapper();
-
- expect(findGaugeChart().props('splitNumber')).toBe(20);
- });
-
- it('if not explicitly set, passes a default value to chart', () => {
- createWrapper({ split: '' });
-
- expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT);
- });
-
- it('if set as a number that is not an integer, passes the default value to chart', () => {
- createWrapper({ split: 10.5 });
-
- expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT);
- });
-
- it('if set as a negative number, passes the default value to chart', () => {
- createWrapper({ split: -10 });
-
- expect(findGaugeChart().props('splitNumber')).toBe(SPLIT_DEFAULT);
- });
- });
-
- describe('text (the text displayed on the gauge for the current value)', () => {
- it('displays the query result value when format is not set', () => {
- createWrapper({ format: '' });
-
- expect(findGaugeChart().props('text')).toBe('3');
- });
-
- it('displays the query result value when format is set to invalid value', () => {
- createWrapper({ format: 'invalid' });
-
- expect(findGaugeChart().props('text')).toBe('3');
- });
-
- it('displays a formatted query result value when format is set', () => {
- createWrapper();
-
- expect(findGaugeChart().props('text')).toBe('3kB');
- });
-
- it('displays a placeholder value when metric is empty', () => {
- createWrapper({ metrics: [] });
-
- expect(findGaugeChart().props('text')).toBe('--');
- });
- });
-
- describe('value', () => {
- it('correct value is passed', () => {
- createWrapper();
-
- expect(findGaugeChart().props('value')).toBe(3);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/heatmap_spec.js b/spec/frontend/monitoring/components/charts/heatmap_spec.js
deleted file mode 100644
index 54245cbdbc1..00000000000
--- a/spec/frontend/monitoring/components/charts/heatmap_spec.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import { GlHeatmap } from '@gitlab/ui/dist/charts';
-import { shallowMount } from '@vue/test-utils';
-import timezoneMock from 'timezone-mock';
-import Heatmap from '~/monitoring/components/charts/heatmap.vue';
-import { heatmapGraphData } from '../../graph_data';
-
-describe('Heatmap component', () => {
- let wrapper;
- let store;
-
- const findChart = () => wrapper.findComponent(GlHeatmap);
-
- const graphData = heatmapGraphData();
-
- const createWrapper = (props = {}) => {
- wrapper = shallowMount(Heatmap, {
- propsData: {
- graphData: heatmapGraphData(),
- containerWidth: 100,
- ...props,
- },
- store,
- });
- };
-
- describe('wrapped chart', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('should display a label on the x axis', () => {
- expect(wrapper.vm.xAxisName).toBe(graphData.xLabel);
- });
-
- it('should display a label on the y axis', () => {
- expect(wrapper.vm.yAxisName).toBe(graphData.y_label);
- });
-
- // According to the echarts docs https://echarts.apache.org/en/option.html#series-heatmap.data
- // each row of the heatmap chart is represented by an array inside another parent array
- // e.g. [[0, 0, 10]], the format represents the column, the row and finally the value
- // corresponding to the cell
-
- it('should return chartData with a length of x by y, with a length of 3 per array', () => {
- const row = wrapper.vm.chartData[0];
-
- expect(row.length).toBe(3);
- expect(wrapper.vm.chartData.length).toBe(6);
- });
-
- it('returns a series of labels for the x axis', () => {
- const { xAxisLabels } = wrapper.vm;
-
- expect(xAxisLabels.length).toBe(2);
- });
-
- describe('y axis labels', () => {
- const gmtLabels = ['8:10 PM', '8:12 PM', '8:14 PM'];
-
- it('y-axis labels are formatted in AM/PM format', () => {
- expect(findChart().props('yAxisLabels')).toEqual(gmtLabels);
- });
-
- describe('when in PT timezone', () => {
- const ptLabels = ['1:10 PM', '1:12 PM', '1:14 PM'];
- const utcLabels = gmtLabels; // Identical in this case
-
- beforeAll(() => {
- timezoneMock.register('US/Pacific');
- });
-
- afterAll(() => {
- timezoneMock.unregister();
- });
-
- it('by default, y-axis is formatted in PT', () => {
- createWrapper();
- expect(findChart().props('yAxisLabels')).toEqual(ptLabels);
- });
-
- it('when the chart uses local timezone, y-axis is formatted in PT', () => {
- createWrapper({ timezone: 'LOCAL' });
- expect(findChart().props('yAxisLabels')).toEqual(ptLabels);
- });
-
- it('when the chart uses UTC, y-axis is formatted in UTC', () => {
- createWrapper({ timezone: 'UTC' });
- expect(findChart().props('yAxisLabels')).toEqual(utcLabels);
- });
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/options_spec.js b/spec/frontend/monitoring/components/charts/options_spec.js
deleted file mode 100644
index 064ce6f204c..00000000000
--- a/spec/frontend/monitoring/components/charts/options_spec.js
+++ /dev/null
@@ -1,327 +0,0 @@
-import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
-import {
- getYAxisOptions,
- getTooltipFormatter,
- getValidThresholds,
-} from '~/monitoring/components/charts/options';
-
-describe('options spec', () => {
- describe('getYAxisOptions', () => {
- it('default options', () => {
- const options = getYAxisOptions();
-
- expect(options).toMatchObject({
- name: expect.any(String),
- axisLabel: {
- formatter: expect.any(Function),
- },
- scale: true,
- boundaryGap: [expect.any(Number), expect.any(Number)],
- });
-
- expect(options.name).not.toHaveLength(0);
- });
-
- it('name options', () => {
- const yAxisName = 'My axis values';
- const options = getYAxisOptions({
- name: yAxisName,
- });
-
- expect(options).toMatchObject({
- name: yAxisName,
- nameLocation: 'center',
- nameGap: expect.any(Number),
- });
- });
-
- it('formatter options defaults to engineering notation', () => {
- const options = getYAxisOptions();
-
- expect(options.axisLabel.formatter).toEqual(expect.any(Function));
- expect(options.axisLabel.formatter(3002.1)).toBe('3k');
- });
-
- it('formatter options allows for precision to be set explicitly', () => {
- const options = getYAxisOptions({
- precision: 4,
- });
-
- expect(options.axisLabel.formatter).toEqual(expect.any(Function));
- expect(options.axisLabel.formatter(5002.1)).toBe('5.0021k');
- });
-
- it('formatter options allows for overrides in milliseconds', () => {
- const options = getYAxisOptions({
- format: SUPPORTED_FORMATS.milliseconds,
- });
-
- expect(options.axisLabel.formatter).toEqual(expect.any(Function));
- expect(options.axisLabel.formatter(1.1234)).toBe('1.12ms');
- });
-
- it('formatter options allows for overrides in bytes', () => {
- const options = getYAxisOptions({
- format: SUPPORTED_FORMATS.bytes,
- });
-
- expect(options.axisLabel.formatter).toEqual(expect.any(Function));
- expect(options.axisLabel.formatter(1)).toBe('1.00B');
- });
- });
-
- describe('getTooltipFormatter', () => {
- it('default format', () => {
- const formatter = getTooltipFormatter();
-
- expect(formatter).toEqual(expect.any(Function));
- expect(formatter(0.11111)).toBe('111.1m');
- });
-
- it('defined format', () => {
- const formatter = getTooltipFormatter({
- format: SUPPORTED_FORMATS.bytes,
- });
-
- expect(formatter(1)).toBe('1.000B');
- });
- });
-
- describe('getValidThresholds', () => {
- const invalidCases = [null, undefined, NaN, 'a string', true, false];
-
- let thresholds;
-
- afterEach(() => {
- thresholds = null;
- });
-
- it('returns same thresholds when passed values within range', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [10, 50],
- });
-
- expect(thresholds).toEqual([10, 50]);
- });
-
- it('filters out thresholds that are out of range', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [-5, 10, 110],
- });
-
- expect(thresholds).toEqual([10]);
- });
- it('filters out duplicate thresholds', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [5, 5, 10, 10],
- });
-
- expect(thresholds).toEqual([5, 10]);
- });
-
- it('sorts passed thresholds and applies only the first two in ascending order', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [10, 1, 35, 20, 5],
- });
-
- expect(thresholds).toEqual([1, 5]);
- });
-
- it('thresholds equal to min or max are filtered out', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [0, 100],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it.each(invalidCases)('invalid values for thresholds are filtered out', (invalidValue) => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [10, invalidValue],
- });
-
- expect(thresholds).toEqual([10]);
- });
-
- describe('range', () => {
- it('when range is not defined, empty result is returned', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- values: [10, 20],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it('when min is not defined, empty result is returned', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { max: 100 },
- values: [10, 20],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it('when max is not defined, empty result is returned', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0 },
- values: [10, 20],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it('when min is larger than max, empty result is returned', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 100, max: 0 },
- values: [10, 20],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it.each(invalidCases)(
- 'when min has invalid value, empty result is returned',
- (invalidValue) => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: invalidValue, max: 100 },
- values: [10, 20],
- });
-
- expect(thresholds).toEqual([]);
- },
- );
-
- it.each(invalidCases)(
- 'when max has invalid value, empty result is returned',
- (invalidValue) => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: invalidValue },
- values: [10, 20],
- });
-
- expect(thresholds).toEqual([]);
- },
- );
- });
-
- describe('values', () => {
- it('if values parameter is omitted, empty result is returned', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it('if there are no values passed, empty result is returned', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- it.each(invalidCases)(
- 'if invalid values are passed, empty result is returned',
- (invalidValue) => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [invalidValue],
- });
-
- expect(thresholds).toEqual([]);
- },
- );
- });
-
- describe('mode', () => {
- it.each(invalidCases)(
- 'if invalid values are passed, empty result is returned',
- (invalidValue) => {
- thresholds = getValidThresholds({
- mode: invalidValue,
- range: { min: 0, max: 100 },
- values: [10, 50],
- });
-
- expect(thresholds).toEqual([]);
- },
- );
-
- it('if mode is not passed, empty result is returned', () => {
- thresholds = getValidThresholds({
- range: { min: 0, max: 100 },
- values: [10, 50],
- });
-
- expect(thresholds).toEqual([]);
- });
-
- describe('absolute mode', () => {
- it('absolute mode behaves correctly', () => {
- thresholds = getValidThresholds({
- mode: 'absolute',
- range: { min: 0, max: 100 },
- values: [10, 50],
- });
-
- expect(thresholds).toEqual([10, 50]);
- });
- });
-
- describe('percentage mode', () => {
- it('percentage mode behaves correctly', () => {
- thresholds = getValidThresholds({
- mode: 'percentage',
- range: { min: 0, max: 1000 },
- values: [10, 50],
- });
-
- expect(thresholds).toEqual([100, 500]);
- });
-
- const outOfPercentBoundsValues = [-1, 0, 100, 101];
- it.each(outOfPercentBoundsValues)(
- 'when values out of 0-100 range are passed, empty result is returned',
- (invalidValue) => {
- thresholds = getValidThresholds({
- mode: 'percentage',
- range: { min: 0, max: 1000 },
- values: [invalidValue],
- });
-
- expect(thresholds).toEqual([]);
- },
- );
- });
- });
-
- it('calling without passing object parameter returns empty array', () => {
- thresholds = getValidThresholds();
-
- expect(thresholds).toEqual([]);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js
deleted file mode 100644
index fa31b479296..00000000000
--- a/spec/frontend/monitoring/components/charts/single_stat_spec.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import { GlSingleStat } from '@gitlab/ui/dist/charts';
-import { shallowMount } from '@vue/test-utils';
-import SingleStatChart from '~/monitoring/components/charts/single_stat.vue';
-import { singleStatGraphData } from '../../graph_data';
-
-describe('Single Stat Chart component', () => {
- let wrapper;
-
- const createComponent = (props = {}) => {
- wrapper = shallowMount(SingleStatChart, {
- propsData: {
- graphData: singleStatGraphData({}, { unit: 'MB' }),
- ...props,
- },
- });
- };
-
- const findChart = () => wrapper.findComponent(GlSingleStat);
-
- beforeEach(() => {
- createComponent();
- });
-
- describe('computed', () => {
- describe('statValue', () => {
- it('should display the correct value', () => {
- expect(findChart().props('value')).toBe('1.00');
- });
-
- it('should display the correct value unit', () => {
- expect(findChart().props('unit')).toBe('MB');
- });
-
- it('should change the value representation to a percentile one', () => {
- createComponent({
- graphData: singleStatGraphData({ max_value: 120 }, { value: 91 }),
- });
-
- expect(findChart().props('value')).toBe('75.83');
- expect(findChart().props('unit')).toBe('%');
- });
-
- it('should display NaN for non numeric maxValue values', () => {
- createComponent({
- graphData: singleStatGraphData({ max_value: 'not a number' }),
- });
-
- expect(findChart().props('value')).toContain('NaN');
- });
-
- it('should display NaN for missing query values', () => {
- createComponent({
- graphData: singleStatGraphData({ max_value: 120 }, { value: 'NaN' }),
- });
-
- expect(findChart().props('value')).toContain('NaN');
- });
-
- it('should not display `unit` when `unit` is undefined', () => {
- createComponent({
- graphData: singleStatGraphData({}, { unit: undefined }),
- });
-
- expect(findChart().props('value')).not.toContain('undefined');
- });
-
- it('should not display `unit` when `unit` is null', () => {
- createComponent({
- graphData: singleStatGraphData({}, { unit: null }),
- });
-
- expect(findChart().props('value')).not.toContain('null');
- });
-
- describe('when a field attribute is set', () => {
- it('displays a label value instead of metric value when field attribute is used', () => {
- createComponent({
- graphData: singleStatGraphData({ field: 'job' }, { isVector: true }),
- });
-
- expect(findChart().props('value')).toContain('prometheus');
- });
-
- it('displays No data to display if field attribute is not present', () => {
- createComponent({
- graphData: singleStatGraphData({ field: 'this-does-not-exist' }),
- });
-
- expect(findChart().props('value')).toContain('No data to display');
- });
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/stacked_column_spec.js b/spec/frontend/monitoring/components/charts/stacked_column_spec.js
deleted file mode 100644
index 779ded090c2..00000000000
--- a/spec/frontend/monitoring/components/charts/stacked_column_spec.js
+++ /dev/null
@@ -1,193 +0,0 @@
-import { GlStackedColumnChart, GlChartLegend } from '@gitlab/ui/dist/charts';
-import { shallowMount, mount } from '@vue/test-utils';
-import { cloneDeep } from 'lodash';
-import timezoneMock from 'timezone-mock';
-import { nextTick } from 'vue';
-import StackedColumnChart from '~/monitoring/components/charts/stacked_column.vue';
-import { stackedColumnGraphData } from '../../graph_data';
-
-jest.mock('~/lib/utils/icon_utils', () => ({
- getSvgIconPathContent: jest.fn().mockImplementation((icon) => Promise.resolve(`${icon}-content`)),
-}));
-
-describe('Stacked column chart component', () => {
- const stackedColumnMockedData = stackedColumnGraphData();
-
- let wrapper;
-
- const findChart = () => wrapper.findComponent(GlStackedColumnChart);
- const findLegend = () => wrapper.findComponent(GlChartLegend);
-
- const createWrapper = (props = {}, mountingMethod = shallowMount) =>
- mountingMethod(StackedColumnChart, {
- propsData: {
- graphData: stackedColumnMockedData,
- ...props,
- },
- stubs: {
- GlPopover: true,
- },
- attachTo: document.body,
- });
-
- beforeEach(() => {
- wrapper = createWrapper({}, mount);
- });
-
- describe('when graphData is present', () => {
- beforeEach(async () => {
- createWrapper();
- await nextTick();
- });
-
- it('chart is rendered', () => {
- expect(findChart().exists()).toBe(true);
- });
-
- it('data should match the graphData y value for each series', () => {
- const data = findChart().props('bars');
-
- data.forEach((series, index) => {
- const { values } = stackedColumnMockedData.metrics[index].result[0];
- expect(series.data).toEqual(values.map((value) => value[1]));
- });
- });
-
- it('data should be the same length as the graphData metrics labels', () => {
- const barDataProp = findChart().props('bars');
-
- expect(barDataProp).toHaveLength(stackedColumnMockedData.metrics.length);
- barDataProp.forEach(({ name }, index) => {
- expect(stackedColumnMockedData.metrics[index].label).toBe(name);
- });
- });
-
- it('group by should be the same as the graphData first metric results', () => {
- const groupBy = findChart().props('groupBy');
-
- expect(groupBy).toEqual([
- '2015-07-01T20:10:50.000Z',
- '2015-07-01T20:12:50.000Z',
- '2015-07-01T20:14:50.000Z',
- ]);
- });
-
- it('chart options should configure data zoom and axis label', () => {
- const chartOptions = findChart().props('option');
- const xAxisType = findChart().props('xAxisType');
-
- expect(chartOptions).toMatchObject({
- dataZoom: [{ handleIcon: 'path://scroll-handle-content' }],
- xAxis: {
- axisLabel: { formatter: expect.any(Function) },
- },
- });
-
- expect(xAxisType).toBe('category');
- });
-
- it('chart options should configure category as x axis type', () => {
- const chartOptions = findChart().props('option');
- const xAxisType = findChart().props('xAxisType');
-
- expect(chartOptions).toMatchObject({
- xAxis: {
- type: 'category',
- },
- });
- expect(xAxisType).toBe('category');
- });
-
- it('format date is correct', () => {
- const { xAxis } = findChart().props('option');
- expect(xAxis.axisLabel.formatter('2020-01-30T12:01:00.000Z')).toBe('12:01 PM');
- });
-
- describe('when in PT timezone', () => {
- beforeAll(() => {
- timezoneMock.register('US/Pacific');
- });
-
- afterAll(() => {
- timezoneMock.unregister();
- });
-
- it('date is shown in local time', () => {
- const { xAxis } = findChart().props('option');
- expect(xAxis.axisLabel.formatter('2020-01-30T12:01:00.000Z')).toBe('4:01 AM');
- });
-
- it('date is shown in UTC', async () => {
- wrapper.setProps({ timezone: 'UTC' });
-
- await nextTick();
- const { xAxis } = findChart().props('option');
- expect(xAxis.axisLabel.formatter('2020-01-30T12:01:00.000Z')).toBe('12:01 PM');
- });
- });
- });
-
- describe('when graphData has results missing', () => {
- beforeEach(async () => {
- const graphData = cloneDeep(stackedColumnMockedData);
-
- graphData.metrics[0].result = null;
-
- createWrapper({ graphData });
- await nextTick();
- });
-
- it('chart is rendered', () => {
- expect(findChart().exists()).toBe(true);
- });
- });
-
- describe('legend', () => {
- beforeEach(() => {
- wrapper = createWrapper({}, mount);
- });
-
- it('allows user to override legend label texts using props', async () => {
- const legendRelatedProps = {
- legendMinText: 'legendMinText',
- legendMaxText: 'legendMaxText',
- legendAverageText: 'legendAverageText',
- legendCurrentText: 'legendCurrentText',
- };
- wrapper.setProps({
- ...legendRelatedProps,
- });
-
- await nextTick();
- expect(findChart().props()).toMatchObject(legendRelatedProps);
- });
-
- it('should render a tabular legend layout by default', () => {
- expect(findLegend().props('layout')).toBe('table');
- });
-
- describe('when inline legend layout prop is set', () => {
- beforeEach(() => {
- wrapper.setProps({
- legendLayout: 'inline',
- });
- });
-
- it('should render an inline legend layout', () => {
- expect(findLegend().props('layout')).toBe('inline');
- });
- });
-
- describe('when table legend layout prop is set', () => {
- beforeEach(() => {
- wrapper.setProps({
- legendLayout: 'table',
- });
- });
-
- it('should render a tabular legend layout', () => {
- expect(findLegend().props('layout')).toBe('table');
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
deleted file mode 100644
index c1b51f71a7e..00000000000
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ /dev/null
@@ -1,748 +0,0 @@
-import { GlLink } from '@gitlab/ui';
-import {
- GlAreaChart,
- GlLineChart,
- GlChartSeriesLabel,
- GlChartLegend,
-} from '@gitlab/ui/dist/charts';
-import { mount, shallowMount } from '@vue/test-utils';
-import timezoneMock from 'timezone-mock';
-import { nextTick } from 'vue';
-import { TEST_HOST } from 'helpers/test_constants';
-import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
-import TimeSeries from '~/monitoring/components/charts/time_series.vue';
-import { panelTypes, chartHeight } from '~/monitoring/constants';
-import { timeSeriesGraphData } from '../../graph_data';
-import {
- deploymentData,
- mockProjectDir,
- annotationsData,
- mockFixedTimeRange,
-} from '../../mock_data';
-
-jest.mock('lodash/throttle', () =>
- // this throttle mock executes immediately
- jest.fn((func) => {
- // eslint-disable-next-line no-param-reassign
- func.cancel = jest.fn();
- return func;
- }),
-);
-jest.mock('~/lib/utils/icon_utils', () => ({
- getSvgIconPathContent: jest.fn().mockImplementation((icon) => Promise.resolve(`${icon}-content`)),
-}));
-
-describe('Time series component', () => {
- const defaultGraphData = timeSeriesGraphData();
- let wrapper;
-
- const createWrapper = (
- { graphData = defaultGraphData, ...props } = {},
- mountingMethod = shallowMount,
- ) => {
- wrapper = mountingMethod(TimeSeries, {
- propsData: {
- graphData,
- deploymentData,
- annotations: annotationsData,
- projectPath: `${TEST_HOST}${mockProjectDir}`,
- timeRange: mockFixedTimeRange,
- ...props,
- },
- stubs: {
- GlPopover: true,
- GlLineChart,
- GlAreaChart,
- },
- attachTo: document.body,
- });
- };
-
- describe('With a single time series', () => {
- describe('general functions', () => {
- const findChart = () => wrapper.findComponent({ ref: 'chart' });
-
- beforeEach(async () => {
- createWrapper({}, mount);
- await nextTick();
- });
-
- it('allows user to override legend label texts using props', async () => {
- const legendRelatedProps = {
- legendMinText: 'legendMinText',
- legendMaxText: 'legendMaxText',
- legendAverageText: 'legendAverageText',
- legendCurrentText: 'legendCurrentText',
- };
- wrapper.setProps({
- ...legendRelatedProps,
- });
-
- await nextTick();
- expect(findChart().props()).toMatchObject(legendRelatedProps);
- });
-
- it('chart sets a default height', () => {
- createWrapper();
- expect(wrapper.props('height')).toBe(chartHeight);
- });
-
- it('chart has a configurable height', async () => {
- const mockHeight = 599;
- createWrapper();
-
- wrapper.setProps({ height: mockHeight });
- await nextTick();
- expect(wrapper.props('height')).toBe(mockHeight);
- });
-
- describe('events', () => {
- describe('datazoom', () => {
- let eChartMock;
- let startValue;
- let endValue;
-
- beforeEach(async () => {
- eChartMock = {
- handlers: {},
- getOption: () => ({
- dataZoom: [
- {
- startValue,
- endValue,
- },
- ],
- }),
- off: jest.fn((eChartEvent) => {
- delete eChartMock.handlers[eChartEvent];
- }),
- on: jest.fn((eChartEvent, fn) => {
- eChartMock.handlers[eChartEvent] = fn;
- }),
- };
-
- createWrapper({}, mount);
- await nextTick();
- findChart().vm.$emit('created', eChartMock);
- });
-
- it('handles datazoom event from chart', () => {
- startValue = 1577836800000; // 2020-01-01T00:00:00.000Z
- endValue = 1577840400000; // 2020-01-01T01:00:00.000Z
- eChartMock.handlers.datazoom();
-
- expect(wrapper.emitted('datazoom')).toHaveLength(1);
- expect(wrapper.emitted('datazoom')[0]).toEqual([
- {
- start: new Date(startValue).toISOString(),
- end: new Date(endValue).toISOString(),
- },
- ]);
- });
- });
- });
-
- describe('methods', () => {
- describe('formatTooltipText', () => {
- const mockCommitUrl = deploymentData[0].commitUrl;
- const mockDate = deploymentData[0].created_at;
- const mockSha = 'f5bcd1d9';
- const mockLineSeriesData = () => ({
- seriesData: [
- {
- seriesName: wrapper.vm.chartData[0].name,
- componentSubType: 'line',
- value: [mockDate, 5.55555],
- dataIndex: 0,
- },
- ],
- value: mockDate,
- });
-
- const annotationsMetadata = {
- tooltipData: {
- sha: mockSha,
- commitUrl: mockCommitUrl,
- },
- };
-
- const mockAnnotationsSeriesData = {
- seriesData: [
- {
- componentSubType: 'scatter',
- seriesName: 'series01',
- dataIndex: 0,
- value: [mockDate, 5.55555],
- type: 'scatter',
- name: 'deployments',
- },
- ],
- value: mockDate,
- };
-
- it('does not throw error if data point is outside the zoom range', () => {
- const seriesDataWithoutValue = {
- ...mockLineSeriesData(),
- seriesData: mockLineSeriesData().seriesData.map((data) => ({
- ...data,
- value: undefined,
- })),
- };
- expect(wrapper.vm.formatTooltipText(seriesDataWithoutValue)).toBeUndefined();
- });
-
- describe('when series is of line type', () => {
- beforeEach(async () => {
- createWrapper({}, mount);
- wrapper.vm.formatTooltipText(mockLineSeriesData());
- await nextTick();
- });
-
- it('formats tooltip title', () => {
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (UTC)');
- });
-
- it('formats tooltip content', () => {
- const name = 'Metric 1';
- const value = '5.556';
- const dataIndex = 0;
- const seriesLabel = wrapper.findComponent(GlChartSeriesLabel);
-
- expect(seriesLabel.vm.color).toBe('');
-
- expect(shallowWrapperContainsSlotText(seriesLabel, 'default', name)).toBe(true);
- expect(wrapper.vm.tooltip.content).toEqual([
- { name, value, dataIndex, color: undefined },
- ]);
-
- expect(
- shallowWrapperContainsSlotText(
- wrapper.findComponent(GlLineChart),
- 'tooltip-content',
- value,
- ),
- ).toBe(true);
- });
-
- describe('when in PT timezone', () => {
- beforeAll(() => {
- // Note: node.js env renders (GMT-0700), in the browser we see (PDT)
- timezoneMock.register('US/Pacific');
- });
-
- afterAll(() => {
- timezoneMock.unregister();
- });
-
- it('formats tooltip title in local timezone by default', async () => {
- createWrapper();
- wrapper.vm.formatTooltipText(mockLineSeriesData());
- await nextTick();
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 3:14AM (GMT-0700)');
- });
-
- it('formats tooltip title in local timezone', async () => {
- createWrapper({ timezone: 'LOCAL' });
- wrapper.vm.formatTooltipText(mockLineSeriesData());
- await nextTick();
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 3:14AM (GMT-0700)');
- });
-
- it('formats tooltip title in UTC format', async () => {
- createWrapper({ timezone: 'UTC' });
- wrapper.vm.formatTooltipText(mockLineSeriesData());
- await nextTick();
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (UTC)');
- });
- });
- });
-
- describe('when series is of scatter type, for deployments', () => {
- beforeEach(async () => {
- wrapper.vm.formatTooltipText({
- ...mockAnnotationsSeriesData,
- seriesData: mockAnnotationsSeriesData.seriesData.map((data) => ({
- ...data,
- data: annotationsMetadata,
- })),
- });
- await nextTick();
- });
-
- it('set tooltip type to deployments', () => {
- expect(wrapper.vm.tooltip.type).toBe('deployments');
- });
-
- it('formats tooltip title', () => {
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (UTC)');
- });
-
- it('formats tooltip sha', () => {
- expect(wrapper.vm.tooltip.sha).toBe('f5bcd1d9');
- });
-
- it('formats tooltip commit url', () => {
- expect(wrapper.vm.tooltip.commitUrl).toBe(mockCommitUrl);
- });
- });
-
- describe('when series is of scatter type and deployments data is missing', () => {
- beforeEach(async () => {
- wrapper.vm.formatTooltipText(mockAnnotationsSeriesData);
- await nextTick();
- });
-
- it('formats tooltip title', () => {
- expect(wrapper.vm.tooltip.title).toBe('16 Jul 2019, 10:14AM (UTC)');
- });
-
- it('formats tooltip sha', () => {
- expect(wrapper.vm.tooltip.sha).toBeUndefined();
- });
-
- it('formats tooltip commit url', () => {
- expect(wrapper.vm.tooltip.commitUrl).toBeUndefined();
- });
- });
- });
-
- describe('formatAnnotationsTooltipText', () => {
- const annotationsMetadata = {
- name: 'annotations',
- xAxis: annotationsData[0].from,
- yAxis: 0,
- tooltipData: {
- title: '2020/02/19 10:01:41',
- content: annotationsData[0].description,
- },
- };
-
- const mockMarkPoint = {
- componentType: 'markPoint',
- name: 'annotations',
- value: undefined,
- data: annotationsMetadata,
- };
-
- it('formats tooltip title and sets tooltip content', () => {
- const formattedTooltipData = wrapper.vm.formatAnnotationsTooltipText(mockMarkPoint);
- expect(formattedTooltipData.title).toBe('19 Feb 2020, 10:01AM (UTC)');
- expect(formattedTooltipData.content).toBe(annotationsMetadata.tooltipData.content);
- });
- });
- });
-
- describe('computed', () => {
- const getChartOptions = () => findChart().props('option');
-
- describe('chartData', () => {
- let chartData;
- const seriesData = () => chartData[0];
-
- beforeEach(() => {
- ({ chartData } = wrapper.vm);
- });
-
- it('utilizes all data points', () => {
- expect(chartData.length).toBe(1);
- expect(seriesData().data.length).toBe(3);
- });
-
- it('creates valid data', () => {
- const { data } = seriesData();
-
- expect(
- data.filter(
- ([time, value]) => new Date(time).getTime() > 0 && typeof value === 'number',
- ).length,
- ).toBe(data.length);
- });
-
- it('formats line width correctly', () => {
- expect(chartData[0].lineStyle.width).toBe(2);
- });
- });
-
- describe('chartOptions', () => {
- describe('x-Axis bounds', () => {
- it('is set to the time range bounds', () => {
- expect(getChartOptions().xAxis).toMatchObject({
- min: mockFixedTimeRange.start,
- max: mockFixedTimeRange.end,
- });
- });
-
- it('is not set if time range is not set or incorrectly set', async () => {
- wrapper.setProps({
- timeRange: {},
- });
- await nextTick();
- expect(getChartOptions().xAxis).not.toHaveProperty('min');
- expect(getChartOptions().xAxis).not.toHaveProperty('max');
- });
- });
-
- describe('dataZoom', () => {
- it('renders with scroll handle icons', () => {
- expect(getChartOptions().dataZoom).toHaveLength(1);
- expect(getChartOptions().dataZoom[0]).toMatchObject({
- handleIcon: 'path://scroll-handle-content',
- });
- });
- });
-
- describe('xAxis pointer', () => {
- it('snap is set to false by default', () => {
- expect(getChartOptions().xAxis.axisPointer.snap).toBe(false);
- });
- });
-
- describe('are extended by `option`', () => {
- const mockSeriesName = 'Extra series 1';
- const mockOption = {
- option1: 'option1',
- option2: 'option2',
- };
-
- it('arbitrary options', async () => {
- wrapper.setProps({
- option: mockOption,
- });
-
- await nextTick();
- expect(getChartOptions()).toEqual(expect.objectContaining(mockOption));
- });
-
- it('additional series', async () => {
- wrapper.setProps({
- option: {
- series: [
- {
- name: mockSeriesName,
- type: 'line',
- data: [],
- },
- ],
- },
- });
-
- await nextTick();
- const optionSeries = getChartOptions().series;
-
- expect(optionSeries.length).toEqual(2);
- expect(optionSeries[0].name).toEqual(mockSeriesName);
- });
-
- it('additional y-axis data', async () => {
- const mockCustomYAxisOption = {
- name: 'Custom y-axis label',
- axisLabel: {
- formatter: jest.fn(),
- },
- };
-
- wrapper.setProps({
- option: {
- yAxis: mockCustomYAxisOption,
- },
- });
-
- await nextTick();
- const { yAxis } = getChartOptions();
-
- expect(yAxis[0]).toMatchObject(mockCustomYAxisOption);
- });
-
- it('additional x axis data', async () => {
- const mockCustomXAxisOption = {
- name: 'Custom x axis label',
- };
-
- wrapper.setProps({
- option: {
- xAxis: mockCustomXAxisOption,
- },
- });
-
- await nextTick();
- const { xAxis } = getChartOptions();
-
- expect(xAxis).toMatchObject(mockCustomXAxisOption);
- });
- });
-
- describe('yAxis formatter', () => {
- let dataFormatter;
- let deploymentFormatter;
-
- beforeEach(() => {
- dataFormatter = getChartOptions().yAxis[0].axisLabel.formatter;
- deploymentFormatter = getChartOptions().yAxis[1].axisLabel.formatter;
- });
-
- it('formats by default to precision notation', () => {
- expect(dataFormatter(0.88888)).toBe('889m');
- });
-
- it('deployment formatter is set as is required to display a tooltip', () => {
- expect(deploymentFormatter).toEqual(expect.any(Function));
- });
- });
- });
-
- describe('annotationSeries', () => {
- it('utilizes deployment data', () => {
- const annotationSeries = wrapper.vm.chartOptionSeries[0];
- expect(annotationSeries.yAxisIndex).toBe(1); // same as annotations y axis
- expect(annotationSeries.data).toEqual([
- expect.objectContaining({
- symbolSize: 14,
- symbol: 'path://rocket-content',
- value: ['2019-07-16T10:14:25.589Z', expect.any(Number)],
- }),
- expect.objectContaining({
- symbolSize: 14,
- symbol: 'path://rocket-content',
- value: ['2019-07-16T11:14:25.589Z', expect.any(Number)],
- }),
- expect.objectContaining({
- symbolSize: 14,
- symbol: 'path://rocket-content',
- value: ['2019-07-16T12:14:25.589Z', expect.any(Number)],
- }),
- ]);
- });
- });
-
- describe('xAxisLabel', () => {
- const mockDate = Date.UTC(2020, 4, 26, 20); // 8:00 PM in GMT
-
- const useXAxisFormatter = (date) => {
- const { xAxis } = getChartOptions();
- const { formatter } = xAxis.axisLabel;
- return formatter(date);
- };
-
- it('x-axis is formatted correctly in m/d h:MM TT format', () => {
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 8:00 PM');
- });
-
- describe('when in PT timezone', () => {
- beforeAll(() => {
- timezoneMock.register('US/Pacific');
- });
-
- afterAll(() => {
- timezoneMock.unregister();
- });
-
- it('by default, values are formatted in PT', () => {
- createWrapper();
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 1:00 PM');
- });
-
- it('when the chart uses local timezone, y-axis is formatted in PT', () => {
- createWrapper({ timezone: 'LOCAL' });
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 1:00 PM');
- });
-
- it('when the chart uses UTC, y-axis is formatted in UTC', () => {
- createWrapper({ timezone: 'UTC' });
- expect(useXAxisFormatter(mockDate)).toEqual('5/26 8:00 PM');
- });
- });
- });
-
- describe('yAxisLabel', () => {
- it('y-axis is configured correctly', () => {
- const { yAxis } = getChartOptions();
-
- expect(yAxis).toHaveLength(2);
-
- const [dataAxis, deploymentAxis] = yAxis;
-
- expect(dataAxis.boundaryGap).toHaveLength(2);
- expect(dataAxis.scale).toBe(true);
-
- expect(deploymentAxis.show).toBe(false);
- expect(deploymentAxis.min).toEqual(expect.any(Number));
- expect(deploymentAxis.max).toEqual(expect.any(Number));
- expect(deploymentAxis.min).toBeLessThan(deploymentAxis.max);
- });
-
- it('constructs a label for the chart y-axis', () => {
- const { yAxis } = getChartOptions();
-
- expect(yAxis[0].name).toBe('Y Axis');
- });
- });
- });
- });
-
- describe('wrapped components', () => {
- const glChartComponents = [
- {
- chartType: panelTypes.AREA_CHART,
- component: GlAreaChart,
- },
- {
- chartType: panelTypes.LINE_CHART,
- component: GlLineChart,
- },
- ];
-
- glChartComponents.forEach((dynamicComponent) => {
- describe(`GitLab UI: ${dynamicComponent.chartType}`, () => {
- const findChartComponent = () => wrapper.findComponent(dynamicComponent.component);
-
- beforeEach(async () => {
- createWrapper(
- { graphData: timeSeriesGraphData({ type: dynamicComponent.chartType }) },
- mount,
- );
- await nextTick();
- });
-
- it('exists', () => {
- expect(findChartComponent().exists()).toBe(true);
- });
-
- it('receives data properties needed for proper chart render', () => {
- const props = findChartComponent().props();
-
- expect(props.data).toBe(wrapper.vm.chartData);
- expect(props.option).toBe(wrapper.vm.chartOptions);
- expect(props.formatTooltipText).toBe(wrapper.vm.formatTooltipText);
- });
-
- it('receives a tooltip title', async () => {
- const mockTitle = 'mockTitle';
- wrapper.vm.tooltip.title = mockTitle;
-
- await nextTick();
- expect(
- shallowWrapperContainsSlotText(findChartComponent(), 'tooltip-title', mockTitle),
- ).toBe(true);
- });
-
- describe('when tooltip is showing deployment data', () => {
- const mockSha = 'mockSha';
- const commitUrl = `${mockProjectDir}/-/commit/${mockSha}`;
-
- beforeEach(async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- tooltip: {
- type: 'deployments',
- },
- });
- await nextTick();
- });
-
- it('uses deployment title', () => {
- expect(
- shallowWrapperContainsSlotText(findChartComponent(), 'tooltip-title', 'Deployed'),
- ).toBe(true);
- });
-
- it('renders clickable commit sha in tooltip content', async () => {
- wrapper.vm.tooltip.sha = mockSha;
- wrapper.vm.tooltip.commitUrl = commitUrl;
-
- await nextTick();
- const commitLink = wrapper.findComponent(GlLink);
-
- expect(shallowWrapperContainsSlotText(commitLink, 'default', mockSha)).toBe(true);
- expect(commitLink.attributes('href')).toEqual(commitUrl);
- });
- });
- });
- });
- });
- });
-
- describe('with multiple time series', () => {
- describe('General functions', () => {
- beforeEach(async () => {
- const graphData = timeSeriesGraphData({ type: panelTypes.AREA_CHART, multiMetric: true });
-
- createWrapper({ graphData }, mount);
- await nextTick();
- });
-
- describe('Color match', () => {
- let lineColors;
-
- beforeEach(() => {
- lineColors = wrapper
- .findComponent(GlAreaChart)
- .vm.series.map((item) => item.lineStyle.color);
- });
-
- it('should contain different colors for contiguous time series', () => {
- lineColors.forEach((color, index) => {
- expect(color).not.toBe(lineColors[index + 1]);
- });
- });
-
- it('should match series color with tooltip label color', () => {
- const labels = wrapper.findAllComponents(GlChartSeriesLabel);
-
- lineColors.forEach((color, index) => {
- const labelColor = labels.at(index).props('color');
- expect(color).toBe(labelColor);
- });
- });
-
- it('should match series color with legend color', () => {
- const legendColors = wrapper
- .findComponent(GlChartLegend)
- .props('seriesInfo')
- .map((item) => item.color);
-
- lineColors.forEach((color, index) => {
- expect(color).toBe(legendColors[index]);
- });
- });
- });
- });
- });
-
- describe('legend layout', () => {
- const findLegend = () => wrapper.findComponent(GlChartLegend);
-
- beforeEach(async () => {
- createWrapper({}, mount);
- await nextTick();
- });
-
- it('should render a tabular legend layout by default', () => {
- expect(findLegend().props('layout')).toBe('table');
- });
-
- describe('when inline legend layout prop is set', () => {
- beforeEach(() => {
- wrapper.setProps({
- legendLayout: 'inline',
- });
- });
-
- it('should render an inline legend layout', () => {
- expect(findLegend().props('layout')).toBe('inline');
- });
- });
-
- describe('when table legend layout prop is set', () => {
- beforeEach(() => {
- wrapper.setProps({
- legendLayout: 'table',
- });
- });
-
- it('should render a tabular legend layout', () => {
- expect(findLegend().props('layout')).toBe('table');
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js b/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
deleted file mode 100644
index eb05b1f184a..00000000000
--- a/spec/frontend/monitoring/components/create_dashboard_modal_spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue';
-
-describe('Create dashboard modal', () => {
- let wrapper;
-
- const defaultProps = {
- modalId: 'id',
- projectPath: 'https://localhost/',
- addDashboardDocumentationPath: 'https://link/to/docs',
- };
-
- const findDocsButton = () => wrapper.find('[data-testid="create-dashboard-modal-docs-button"]');
- const findRepoButton = () => wrapper.find('[data-testid="create-dashboard-modal-repo-button"]');
-
- const createWrapper = (props = {}, options = {}) => {
- wrapper = shallowMount(CreateDashboardModal, {
- propsData: { ...defaultProps, ...props },
- stubs: {
- GlModal,
- },
- ...options,
- });
- };
-
- beforeEach(() => {
- createWrapper();
- });
-
- it('has button that links to the project url', async () => {
- findRepoButton().trigger('click');
-
- await nextTick();
- expect(findRepoButton().exists()).toBe(true);
- expect(findRepoButton().attributes('href')).toBe(defaultProps.projectPath);
- });
-
- it('has button that links to the docs', () => {
- expect(findDocsButton().exists()).toBe(true);
- expect(findDocsButton().attributes('href')).toBe(defaultProps.addDashboardDocumentationPath);
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
deleted file mode 100644
index 4d290922707..00000000000
--- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
+++ /dev/null
@@ -1,421 +0,0 @@
-import { GlDropdownItem, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
-import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
-import { createStore } from '~/monitoring/stores';
-import * as types from '~/monitoring/stores/mutation_types';
-import Tracking from '~/tracking';
-import { dashboardActionsMenuProps, dashboardGitResponse } from '../mock_data';
-import { setupAllDashboards, setupStoreWithData } from '../store_utils';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- redirectTo: jest.fn(),
- queryToObject: jest.fn(),
-}));
-
-describe('Actions menu', () => {
- const ootbDashboards = [dashboardGitResponse[0], dashboardGitResponse[2]];
- const customDashboard = dashboardGitResponse[1];
-
- let store;
- let wrapper;
-
- const findAddMetricItem = () => wrapper.find('[data-testid="add-metric-item"]');
- const findAddPanelItemEnabled = () => wrapper.find('[data-testid="add-panel-item-enabled"]');
- const findAddPanelItemDisabled = () => wrapper.find('[data-testid="add-panel-item-disabled"]');
- const findAddMetricModal = () => wrapper.find('[data-testid="add-metric-modal"]');
- const findAddMetricModalSubmitButton = () =>
- wrapper.find('[data-testid="add-metric-modal-submit-button"]');
- const findStarDashboardItem = () => wrapper.find('[data-testid="star-dashboard-item"]');
- const findEditDashboardItemEnabled = () =>
- wrapper.find('[data-testid="edit-dashboard-item-enabled"]');
- const findEditDashboardItemDisabled = () =>
- wrapper.find('[data-testid="edit-dashboard-item-disabled"]');
- const findDuplicateDashboardItem = () => wrapper.find('[data-testid="duplicate-dashboard-item"]');
- const findDuplicateDashboardModal = () =>
- wrapper.find('[data-testid="duplicate-dashboard-modal"]');
- const findCreateDashboardItem = () => wrapper.find('[data-testid="create-dashboard-item"]');
- const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]');
-
- const createShallowWrapper = (props = {}, options = {}) => {
- wrapper = shallowMount(ActionsMenu, {
- propsData: { ...dashboardActionsMenuProps, ...props },
- store,
- stubs: {
- GlModal,
- },
- ...options,
- });
- };
-
- beforeEach(() => {
- store = createStore();
- });
-
- describe('add metric item', () => {
- it('is rendered when custom metrics are available', async () => {
- createShallowWrapper();
-
- await nextTick();
- expect(findAddMetricItem().exists()).toBe(true);
- });
-
- it('is not rendered when custom metrics are not available', async () => {
- createShallowWrapper({
- addingMetricsAvailable: false,
- });
-
- await nextTick();
- expect(findAddMetricItem().exists()).toBe(false);
- });
-
- describe('when available', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- it('modal for custom metrics form is rendered', () => {
- expect(findAddMetricModal().exists()).toBe(true);
- expect(findAddMetricModal().props('modalId')).toBe('addMetric');
- });
-
- it('add metric modal submit button exists', () => {
- expect(findAddMetricModalSubmitButton().exists()).toBe(true);
- });
-
- it('renders custom metrics form fields', () => {
- expect(wrapper.findComponent(CustomMetricsFormFields).exists()).toBe(true);
- });
- });
-
- describe('when not available', () => {
- beforeEach(() => {
- createShallowWrapper({ addingMetricsAvailable: false });
- });
-
- it('modal for custom metrics form is not rendered', () => {
- expect(findAddMetricModal().exists()).toBe(false);
- });
- });
-
- describe('adding new metric from modal', () => {
- let origPage;
-
- beforeEach(() => {
- jest.spyOn(Tracking, 'event').mockReturnValue();
- createShallowWrapper();
-
- setupStoreWithData(store);
-
- origPage = document.body.dataset.page;
- document.body.dataset.page = 'projects:environments:metrics';
-
- return nextTick();
- });
-
- afterEach(() => {
- document.body.dataset.page = origPage;
- });
-
- it('is tracked', async () => {
- const submitButton = findAddMetricModalSubmitButton().vm;
-
- await nextTick();
- submitButton.$el.click();
- await nextTick();
- expect(Tracking.event).toHaveBeenCalledWith(document.body.dataset.page, 'click_button', {
- label: 'add_new_metric',
- property: 'modal',
- value: undefined,
- });
- });
- });
- });
-
- describe('add panel item', () => {
- const GlDropdownItemStub = {
- extends: GlDropdownItem,
- props: {
- to: [String, Object],
- },
- };
-
- let $route;
-
- beforeEach(() => {
- $route = { name: DASHBOARD_PAGE, params: { dashboard: 'my_dashboard.yml' } };
-
- createShallowWrapper(
- {
- isOotbDashboard: false,
- },
- {
- mocks: { $route },
- stubs: { GlDropdownItem: GlDropdownItemStub },
- },
- );
- });
-
- it('is disabled for ootb dashboards', async () => {
- createShallowWrapper({
- isOotbDashboard: true,
- });
-
- await nextTick();
- expect(findAddPanelItemDisabled().exists()).toBe(true);
- });
-
- it('is visible for custom dashboards', () => {
- expect(findAddPanelItemEnabled().exists()).toBe(true);
- });
-
- it('renders a link to the new panel page for custom dashboards', () => {
- expect(findAddPanelItemEnabled().props('to')).toEqual({
- name: PANEL_NEW_PAGE,
- params: {
- dashboard: 'my_dashboard.yml',
- },
- });
- });
- });
-
- describe('edit dashboard yml item', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- describe('when current dashboard is custom', () => {
- beforeEach(() => {
- setupAllDashboards(store, customDashboard.path);
- });
-
- it('enabled item is rendered and has falsy disabled attribute', () => {
- expect(findEditDashboardItemEnabled().exists()).toBe(true);
- expect(findEditDashboardItemEnabled().attributes('disabled')).toBe(undefined);
- });
-
- it('enabled item links to their edit path', () => {
- expect(findEditDashboardItemEnabled().attributes('href')).toBe(
- customDashboard.project_blob_path,
- );
- });
-
- it('disabled item is not rendered', () => {
- expect(findEditDashboardItemDisabled().exists()).toBe(false);
- });
- });
-
- describe.each(ootbDashboards)('when current dashboard is OOTB', (dashboard) => {
- beforeEach(() => {
- setupAllDashboards(store, dashboard.path);
- });
-
- it('disabled item is rendered and has disabled attribute set on it', () => {
- expect(findEditDashboardItemDisabled().exists()).toBe(true);
- expect(findEditDashboardItemDisabled().attributes('disabled')).toBe('');
- });
-
- it('enabled item is not rendered', () => {
- expect(findEditDashboardItemEnabled().exists()).toBe(false);
- });
- });
- });
-
- describe('duplicate dashboard item', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- describe.each(ootbDashboards)('when current dashboard is OOTB', (dashboard) => {
- beforeEach(() => {
- setupAllDashboards(store, dashboard.path);
- });
-
- it('is rendered', () => {
- expect(findDuplicateDashboardItem().exists()).toBe(true);
- });
-
- it('duplicate dashboard modal is rendered', () => {
- expect(findDuplicateDashboardModal().exists()).toBe(true);
- });
-
- it('clicking on item opens up the duplicate dashboard modal', async () => {
- const modalId = 'duplicateDashboard';
- const modalTrigger = findDuplicateDashboardItem();
- const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
-
- modalTrigger.trigger('click');
-
- await nextTick();
- expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
- });
- });
-
- describe('when current dashboard is custom', () => {
- beforeEach(() => {
- setupAllDashboards(store, customDashboard.path);
- });
-
- it('is not rendered', () => {
- expect(findDuplicateDashboardItem().exists()).toBe(false);
- });
-
- it('duplicate dashboard modal is not rendered', () => {
- expect(findDuplicateDashboardModal().exists()).toBe(false);
- });
- });
-
- describe('when no dashboard is set', () => {
- it('is not rendered', () => {
- expect(findDuplicateDashboardItem().exists()).toBe(false);
- });
-
- it('duplicate dashboard modal is not rendered', () => {
- expect(findDuplicateDashboardModal().exists()).toBe(false);
- });
- });
-
- describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => {
- beforeEach(() => {
- store.state.monitoringDashboard.projectPath = 'root/sandbox';
-
- setupAllDashboards(store, dashboardGitResponse[0].path);
- });
-
- it('redirects to the newly created dashboard', async () => {
- const newDashboard = dashboardGitResponse[1];
-
- const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml';
- findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard);
-
- await nextTick();
- expect(redirectTo).toHaveBeenCalled(); // eslint-disable-line import/no-deprecated
- expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl); // eslint-disable-line import/no-deprecated
- });
- });
- });
-
- describe('star dashboard item', () => {
- beforeEach(() => {
- createShallowWrapper();
- setupAllDashboards(store);
-
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- });
-
- it('is shown', () => {
- expect(findStarDashboardItem().exists()).toBe(true);
- });
-
- it('is not disabled', () => {
- expect(findStarDashboardItem().attributes('disabled')).toBeUndefined();
- });
-
- it('is disabled when starring is taking place', async () => {
- store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`);
-
- await nextTick();
- expect(findStarDashboardItem().exists()).toBe(true);
- expect(findStarDashboardItem().attributes('disabled')).toBeDefined();
- });
-
- it('on click it dispatches a toggle star action', async () => {
- findStarDashboardItem().vm.$emit('click');
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/toggleStarredValue',
- undefined,
- );
- });
-
- describe('when dashboard is not starred', () => {
- beforeEach(async () => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: dashboardGitResponse[0].path,
- });
- await nextTick();
- });
-
- it('item text shows "Star dashboard"', () => {
- expect(findStarDashboardItem().html()).toMatch(/Star dashboard/);
- });
- });
-
- describe('when dashboard is starred', () => {
- beforeEach(async () => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: dashboardGitResponse[1].path,
- });
- await nextTick();
- });
-
- it('item text shows "Unstar dashboard"', () => {
- expect(findStarDashboardItem().html()).toMatch(/Unstar dashboard/);
- });
- });
- });
-
- describe('create dashboard item', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- it('is rendered by default but it is disabled', () => {
- expect(findCreateDashboardItem().attributes('disabled')).toBeDefined();
- });
-
- describe('when project path is set', () => {
- const mockProjectPath = 'root/sandbox';
- const mockAddDashboardDocPath = '/doc/add-dashboard';
-
- beforeEach(() => {
- store.state.monitoringDashboard.projectPath = mockProjectPath;
- store.state.monitoringDashboard.addDashboardDocumentationPath = mockAddDashboardDocPath;
- });
-
- it('is not disabled', () => {
- expect(findCreateDashboardItem().attributes('disabled')).toBe(undefined);
- });
-
- it('renders a modal for creating a dashboard', () => {
- expect(findCreateDashboardModal().exists()).toBe(true);
- });
-
- it('clicking opens up the modal', async () => {
- const modalId = 'createDashboard';
- const modalTrigger = findCreateDashboardItem();
- const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
-
- modalTrigger.trigger('click');
-
- await nextTick();
- expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
- });
-
- it('modal gets passed correct props', () => {
- expect(findCreateDashboardModal().props('projectPath')).toBe(mockProjectPath);
- expect(findCreateDashboardModal().props('addDashboardDocumentationPath')).toBe(
- mockAddDashboardDocPath,
- );
- });
- });
-
- describe('when project path is not set', () => {
- beforeEach(() => {
- store.state.monitoringDashboard.projectPath = null;
- });
-
- it('is disabled', () => {
- expect(findCreateDashboardItem().attributes('disabled')).toBeDefined();
- });
-
- it('does not render a modal for creating a dashboard', () => {
- expect(findCreateDashboardModal().exists()).toBe(false);
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
deleted file mode 100644
index 091e05ab271..00000000000
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ /dev/null
@@ -1,395 +0,0 @@
-import { GlDropdownItem, GlSearchBoxByType, GlLoadingIcon, GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
-import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
-import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
-import RefreshButton from '~/monitoring/components/refresh_button.vue';
-import { createStore } from '~/monitoring/stores';
-import * as types from '~/monitoring/stores/mutation_types';
-import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
-import { environmentData, dashboardGitResponse, dashboardHeaderProps } from '../mock_data';
-import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils';
-
-const mockProjectPath = 'https://path/to/project';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- redirectTo: jest.fn(),
- queryToObject: jest.fn(),
- mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams,
-}));
-
-describe('Dashboard header', () => {
- let store;
- let wrapper;
-
- const findDashboardDropdown = () => wrapper.findComponent(DashboardsDropdown);
-
- const findEnvsDropdown = () => wrapper.findComponent({ ref: 'monitorEnvironmentsDropdown' });
- const findEnvsDropdownItems = () => findEnvsDropdown().findAllComponents(GlDropdownItem);
- const findEnvsDropdownSearch = () => findEnvsDropdown().findComponent(GlSearchBoxByType);
- const findEnvsDropdownSearchMsg = () =>
- wrapper.findComponent({ ref: 'monitorEnvironmentsDropdownMsg' });
- const findEnvsDropdownLoadingIcon = () => findEnvsDropdown().findComponent(GlLoadingIcon);
-
- const findDateTimePicker = () => wrapper.findComponent(DateTimePicker);
- const findRefreshButton = () => wrapper.findComponent(RefreshButton);
-
- const findActionsMenu = () => wrapper.findComponent(ActionsMenu);
-
- const setSearchTerm = (searchTerm) => {
- store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
- };
-
- const createShallowWrapper = (props = {}, options = {}) => {
- wrapper = shallowMount(DashboardHeader, {
- propsData: { ...dashboardHeaderProps, ...props },
- store,
- ...options,
- });
- };
-
- beforeEach(() => {
- store = createStore();
- });
-
- describe('dashboards dropdown', () => {
- beforeEach(() => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- projectPath: mockProjectPath,
- });
-
- createShallowWrapper();
- });
-
- it('shows the dashboard dropdown', () => {
- expect(findDashboardDropdown().exists()).toBe(true);
- });
-
- it('when an out of the box dashboard is selected, encodes dashboard path', () => {
- findDashboardDropdown().vm.$emit('selectDashboard', {
- path: '.gitlab/dashboards/dashboard&copy.yml',
- out_of_the_box_dashboard: true,
- display_name: 'A display name',
- });
-
- // eslint-disable-next-line import/no-deprecated
- expect(redirectTo).toHaveBeenCalledWith(
- `${mockProjectPath}/-/metrics/.gitlab%2Fdashboards%2Fdashboard%26copy.yml`,
- );
- });
-
- it('when a custom dashboard is selected, encodes dashboard display name', () => {
- findDashboardDropdown().vm.$emit('selectDashboard', {
- path: '.gitlab/dashboards/file&path.yml',
- display_name: 'dashboard&copy.yml',
- });
-
- // eslint-disable-next-line import/no-deprecated
- expect(redirectTo).toHaveBeenCalledWith(`${mockProjectPath}/-/metrics/dashboard%26copy.yml`);
- });
- });
-
- describe('environments dropdown', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- it('shows the environments dropdown', () => {
- expect(findEnvsDropdown().exists()).toBe(true);
- });
-
- it('renders a search input', () => {
- expect(findEnvsDropdownSearch().exists()).toBe(true);
- });
-
- describe('when environments data is not loaded', () => {
- beforeEach(async () => {
- setupStoreWithDashboard(store);
- await nextTick();
- });
-
- it('there are no environments listed', () => {
- expect(findEnvsDropdownItems()).toHaveLength(0);
- });
- });
-
- describe('when environments data is loaded', () => {
- const currentDashboard = dashboardGitResponse[0].path;
- const currentEnvironmentName = environmentData[0].name;
-
- beforeEach(async () => {
- setupStoreWithData(store);
- store.state.monitoringDashboard.projectPath = mockProjectPath;
- store.state.monitoringDashboard.currentDashboard = currentDashboard;
- store.state.monitoringDashboard.currentEnvironmentName = currentEnvironmentName;
-
- await nextTick();
- });
-
- it('renders dropdown items with the environment name', () => {
- const path = `${mockProjectPath}/-/metrics/${encodeURIComponent(currentDashboard)}`;
-
- findEnvsDropdownItems().wrappers.forEach((itemWrapper, index) => {
- const { name, id } = environmentData[index];
- const idParam = encodeURIComponent(id);
-
- expect(itemWrapper.text()).toBe(name);
- expect(itemWrapper.attributes('href')).toBe(`${path}?environment=${idParam}`);
- });
- });
-
- it('environments dropdown items can be checked', () => {
- const items = findEnvsDropdownItems();
- const checkItems = findEnvsDropdownItems().filter((item) => item.props('isCheckItem'));
-
- expect(items).toHaveLength(checkItems.length);
- });
-
- it('checks the currently selected environment', () => {
- const selectedItems = findEnvsDropdownItems().filter((item) => item.props('isChecked'));
-
- expect(selectedItems).toHaveLength(1);
- expect(selectedItems.at(0).text()).toBe(currentEnvironmentName);
- });
-
- it('filters rendered dropdown items', async () => {
- const searchTerm = 'production';
- const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1);
- setSearchTerm(searchTerm);
-
- await nextTick();
- expect(findEnvsDropdownItems()).toHaveLength(resultEnvs.length);
- });
-
- it('does not filter dropdown items if search term is empty string', async () => {
- const searchTerm = '';
- setSearchTerm(searchTerm);
-
- await nextTick();
- expect(findEnvsDropdownItems()).toHaveLength(environmentData.length);
- });
-
- it("shows error message if search term doesn't match", async () => {
- const searchTerm = 'does-not-exist';
- setSearchTerm(searchTerm);
-
- await nextTick();
- expect(findEnvsDropdownSearchMsg().isVisible()).toBe(true);
- });
-
- it('shows loading element when environments fetch is still loading', async () => {
- store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
-
- await nextTick();
- expect(findEnvsDropdownLoadingIcon().exists()).toBe(true);
- await store.commit(
- `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
- environmentData,
- );
- expect(findEnvsDropdownLoadingIcon().exists()).toBe(false);
- });
- });
- });
-
- describe('date time picker', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- it('is rendered', () => {
- expect(findDateTimePicker().exists()).toBe(true);
- });
-
- describe('timezone setting', () => {
- const setupWithTimezone = (value) => {
- store = createStore({ dashboardTimezone: value });
- createShallowWrapper();
- };
-
- describe('local timezone is enabled by default', () => {
- it('shows the data time picker in local timezone', () => {
- expect(findDateTimePicker().props('utc')).toBe(false);
- });
- });
-
- describe('when LOCAL timezone is enabled', () => {
- beforeEach(() => {
- setupWithTimezone('LOCAL');
- });
-
- it('shows the data time picker in local timezone', () => {
- expect(findDateTimePicker().props('utc')).toBe(false);
- });
- });
-
- describe('when UTC timezone is enabled', () => {
- beforeEach(() => {
- setupWithTimezone('UTC');
- });
-
- it('shows the data time picker in UTC format', () => {
- expect(findDateTimePicker().props('utc')).toBe(true);
- });
- });
- });
- });
-
- describe('refresh button', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- it('is rendered', () => {
- expect(findRefreshButton().exists()).toBe(true);
- });
- });
-
- describe('external dashboard link', () => {
- beforeEach(async () => {
- store.state.monitoringDashboard.externalDashboardUrl = '/mockUrl';
- createShallowWrapper();
-
- await nextTick();
- });
-
- it('shows the link', () => {
- const externalDashboardButton = wrapper.find('.js-external-dashboard-link');
-
- expect(externalDashboardButton.exists()).toBe(true);
- expect(externalDashboardButton.is(GlButton)).toBe(true);
- expect(externalDashboardButton.text()).toContain('View full dashboard');
- });
- });
-
- describe('actions menu', () => {
- const ootbDashboards = [dashboardGitResponse[0].path];
- const customDashboards = [dashboardGitResponse[1].path];
-
- it('is rendered', () => {
- createShallowWrapper();
-
- expect(findActionsMenu().exists()).toBe(true);
- });
-
- describe('adding metrics prop', () => {
- it.each(ootbDashboards)(
- 'gets passed true if current dashboard is OOTB',
- async (dashboardPath) => {
- createShallowWrapper({ customMetricsAvailable: true });
-
- store.state.monitoringDashboard.emptyState = false;
- setupAllDashboards(store, dashboardPath);
-
- await nextTick();
- expect(findActionsMenu().props('addingMetricsAvailable')).toBe(true);
- },
- );
-
- it.each(customDashboards)(
- 'gets passed false if current dashboard is custom',
- async (dashboardPath) => {
- createShallowWrapper({ customMetricsAvailable: true });
-
- store.state.monitoringDashboard.emptyState = false;
- setupAllDashboards(store, dashboardPath);
-
- await nextTick();
- expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
- },
- );
-
- it('gets passed false if empty state is shown', async () => {
- createShallowWrapper({ customMetricsAvailable: true });
-
- store.state.monitoringDashboard.emptyState = true;
- setupAllDashboards(store, ootbDashboards[0]);
-
- await nextTick();
- expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
- });
-
- it('gets passed false if custom metrics are not available', async () => {
- createShallowWrapper({ customMetricsAvailable: false });
-
- store.state.monitoringDashboard.emptyState = false;
- setupAllDashboards(store, ootbDashboards[0]);
-
- await nextTick();
- expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
- });
- });
-
- it('custom metrics path gets passed', async () => {
- const path = 'https://path/to/customMetrics';
-
- createShallowWrapper({ customMetricsPath: path });
-
- await nextTick();
- expect(findActionsMenu().props('customMetricsPath')).toBe(path);
- });
-
- it('validate query path gets passed', async () => {
- const path = 'https://path/to/validateQuery';
-
- createShallowWrapper({ validateQueryPath: path });
-
- await nextTick();
- expect(findActionsMenu().props('validateQueryPath')).toBe(path);
- });
-
- it('default branch gets passed', async () => {
- const branch = 'branchName';
-
- createShallowWrapper({ defaultBranch: branch });
-
- await nextTick();
- expect(findActionsMenu().props('defaultBranch')).toBe(branch);
- });
- });
-
- describe('metrics settings button', () => {
- const findSettingsButton = () => wrapper.find('[data-testid="metrics-settings-button"]');
- const url = 'https://path/to/project/settings';
-
- beforeEach(() => {
- createShallowWrapper();
-
- store.state.monitoringDashboard.canAccessOperationsSettings = false;
- store.state.monitoringDashboard.operationsSettingsPath = '';
- });
-
- it('is rendered when the user can access the project settings and path to settings is available', async () => {
- store.state.monitoringDashboard.canAccessOperationsSettings = true;
- store.state.monitoringDashboard.operationsSettingsPath = url;
-
- await nextTick();
- expect(findSettingsButton().exists()).toBe(true);
- });
-
- it('is not rendered when the user can not access the project settings', async () => {
- store.state.monitoringDashboard.canAccessOperationsSettings = false;
- store.state.monitoringDashboard.operationsSettingsPath = url;
-
- await nextTick();
- expect(findSettingsButton().exists()).toBe(false);
- });
-
- it('is not rendered when the path to settings is unavailable', async () => {
- store.state.monitoringDashboard.canAccessOperationsSettings = false;
- store.state.monitoringDashboard.operationsSettingsPath = '';
-
- await nextTick();
- expect(findSettingsButton().exists()).toBe(false);
- });
-
- it('leads to the project settings page', async () => {
- store.state.monitoringDashboard.canAccessOperationsSettings = true;
- store.state.monitoringDashboard.operationsSettingsPath = url;
-
- await nextTick();
- expect(findSettingsButton().attributes('href')).toBe(url);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
deleted file mode 100644
index 1cfd132b123..00000000000
--- a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
+++ /dev/null
@@ -1,226 +0,0 @@
-import { GlCard, GlForm, GlFormTextarea, GlAlert } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
-import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue';
-import { createStore } from '~/monitoring/stores';
-import * as types from '~/monitoring/stores/mutation_types';
-import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
-import { metricsDashboardResponse } from '../fixture_data';
-import { mockTimeRange } from '../mock_data';
-
-const mockPanel = metricsDashboardResponse.dashboard.panel_groups[0].panels[0];
-
-describe('dashboard invalid url parameters', () => {
- let store;
- let wrapper;
- let mockShowToast;
-
- const createComponent = (props = {}, options = {}) => {
- wrapper = shallowMount(DashboardPanelBuilder, {
- propsData: { ...props },
- store,
- stubs: {
- GlCard,
- },
- mocks: {
- $toast: {
- show: mockShowToast,
- },
- },
- options,
- });
- };
-
- const findForm = () => wrapper.findComponent(GlForm);
- const findTxtArea = () => findForm().findComponent(GlFormTextarea);
- const findSubmitBtn = () => findForm().find('[type="submit"]');
- const findClipboardCopyBtn = () => wrapper.findComponent({ ref: 'clipboardCopyBtn' });
- const findViewDocumentationBtn = () => wrapper.findComponent({ ref: 'viewDocumentationBtn' });
- const findOpenRepositoryBtn = () => wrapper.findComponent({ ref: 'openRepositoryBtn' });
- const findPanel = () => wrapper.findComponent(DashboardPanel);
- const findTimeRangePicker = () => wrapper.findComponent(DateTimePicker);
- const findRefreshButton = () => wrapper.find('[data-testid="previewRefreshButton"]');
-
- beforeEach(() => {
- mockShowToast = jest.fn();
- store = createStore();
- createComponent();
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- });
-
- it('is mounted', () => {
- expect(wrapper.exists()).toBe(true);
- });
-
- it('displays an empty dashboard panel', () => {
- expect(findPanel().exists()).toBe(true);
- expect(findPanel().props('graphData')).toBe(null);
- });
-
- it('does not fetch initial data by default', () => {
- expect(store.dispatch).not.toHaveBeenCalled();
- });
-
- describe('yml form', () => {
- it('form exists and can be submitted', () => {
- expect(findForm().exists()).toBe(true);
- expect(findSubmitBtn().exists()).toBe(true);
- expect(findSubmitBtn().props('disabled')).toBe(false);
- });
-
- it('form has a text area with a default value', () => {
- expect(findTxtArea().exists()).toBe(true);
-
- const value = findTxtArea().attributes('value');
-
- // Panel definition should contain a title and a type
- expect(value).toContain('title:');
- expect(value).toContain('type:');
- });
-
- it('"copy to clipboard" button works', () => {
- findClipboardCopyBtn().vm.$emit('click');
- const clipboardText = findClipboardCopyBtn().attributes('data-clipboard-text');
-
- expect(clipboardText).toContain('title:');
- expect(clipboardText).toContain('type:');
-
- expect(mockShowToast).toHaveBeenCalledTimes(1);
- });
-
- it('on submit fetches a panel preview', async () => {
- findForm().vm.$emit('submit', new Event('submit'));
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/fetchPanelPreview',
- expect.stringContaining('title:'),
- );
- });
-
- describe('when form is submitted', () => {
- beforeEach(async () => {
- store.commit(`monitoringDashboard/${types.REQUEST_PANEL_PREVIEW}`, 'mock yml content');
- await nextTick();
- });
-
- it('submit button is disabled', () => {
- expect(findSubmitBtn().props('disabled')).toBe(true);
- });
- });
- });
-
- describe('time range picker', () => {
- it('is visible by default', () => {
- expect(findTimeRangePicker().exists()).toBe(true);
- });
-
- it('when changed does not trigger data fetch unless preview panel button is clicked', async () => {
- // mimic initial state where SET_PANEL_PREVIEW_IS_SHOWN is set to false
- store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, false);
-
- await nextTick();
- expect(store.dispatch).not.toHaveBeenCalled();
- });
-
- it('when changed triggers data fetch if preview panel button is clicked', async () => {
- findForm().vm.$emit('submit', new Event('submit'));
-
- store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange);
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalled();
- });
- });
-
- describe('refresh', () => {
- it('is visible by default', () => {
- expect(findRefreshButton().exists()).toBe(true);
- });
-
- it('when clicked does not trigger data fetch unless preview panel button is clicked', async () => {
- // mimic initial state where SET_PANEL_PREVIEW_IS_SHOWN is set to false
- store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, false);
-
- await nextTick();
- expect(store.dispatch).not.toHaveBeenCalled();
- });
-
- it('when clicked triggers data fetch if preview panel button is clicked', async () => {
- // mimic state where preview is visible. SET_PANEL_PREVIEW_IS_SHOWN is set to true
- store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, true);
-
- findRefreshButton().vm.$emit('click');
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/fetchPanelPreviewMetrics',
- undefined,
- );
- });
- });
-
- describe('instructions card', () => {
- const mockDocsPath = '/docs-path';
- const mockProjectPath = '/project-path';
-
- beforeEach(() => {
- store.state.monitoringDashboard.addDashboardDocumentationPath = mockDocsPath;
- store.state.monitoringDashboard.projectPath = mockProjectPath;
-
- createComponent();
- });
-
- it('displays next actions for the user', () => {
- expect(findViewDocumentationBtn().exists()).toBe(true);
- expect(findViewDocumentationBtn().attributes('href')).toBe(mockDocsPath);
-
- expect(findOpenRepositoryBtn().exists()).toBe(true);
- expect(findOpenRepositoryBtn().attributes('href')).toBe(mockProjectPath);
- });
- });
-
- describe('when there is an error', () => {
- const mockError = 'an error occurred!';
-
- beforeEach(async () => {
- store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_FAILURE}`, mockError);
- await nextTick();
- });
-
- it('displays an alert', () => {
- expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
- expect(wrapper.findComponent(GlAlert).text()).toBe(mockError);
- });
-
- it('displays an empty dashboard panel', () => {
- expect(findPanel().props('graphData')).toBe(null);
- });
-
- it('changing time range should not refetch data', async () => {
- store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange);
-
- await nextTick();
- expect(store.dispatch).not.toHaveBeenCalled();
- });
- });
-
- describe('when panel data is available', () => {
- beforeEach(async () => {
- store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_SUCCESS}`, mockPanel);
- await nextTick();
- });
-
- it('displays no alert', () => {
- expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
- });
-
- it('displays panel with data', () => {
- const { title, type } = wrapper.findComponent(DashboardPanel).props('graphData');
-
- expect(title).toBe(mockPanel.title);
- expect(type).toBe(mockPanel.type);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
deleted file mode 100644
index 491649e5b96..00000000000
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ /dev/null
@@ -1,582 +0,0 @@
-import { GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import AxiosMockAdapter from 'axios-mock-adapter';
-import Vuex from 'vuex';
-import { nextTick } from 'vue';
-import axios from '~/lib/utils/axios_utils';
-
-import MonitorAnomalyChart from '~/monitoring/components/charts/anomaly.vue';
-import MonitorBarChart from '~/monitoring/components/charts/bar.vue';
-import MonitorColumnChart from '~/monitoring/components/charts/column.vue';
-import MonitorEmptyChart from '~/monitoring/components/charts/empty_chart.vue';
-import MonitorHeatmapChart from '~/monitoring/components/charts/heatmap.vue';
-import MonitorSingleStatChart from '~/monitoring/components/charts/single_stat.vue';
-import MonitorStackedColumnChart from '~/monitoring/components/charts/stacked_column.vue';
-import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
-import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
-import { panelTypes } from '~/monitoring/constants';
-
-import { createStore, monitoringDashboard } from '~/monitoring/stores';
-import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group';
-import { dashboardProps, graphData, graphDataEmpty } from '../fixture_data';
-import {
- anomalyGraphData,
- singleStatGraphData,
- heatmapGraphData,
- barGraphData,
-} from '../graph_data';
-import { mockNamespace, mockNamespacedData, mockTimeRange } from '../mock_data';
-
-const mocks = {
- $toast: {
- show: jest.fn(),
- },
-};
-
-describe('Dashboard Panel', () => {
- let axiosMock;
- let store;
- let state;
- let wrapper;
-
- const exampleText = 'example_text';
-
- const findCopyLink = () => wrapper.findComponent({ ref: 'copyChartLink' });
- const findTimeChart = () => wrapper.findComponent({ ref: 'timeSeriesChart' });
- const findTitle = () => wrapper.findComponent({ ref: 'graphTitle' });
- const findCtxMenu = () => wrapper.findComponent({ ref: 'contextualMenu' });
- const findMenuItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findMenuItemByText = (text) => findMenuItems().filter((i) => i.text() === text);
-
- const createWrapper = (props, { mountFn = shallowMount, ...options } = {}) => {
- wrapper = mountFn(DashboardPanel, {
- propsData: {
- graphData,
- settingsPath: dashboardProps.settingsPath,
- ...props,
- },
- store,
- mocks,
- ...options,
- });
- };
-
- const mockGetterReturnValue = (getter, value) => {
- jest.spyOn(monitoringDashboard.getters, getter).mockReturnValue(value);
- store = new Vuex.Store({
- modules: {
- monitoringDashboard,
- },
- });
- };
-
- beforeEach(() => {
- store = createStore();
- state = store.state.monitoringDashboard;
-
- axiosMock = new AxiosMockAdapter(axios);
-
- jest.spyOn(URL, 'createObjectURL');
- });
-
- afterEach(() => {
- axiosMock.reset();
- });
-
- describe('Renders slots', () => {
- it('renders "topLeft" slot', () => {
- createWrapper(
- {},
- {
- slots: {
- 'top-left': `<div class="top-left-content">OK</div>`,
- },
- },
- );
-
- expect(wrapper.find('.top-left-content').exists()).toBe(true);
- expect(wrapper.find('.top-left-content').text()).toBe('OK');
- });
- });
-
- describe('When no graphData is available', () => {
- beforeEach(() => {
- createWrapper({
- graphData: graphDataEmpty,
- });
- });
-
- it('renders the chart title', () => {
- expect(findTitle().text()).toBe(graphDataEmpty.title);
- });
-
- it('renders no download csv link', () => {
- expect(wrapper.findComponent({ ref: 'downloadCsvLink' }).exists()).toBe(false);
- });
-
- it('does not contain graph widgets', () => {
- expect(findCtxMenu().exists()).toBe(false);
- });
-
- it('The Empty Chart component is rendered and is a Vue instance', () => {
- expect(wrapper.findComponent(MonitorEmptyChart).exists()).toBe(true);
- });
- });
-
- describe('When graphData is null', () => {
- beforeEach(() => {
- createWrapper({
- graphData: null,
- });
- });
-
- it('renders no chart title', () => {
- expect(findTitle().text()).toBe('');
- });
-
- it('renders no download csv link', () => {
- expect(wrapper.findComponent({ ref: 'downloadCsvLink' }).exists()).toBe(false);
- });
-
- it('does not contain graph widgets', () => {
- expect(findCtxMenu().exists()).toBe(false);
- });
-
- it('The Empty Chart component is rendered and is a Vue instance', () => {
- expect(wrapper.findComponent(MonitorEmptyChart).exists()).toBe(true);
- });
- });
-
- describe('When graphData is available', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('renders the chart title', () => {
- expect(findTitle().text()).toBe(graphData.title);
- });
-
- it('contains graph widgets', () => {
- expect(findCtxMenu().exists()).toBe(true);
- expect(wrapper.findComponent({ ref: 'downloadCsvLink' }).exists()).toBe(true);
- });
-
- it('sets no clipboard copy link on dropdown by default', () => {
- expect(findCopyLink().exists()).toBe(false);
- });
-
- it('should emit `timerange` event when a zooming in/out in a chart occcurs', async () => {
- const timeRange = {
- start: '2020-01-01T00:00:00.000Z',
- end: '2020-01-01T01:00:00.000Z',
- };
-
- jest.spyOn(wrapper.vm, '$emit');
-
- findTimeChart().vm.$emit('datazoom', timeRange);
-
- await nextTick();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('timerangezoom', timeRange);
- });
-
- it('includes a default group id', () => {
- expect(wrapper.vm.groupId).toBe('dashboard-panel');
- });
-
- describe('Supports different panel types', () => {
- const dataWithType = (type) => {
- return {
- ...graphData,
- type,
- };
- };
-
- it('empty chart is rendered for empty results', () => {
- createWrapper({ graphData: graphDataEmpty });
- expect(wrapper.findComponent(MonitorEmptyChart).exists()).toBe(true);
- });
-
- it('area chart is rendered by default', () => {
- createWrapper();
- expect(wrapper.findComponent(MonitorTimeSeriesChart).exists()).toBe(true);
- });
-
- describe.each`
- data | component | hasCtxMenu
- ${dataWithType(panelTypes.AREA_CHART)} | ${MonitorTimeSeriesChart} | ${true}
- ${dataWithType(panelTypes.LINE_CHART)} | ${MonitorTimeSeriesChart} | ${true}
- ${singleStatGraphData()} | ${MonitorSingleStatChart} | ${true}
- ${anomalyGraphData()} | ${MonitorAnomalyChart} | ${false}
- ${dataWithType(panelTypes.COLUMN)} | ${MonitorColumnChart} | ${false}
- ${dataWithType(panelTypes.STACKED_COLUMN)} | ${MonitorStackedColumnChart} | ${false}
- ${heatmapGraphData()} | ${MonitorHeatmapChart} | ${false}
- ${barGraphData()} | ${MonitorBarChart} | ${false}
- `('when $data.type data is provided', ({ data, component, hasCtxMenu }) => {
- const attrs = { attr1: 'attr1Value', attr2: 'attr2Value' };
-
- beforeEach(() => {
- createWrapper({ graphData: data }, { attrs });
- });
-
- it(`renders the chart component and binds attributes`, () => {
- expect(wrapper.findComponent(component).exists()).toBe(true);
- expect(wrapper.findComponent(component).attributes()).toMatchObject(attrs);
- });
-
- it(`contextual menu is ${hasCtxMenu ? '' : 'not '}shown`, () => {
- expect(findCtxMenu().exists()).toBe(hasCtxMenu);
- });
- });
- });
-
- describe('computed', () => {
- describe('fixedCurrentTimeRange', () => {
- it('returns fixed time for valid time range', async () => {
- state.timeRange = mockTimeRange;
- await nextTick();
- expect(findTimeChart().props('timeRange')).toEqual(
- expect.objectContaining({
- start: expect.any(String),
- end: expect.any(String),
- }),
- );
- });
-
- it.each`
- input | output
- ${''} | ${{}}
- ${undefined} | ${{}}
- ${null} | ${{}}
- ${'2020-12-03'} | ${{}}
- `('returns $output for invalid input like $input', async ({ input, output }) => {
- state.timeRange = input;
- await nextTick();
- expect(findTimeChart().props('timeRange')).toEqual(output);
- });
- });
- });
- });
-
- describe('Edit custom metric dropdown item', () => {
- const findEditCustomMetricLink = () => wrapper.findComponent({ ref: 'editMetricLink' });
- const mockEditPath = '/root/kubernetes-gke-project/prometheus/metrics/23/edit';
-
- beforeEach(async () => {
- createWrapper();
- await nextTick();
- });
-
- it('is not present if the panel is not a custom metric', () => {
- expect(findEditCustomMetricLink().exists()).toBe(false);
- });
-
- it('is present when the panel contains an edit_path property', async () => {
- wrapper.setProps({
- graphData: {
- ...graphData,
- metrics: [
- {
- ...graphData.metrics[0],
- edit_path: mockEditPath,
- },
- ],
- },
- });
-
- await nextTick();
- expect(findEditCustomMetricLink().exists()).toBe(true);
- expect(findEditCustomMetricLink().text()).toBe('Edit metric');
- expect(findEditCustomMetricLink().attributes('href')).toBe(mockEditPath);
- });
-
- it('shows an "Edit metrics" link pointing to settingsPath for a panel with multiple metrics', async () => {
- wrapper.setProps({
- graphData: {
- ...graphData,
- metrics: [
- {
- ...graphData.metrics[0],
- edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit',
- },
- {
- ...graphData.metrics[0],
- edit_path: '/root/kubernetes-gke-project/prometheus/metrics/23/edit',
- },
- ],
- },
- });
-
- await nextTick();
- expect(findEditCustomMetricLink().text()).toBe('Edit metrics');
- expect(findEditCustomMetricLink().attributes('href')).toBe(dashboardProps.settingsPath);
- });
- });
-
- describe('when clipboard data is available', () => {
- const clipboardText = 'A value to copy.';
-
- beforeEach(() => {
- createWrapper({
- clipboardText,
- });
- });
-
- it('sets clipboard text on the dropdown', () => {
- expect(findCopyLink().exists()).toBe(true);
- expect(findCopyLink().element.dataset.clipboardText).toBe(clipboardText);
- });
-
- it('adds a copy button to the dropdown', () => {
- expect(findCopyLink().text()).toContain('Copy link to chart');
- });
-
- it('opens a toast on click', () => {
- findCopyLink().vm.$emit('click');
-
- expect(wrapper.vm.$toast.show).toHaveBeenCalled();
- });
- });
-
- describe('when clipboard data is not available', () => {
- it('there is no "copy to clipboard" link for a null value', () => {
- createWrapper({ clipboardText: null });
- expect(findCopyLink().exists()).toBe(false);
- });
-
- it('there is no "copy to clipboard" link for an empty value', () => {
- createWrapper({ clipboardText: '' });
- expect(findCopyLink().exists()).toBe(false);
- });
- });
-
- describe('when downloading metrics data as CSV', () => {
- beforeEach(async () => {
- wrapper = shallowMount(DashboardPanel, {
- propsData: {
- clipboardText: exampleText,
- settingsPath: dashboardProps.settingsPath,
- graphData: {
- y_label: 'metric',
- ...graphData,
- },
- },
- store,
- });
- await nextTick();
- });
-
- describe('csvText', () => {
- it('converts metrics data from json to csv', () => {
- const header = `timestamp,"${graphData.y_label} > ${graphData.metrics[0].label}"`;
- const data = graphData.metrics[0].result[0].values;
- const firstRow = `${data[0][0]},${data[0][1]}`;
- const secondRow = `${data[1][0]},${data[1][1]}`;
-
- expect(wrapper.vm.csvText).toMatch(`${header}\r\n${firstRow}\r\n${secondRow}\r\n`);
- });
- });
-
- describe('downloadCsv', () => {
- it('produces a link with a Blob', () => {
- expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(expect.any(Blob));
- expect(global.URL.createObjectURL).toHaveBeenLastCalledWith(
- expect.objectContaining({
- size: wrapper.vm.csvText.length,
- type: 'text/plain',
- }),
- );
- });
- });
- });
-
- describe('when using dynamic modules', () => {
- const { mockDeploymentData, mockProjectPath } = mockNamespacedData;
-
- beforeEach(() => {
- store = createEmbedGroupStore();
- store.registerModule(mockNamespace, monitoringDashboard);
- store.state.embedGroup.modules.push(mockNamespace);
-
- createWrapper({ namespace: mockNamespace });
- });
-
- it('handles namespaced deployment data state', async () => {
- store.state[mockNamespace].deploymentData = mockDeploymentData;
-
- await nextTick();
- expect(findTimeChart().props().deploymentData).toEqual(mockDeploymentData);
- });
-
- it('handles namespaced project path state', async () => {
- store.state[mockNamespace].projectPath = mockProjectPath;
-
- await nextTick();
- expect(findTimeChart().props().projectPath).toBe(mockProjectPath);
- });
-
- it('renders a time series chart with no errors', () => {
- expect(wrapper.findComponent(MonitorTimeSeriesChart).exists()).toBe(true);
- });
- });
-
- describe('panel timezone', () => {
- it('displays a time chart in local timezone', () => {
- createWrapper();
- expect(findTimeChart().props('timezone')).toBe('LOCAL');
- });
-
- it('displays a heatmap in local timezone', () => {
- createWrapper({ graphData: heatmapGraphData() });
- expect(wrapper.findComponent(MonitorHeatmapChart).props('timezone')).toBe('LOCAL');
- });
-
- describe('when timezone is set to UTC', () => {
- beforeEach(() => {
- store = createStore({ dashboardTimezone: 'UTC' });
- });
-
- it('displays a time chart with UTC', () => {
- createWrapper();
- expect(findTimeChart().props('timezone')).toBe('UTC');
- });
-
- it('displays a heatmap with UTC', () => {
- createWrapper({ graphData: heatmapGraphData() });
- expect(wrapper.findComponent(MonitorHeatmapChart).props('timezone')).toBe('UTC');
- });
- });
- });
-
- describe('Expand to full screen', () => {
- const findExpandBtn = () => wrapper.findComponent({ ref: 'expandBtn' });
-
- describe('when there is no @expand listener', () => {
- it('does not show `View full screen` option', () => {
- createWrapper();
- expect(findExpandBtn().exists()).toBe(false);
- });
- });
-
- describe('when there is an @expand listener', () => {
- beforeEach(() => {
- createWrapper({}, { listeners: { expand: () => {} } });
- });
-
- it('shows the `expand` option', () => {
- expect(findExpandBtn().exists()).toBe(true);
- });
-
- it('emits the `expand` event', () => {
- const preventDefault = jest.fn();
- findExpandBtn().vm.$emit('click', { preventDefault });
- expect(wrapper.emitted('expand')).toHaveLength(1);
- expect(preventDefault).toHaveBeenCalled();
- });
- });
- });
-
- describe('When graphData contains links', () => {
- const findManageLinksItem = () => wrapper.findComponent({ ref: 'manageLinksItem' });
- const mockLinks = [
- {
- url: 'https://example.com',
- title: 'Example 1',
- },
- {
- url: 'https://gitlab.com',
- title: 'Example 2',
- },
- ];
- const createWrapperWithLinks = (links = mockLinks) => {
- createWrapper({
- graphData: {
- ...graphData,
- links,
- },
- });
- };
-
- it('custom links are shown', () => {
- createWrapperWithLinks();
-
- mockLinks.forEach(({ url, title }) => {
- const link = findMenuItemByText(title).at(0);
-
- expect(link.exists()).toBe(true);
- expect(link.attributes('href')).toBe(url);
- });
- });
-
- it("custom links don't show unsecure content", () => {
- createWrapperWithLinks([
- {
- title: '<script>alert("XSS")</script>',
- url: 'http://example.com',
- },
- ]);
-
- expect(findMenuItems().at(1).element.innerHTML).toBe(
- '&lt;script&gt;alert("XSS")&lt;/script&gt;',
- );
- });
-
- it("custom links don't show unsecure href attributes", () => {
- const title = 'Owned!';
-
- createWrapperWithLinks([
- {
- title,
- // eslint-disable-next-line no-script-url
- url: 'javascript:alert("Evil")',
- },
- ]);
-
- const link = findMenuItemByText(title).at(0);
- expect(link.attributes('href')).toBe('#');
- });
-
- it('when an editable dashboard is selected, shows `Manage chart links` link to the blob path', () => {
- const editUrl = '/edit';
- mockGetterReturnValue('selectedDashboard', {
- can_edit: true,
- project_blob_path: editUrl,
- });
- createWrapperWithLinks();
-
- expect(findManageLinksItem().exists()).toBe(true);
- expect(findManageLinksItem().attributes('href')).toBe(editUrl);
- });
-
- it('when no dashboard is selected, does not show `Manage chart links`', () => {
- mockGetterReturnValue('selectedDashboard', null);
- createWrapperWithLinks();
-
- expect(findManageLinksItem().exists()).toBe(false);
- });
-
- it('when non-editable dashboard is selected, does not show `Manage chart links`', () => {
- const editUrl = '/edit';
- mockGetterReturnValue('selectedDashboard', {
- can_edit: false,
- project_blob_path: editUrl,
- });
- createWrapperWithLinks();
-
- expect(findManageLinksItem().exists()).toBe(false);
- });
- });
-
- describe('Runbook url', () => {
- const findRunbookLinks = () => wrapper.findAll('[data-testid="runbookLink"]');
-
- beforeEach(() => {
- mockGetterReturnValue('metricsSavedToDb', []);
- });
-
- it('does not show a runbook link when alerts are not present', () => {
- createWrapper();
-
- expect(findRunbookLinks().length).toBe(0);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
deleted file mode 100644
index d7f1d4873bb..00000000000
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ /dev/null
@@ -1,784 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import VueDraggable from 'vuedraggable';
-import { nextTick } from 'vue';
-import setWindowLocation from 'helpers/set_window_location_helper';
-import { TEST_HOST } from 'helpers/test_constants';
-import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { objectToQuery } from '~/lib/utils/url_utility';
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
-import EmptyState from '~/monitoring/components/empty_state.vue';
-import GraphGroup from '~/monitoring/components/graph_group.vue';
-import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
-import LinksSection from '~/monitoring/components/links_section.vue';
-import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
-import { createStore } from '~/monitoring/stores';
-import * as types from '~/monitoring/stores/mutation_types';
-import {
- metricsDashboardViewModel,
- metricsDashboardPanelCount,
- dashboardProps,
-} from '../fixture_data';
-import { dashboardGitResponse, storeVariables } from '../mock_data';
-import {
- setupAllDashboards,
- setupStoreWithDashboard,
- setMetricResult,
- setupStoreWithData,
- setupStoreWithDataForPanelCount,
- setupStoreWithLinks,
-} from '../store_utils';
-
-jest.mock('~/alert');
-
-describe('Dashboard', () => {
- let store;
- let wrapper;
- let mock;
-
- const createShallowWrapper = (props = {}, options = {}) => {
- wrapper = shallowMountExtended(Dashboard, {
- propsData: { ...dashboardProps, ...props },
- store,
- stubs: {
- DashboardHeader,
- },
- ...options,
- });
- };
-
- const createMountedWrapper = (props = {}, options = {}) => {
- wrapper = mountExtended(Dashboard, {
- propsData: { ...dashboardProps, ...props },
- store,
- stubs: {
- 'graph-group': true,
- 'dashboard-panel': true,
- 'dashboard-header': DashboardHeader,
- },
- ...options,
- });
- };
-
- beforeEach(() => {
- store = createStore();
- mock = new MockAdapter(axios);
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- });
-
- afterEach(() => {
- mock.restore();
- if (store.dispatch.mockReset) {
- store.dispatch.mockReset();
- }
- });
-
- describe('request information to the server', () => {
- it('calls to set time range and fetch data', async () => {
- createShallowWrapper({ hasMetrics: true });
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- expect.any(Object),
- );
-
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
-
- it('shows up a loading state', async () => {
- store.state.monitoringDashboard.emptyState = dashboardEmptyStates.LOADING;
-
- createShallowWrapper({ hasMetrics: true });
-
- await nextTick();
- expect(wrapper.findComponent(EmptyState).exists()).toBe(true);
- expect(wrapper.findComponent(EmptyState).props('selectedState')).toBe(
- dashboardEmptyStates.LOADING,
- );
- });
-
- it('hides the group panels when showPanels is false', async () => {
- createMountedWrapper({ hasMetrics: true, showPanels: false });
-
- setupStoreWithData(store);
-
- await nextTick();
- expect(wrapper.vm.emptyState).toBeNull();
- expect(wrapper.findAll('.prometheus-panel')).toHaveLength(0);
- });
-
- it('fetches the metrics data with proper time window', async () => {
- createMountedWrapper({ hasMetrics: true });
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- expect.objectContaining({ duration: { seconds: 28800 } }),
- );
- });
- });
-
- describe('panel containers layout', () => {
- const findPanelLayoutWrapperAt = (index) => {
- return wrapper
- .findComponent(GraphGroup)
- .findAll('[data-testid="dashboard-panel-layout-wrapper"]')
- .at(index);
- };
-
- beforeEach(async () => {
- createMountedWrapper({ hasMetrics: true });
- await nextTick();
- });
-
- describe('when the graph group has an even number of panels', () => {
- it('2 panels - all panel wrappers take half width of their parent', async () => {
- setupStoreWithDataForPanelCount(store, 2);
-
- await nextTick();
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
- });
-
- it('4 panels - all panel wrappers take half width of their parent', async () => {
- setupStoreWithDataForPanelCount(store, 4);
-
- await nextTick();
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(3).classes('col-lg-6')).toBe(true);
- });
- });
-
- describe('when the graph group has an odd number of panels', () => {
- it('1 panel - panel wrapper does not take half width of its parent', async () => {
- setupStoreWithDataForPanelCount(store, 1);
-
- await nextTick();
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(false);
- });
-
- it('3 panels - all panels but last take half width of their parents', async () => {
- setupStoreWithDataForPanelCount(store, 3);
-
- await nextTick();
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(false);
- });
-
- it('5 panels - all panels but last take half width of their parents', async () => {
- setupStoreWithDataForPanelCount(store, 5);
-
- await nextTick();
- expect(findPanelLayoutWrapperAt(0).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(1).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(2).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(3).classes('col-lg-6')).toBe(true);
- expect(findPanelLayoutWrapperAt(4).classes('col-lg-6')).toBe(false);
- });
- });
- });
-
- describe('dashboard validation warning', () => {
- it('displays a warning if there are validation warnings', async () => {
- createMountedWrapper({ hasMetrics: true });
-
- store.commit(
- `monitoringDashboard/${types.RECEIVE_DASHBOARD_VALIDATION_WARNINGS_SUCCESS}`,
- true,
- );
-
- await nextTick();
- expect(createAlert).toHaveBeenCalled();
- });
-
- it('does not display a warning if there are no validation warnings', async () => {
- createMountedWrapper({ hasMetrics: true });
-
- store.commit(
- `monitoringDashboard/${types.RECEIVE_DASHBOARD_VALIDATION_WARNINGS_SUCCESS}`,
- false,
- );
-
- await nextTick();
- expect(createAlert).not.toHaveBeenCalled();
- });
- });
-
- describe('when the URL contains a reference to a panel', () => {
- const location = window.location.href;
-
- const setSearch = (searchParams) => {
- setWindowLocation(`?${objectToQuery(searchParams)}`);
- };
-
- afterEach(() => {
- setWindowLocation(location);
- });
-
- it('when the URL points to a panel it expands', async () => {
- const panelGroup = metricsDashboardViewModel.panelGroups[0];
- const panel = panelGroup.panels[0];
-
- setSearch({
- group: panelGroup.group,
- title: panel.title,
- y_label: panel.y_label,
- });
-
- createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(store);
-
- await nextTick();
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', {
- group: panelGroup.group,
- panel: expect.objectContaining({
- title: panel.title,
- y_label: panel.y_label,
- }),
- });
- });
-
- it('when the URL does not link to any panel, no panel is expanded', async () => {
- setSearch();
-
- createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(store);
-
- await nextTick();
- expect(store.dispatch).not.toHaveBeenCalledWith(
- 'monitoringDashboard/setExpandedPanel',
- expect.anything(),
- );
- });
-
- it('when the URL points to an incorrect panel it shows an error', async () => {
- const panelGroup = metricsDashboardViewModel.panelGroups[0];
- const panel = panelGroup.panels[0];
-
- setSearch({
- group: panelGroup.group,
- title: 'incorrect',
- y_label: panel.y_label,
- });
-
- createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(store);
-
- await nextTick();
- expect(createAlert).toHaveBeenCalled();
- expect(store.dispatch).not.toHaveBeenCalledWith(
- 'monitoringDashboard/setExpandedPanel',
- expect.anything(),
- );
- });
- });
-
- describe('when the panel is expanded', () => {
- let group;
- let panel;
-
- const expandPanel = (mockGroup, mockPanel) => {
- store.commit(`monitoringDashboard/${types.SET_EXPANDED_PANEL}`, {
- group: mockGroup,
- panel: mockPanel,
- });
- };
-
- beforeEach(() => {
- setupStoreWithData(store);
-
- const { panelGroups } = store.state.monitoringDashboard.dashboard;
- group = panelGroups[0].group;
- [panel] = panelGroups[0].panels;
-
- jest.spyOn(window.history, 'pushState').mockImplementation();
- });
-
- afterEach(() => {
- window.history.pushState.mockRestore();
- });
-
- it('URL is updated with panel parameters', async () => {
- createMountedWrapper({ hasMetrics: true });
- expandPanel(group, panel);
-
- const expectedSearch = objectToQuery({
- group,
- title: panel.title,
- y_label: panel.y_label,
- });
-
- await nextTick();
- expect(window.history.pushState).toHaveBeenCalledTimes(1);
- expect(window.history.pushState).toHaveBeenCalledWith(
- expect.anything(), // state
- expect.any(String), // document title
- expect.stringContaining(`${expectedSearch}`),
- );
- });
-
- it('URL is updated with panel parameters and custom dashboard', async () => {
- const dashboard = 'dashboard.yml';
-
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: dashboard,
- });
- createMountedWrapper({ hasMetrics: true });
- expandPanel(group, panel);
-
- const expectedSearch = objectToQuery({
- dashboard,
- group,
- title: panel.title,
- y_label: panel.y_label,
- });
-
- await nextTick();
- expect(window.history.pushState).toHaveBeenCalledTimes(1);
- expect(window.history.pushState).toHaveBeenCalledWith(
- expect.anything(), // state
- expect.any(String), // document title
- expect.stringContaining(`${expectedSearch}`),
- );
- });
-
- it('URL is updated with no parameters', async () => {
- expandPanel(group, panel);
- createMountedWrapper({ hasMetrics: true });
- expandPanel(null, null);
-
- await nextTick();
- expect(window.history.pushState).toHaveBeenCalledTimes(1);
- expect(window.history.pushState).toHaveBeenCalledWith(
- expect.anything(), // state
- expect.any(String), // document title
- expect.not.stringMatching(/group|title|y_label/), // no panel params
- );
- });
- });
-
- describe('when all panels in the first group are loading', () => {
- const findGroupAt = (i) => wrapper.findAllComponents(GraphGroup).at(i);
-
- beforeEach(async () => {
- setupStoreWithDashboard(store);
-
- const { panels } = store.state.monitoringDashboard.dashboard.panelGroups[0];
- panels.forEach(({ metrics }) => {
- store.commit(`monitoringDashboard/${types.REQUEST_METRIC_RESULT}`, {
- metricId: metrics[0].metricId,
- });
- });
-
- createShallowWrapper();
-
- await nextTick();
- });
-
- it('a loading icon appears in the first group', () => {
- expect(findGroupAt(0).props('isLoading')).toBe(true);
- });
-
- it('a loading icon does not appear in the second group', () => {
- expect(findGroupAt(1).props('isLoading')).toBe(false);
- });
- });
-
- describe('when all requests have been committed by the store', () => {
- beforeEach(async () => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentEnvironmentName: 'production',
- currentDashboard: dashboardGitResponse[0].path,
- projectPath: TEST_HOST,
- });
- createMountedWrapper({ hasMetrics: true });
- setupStoreWithData(store);
-
- await nextTick();
- });
-
- it('does not show loading icons in any group', async () => {
- setupStoreWithData(store);
-
- await nextTick();
- wrapper.findAllComponents(GraphGroup).wrappers.forEach((groupWrapper) => {
- expect(groupWrapper.props('isLoading')).toBe(false);
- });
- });
- });
-
- describe('variables section', () => {
- beforeEach(async () => {
- createShallowWrapper({ hasMetrics: true });
- setupStoreWithData(store);
- store.state.monitoringDashboard.variables = storeVariables;
- await nextTick();
- });
-
- it('shows the variables section', () => {
- expect(wrapper.vm.shouldShowVariablesSection).toBe(true);
- });
- });
-
- describe('links section', () => {
- beforeEach(async () => {
- createShallowWrapper({ hasMetrics: true });
- setupStoreWithData(store);
- setupStoreWithLinks(store);
- await nextTick();
- });
-
- it('shows the links section', () => {
- expect(wrapper.vm.shouldShowLinksSection).toBe(true);
- expect(wrapper.findComponent(LinksSection).exists()).toBe(true);
- });
- });
-
- describe('single panel expands to "full screen" mode', () => {
- const findExpandedPanel = () => wrapper.findComponent({ ref: 'expandedPanel' });
-
- describe('when the panel is not expanded', () => {
- beforeEach(async () => {
- createShallowWrapper({ hasMetrics: true });
- setupStoreWithData(store);
- await nextTick();
- });
-
- it('expanded panel is not visible', () => {
- expect(findExpandedPanel().isVisible()).toBe(false);
- });
-
- it('can set a panel as expanded', () => {
- const panel = wrapper.findAllComponents(DashboardPanel).at(1);
-
- jest.spyOn(store, 'dispatch');
-
- panel.vm.$emit('expand');
-
- const groupData = metricsDashboardViewModel.panelGroups[0];
-
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setExpandedPanel', {
- group: groupData.group,
- panel: expect.objectContaining({
- id: groupData.panels[0].id,
- }),
- });
- });
- });
-
- describe('when the panel is expanded', () => {
- let group;
- let panel;
-
- const MockPanel = {
- template: `<div><slot name="top-left"/></div>`,
- };
-
- beforeEach(async () => {
- createShallowWrapper({ hasMetrics: true }, { stubs: { DashboardPanel: MockPanel } });
- setupStoreWithData(store);
-
- const { panelGroups } = store.state.monitoringDashboard.dashboard;
-
- group = panelGroups[0].group;
- [panel] = panelGroups[0].panels;
-
- store.commit(`monitoringDashboard/${types.SET_EXPANDED_PANEL}`, {
- group,
- panel,
- });
-
- jest.spyOn(store, 'dispatch');
- await nextTick();
- });
-
- it('displays a single panel and others are hidden', () => {
- const panels = wrapper.findAllComponents(MockPanel);
- const visiblePanels = panels.filter((w) => w.isVisible());
-
- expect(findExpandedPanel().isVisible()).toBe(true);
- // v-show for hiding panels is more performant than v-if
- // check for panels to be hidden.
- expect(panels.length).toBe(metricsDashboardPanelCount + 1);
- expect(visiblePanels.length).toBe(1);
- });
-
- it('sets a link to the expanded panel', () => {
- const searchQuery =
- '?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=System%20metrics%20(Kubernetes)&title=Memory%20Usage%20(Total)&y_label=Total%20Memory%20Used%20(GB)';
-
- expect(findExpandedPanel().attributes('clipboard-text')).toEqual(
- expect.stringContaining(searchQuery),
- );
- });
-
- it('restores full dashboard by clicking `back`', () => {
- wrapper.findComponent({ ref: 'goBackBtn' }).vm.$emit('click');
-
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/clearExpandedPanel',
- undefined,
- );
- });
- });
- });
-
- describe('when one of the metrics is missing', () => {
- beforeEach(async () => {
- createShallowWrapper({ hasMetrics: true });
-
- setupStoreWithDashboard(store);
- setMetricResult({ store, result: [], panel: 2 });
- await nextTick();
- });
-
- it('shows a group empty area', () => {
- const emptyGroup = wrapper.findAllComponents({ ref: 'empty-group' });
-
- expect(emptyGroup).toHaveLength(1);
- expect(emptyGroup.is(GroupEmptyState)).toBe(true);
- });
-
- it('group empty area displays a NO_DATA state', () => {
- expect(
- wrapper.findAllComponents({ ref: 'empty-group' }).at(0).props('selectedState'),
- ).toEqual(metricStates.NO_DATA);
- });
- });
-
- describe('drag and drop function', () => {
- const findDraggables = () => wrapper.findAllComponents(VueDraggable);
- const findEnabledDraggables = () => findDraggables().filter((f) => !f.attributes('disabled'));
- const findDraggablePanels = () => wrapper.findAll('.js-draggable-panel');
- const findRearrangeButton = () => wrapper.find('.js-rearrange-button');
-
- const setup = async () => {
- // call original dispatch
- store.dispatch.mockRestore();
-
- createShallowWrapper({ hasMetrics: true });
- setupStoreWithData(store);
- await nextTick();
- };
-
- it('wraps vuedraggable', async () => {
- await setup();
-
- expect(findDraggablePanels().exists()).toBe(true);
- expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount);
- });
-
- it('is disabled by default', async () => {
- await setup();
-
- expect(findRearrangeButton().exists()).toBe(false);
- expect(findEnabledDraggables().length).toBe(0);
- });
-
- describe('when rearrange is enabled', () => {
- beforeEach(async () => {
- // call original dispatch
- store.dispatch.mockRestore();
-
- createShallowWrapper({ hasMetrics: true, rearrangePanelsAvailable: true });
- setupStoreWithData(store);
-
- await nextTick();
- });
-
- it('displays rearrange button', () => {
- expect(findRearrangeButton().exists()).toBe(true);
- });
-
- describe('when rearrange button is clicked', () => {
- const findFirstDraggableRemoveButton = () =>
- findDraggablePanels().at(0).find('.js-draggable-remove');
-
- it('enables draggables', async () => {
- findRearrangeButton().vm.$emit('click');
- await nextTick();
-
- expect(findRearrangeButton().attributes('pressed')).toBe('true');
- expect(findEnabledDraggables().wrappers).toEqual(findDraggables().wrappers);
- });
-
- it('metrics can be swapped', async () => {
- findRearrangeButton().vm.$emit('click');
- await nextTick();
-
- const firstDraggable = findDraggables().at(0);
- const mockMetrics = [...metricsDashboardViewModel.panelGroups[0].panels];
-
- const firstTitle = mockMetrics[0].title;
- const secondTitle = mockMetrics[1].title;
-
- // swap two elements and `input` them
- [mockMetrics[0], mockMetrics[1]] = [mockMetrics[1], mockMetrics[0]];
- firstDraggable.vm.$emit('input', mockMetrics);
-
- await nextTick();
-
- const { panels } = wrapper.vm.dashboard.panelGroups[0];
-
- expect(panels[1].title).toEqual(firstTitle);
- expect(panels[0].title).toEqual(secondTitle);
- });
-
- it('shows a remove button, which removes a panel', async () => {
- findRearrangeButton().vm.$emit('click');
- await nextTick();
-
- expect(findFirstDraggableRemoveButton().find('a').exists()).toBe(true);
-
- expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount);
- await findFirstDraggableRemoveButton().trigger('click');
-
- expect(findDraggablePanels().length).toEqual(metricsDashboardPanelCount - 1);
- });
-
- it('disables draggables when clicked again', async () => {
- findRearrangeButton().vm.$emit('click');
- await nextTick();
-
- findRearrangeButton().vm.$emit('click');
- await nextTick();
- expect(findRearrangeButton().attributes('pressed')).toBeUndefined();
- expect(findEnabledDraggables().length).toBe(0);
- });
- });
- });
- });
-
- describe('cluster health', () => {
- beforeEach(async () => {
- createShallowWrapper({ hasMetrics: true, showHeader: false });
-
- // all_dashboards is not defined in health dashboards
- store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, undefined);
- await nextTick();
- });
-
- it('hides dashboard header by default', () => {
- expect(wrapper.findComponent({ ref: 'prometheusGraphsHeader' }).exists()).toEqual(false);
- });
-
- it('renders correctly', () => {
- expect(wrapper.html()).not.toBe('');
- });
- });
-
- describe('document title', () => {
- const originalTitle = 'Original Title';
- const overviewDashboardName = dashboardGitResponse[0].display_name;
-
- beforeEach(() => {
- document.title = originalTitle;
- createShallowWrapper({ hasMetrics: true });
- });
-
- afterAll(() => {
- document.title = '';
- });
-
- it('is prepended with the overview dashboard name by default', async () => {
- setupAllDashboards(store);
-
- await nextTick();
- expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
- });
-
- it('is prepended with dashboard name if path is known', async () => {
- const dashboard = dashboardGitResponse[1];
- const currentDashboard = dashboard.path;
-
- setupAllDashboards(store, currentDashboard);
-
- await nextTick();
- expect(document.title.startsWith(`${dashboard.display_name} · `)).toBe(true);
- });
-
- it('is prepended with the overview dashboard name if path is not known', async () => {
- setupAllDashboards(store, 'unknown/path');
-
- await nextTick();
- expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
- });
-
- it('is not modified when dashboard name is not provided', async () => {
- const dashboard = { ...dashboardGitResponse[1], display_name: null };
- const currentDashboard = dashboard.path;
-
- store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, [dashboard]);
-
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard,
- });
-
- await nextTick();
- expect(document.title).toBe(originalTitle);
- });
- });
-
- describe('Clipboard text in panels', () => {
- const currentDashboard = dashboardGitResponse[1].path;
- const panelIndex = 1; // skip expanded panel
-
- const getClipboardTextFirstPanel = () =>
- wrapper.findAllComponents(DashboardPanel).at(panelIndex).props('clipboardText');
-
- beforeEach(async () => {
- setupStoreWithData(store);
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard,
- });
- createShallowWrapper({ hasMetrics: true });
- await nextTick();
- });
-
- it('contains a link to the dashboard', () => {
- const dashboardParam = `dashboard=${encodeURIComponent(currentDashboard)}`;
-
- expect(getClipboardTextFirstPanel()).toContain(dashboardParam);
- expect(getClipboardTextFirstPanel()).toContain(`group=`);
- expect(getClipboardTextFirstPanel()).toContain(`title=`);
- expect(getClipboardTextFirstPanel()).toContain(`y_label=`);
- });
- });
-
- describe('keyboard shortcuts', () => {
- const currentDashboard = dashboardGitResponse[1].path;
- const panelRef = 'dashboard-panel-response-metrics-aws-elb-4-1'; // skip expanded panel
-
- // While the recommendation in the documentation is to test
- // with a data-testid attribute, I want to make sure that
- // the dashboard panels have a ref attribute set.
- const getDashboardPanel = () => wrapper.findComponent({ ref: panelRef });
-
- beforeEach(async () => {
- setupStoreWithData(store);
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard,
- });
- createShallowWrapper({ hasMetrics: true });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ hoveredPanel: panelRef });
- await nextTick();
- });
-
- it('contains a ref attribute inside a DashboardPanel component', () => {
- const dashboardPanel = getDashboardPanel();
-
- expect(dashboardPanel.exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_template_spec.js b/spec/frontend/monitoring/components/dashboard_template_spec.js
deleted file mode 100644
index 4e220d724f4..00000000000
--- a/spec/frontend/monitoring/components/dashboard_template_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import { createStore } from '~/monitoring/stores';
-import { dashboardProps } from '../fixture_data';
-import { setupAllDashboards } from '../store_utils';
-
-jest.mock('~/lib/utils/url_utility');
-
-describe('Dashboard template', () => {
- let wrapper;
- let store;
- let mock;
-
- beforeEach(() => {
- store = createStore({
- currentEnvironmentName: 'production',
- });
- mock = new MockAdapter(axios);
-
- setupAllDashboards(store);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('matches the default snapshot', () => {
- wrapper = shallowMount(Dashboard, {
- propsData: { ...dashboardProps },
- store,
- stubs: {
- DashboardHeader,
- },
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
deleted file mode 100644
index b123d1e7d79..00000000000
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ /dev/null
@@ -1,159 +0,0 @@
-import { mount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import {
- queryToObject,
- redirectTo, // eslint-disable-line import/no-deprecated
- removeParams,
- mergeUrlParams,
- updateHistory,
-} from '~/lib/utils/url_utility';
-
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import { createStore } from '~/monitoring/stores';
-import { defaultTimeRange } from '~/vue_shared/constants';
-import { dashboardProps } from '../fixture_data';
-import { mockProjectDir } from '../mock_data';
-
-jest.mock('~/alert');
-jest.mock('~/lib/utils/url_utility');
-
-describe('dashboard invalid url parameters', () => {
- let store;
- let wrapper;
- let mock;
-
- const createMountedWrapper = (props = { hasMetrics: true }, options = {}) => {
- wrapper = mount(Dashboard, {
- propsData: { ...dashboardProps, ...props },
- store,
- stubs: { 'graph-group': true, 'dashboard-panel': true, 'dashboard-header': DashboardHeader },
- ...options,
- });
- };
-
- const findDateTimePicker = () =>
- wrapper.findComponent(DashboardHeader).findComponent({ ref: 'dateTimePicker' });
-
- beforeEach(() => {
- store = createStore();
- jest.spyOn(store, 'dispatch');
-
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- queryToObject.mockReset();
- });
-
- it('passes default url parameters to the time range picker', async () => {
- queryToObject.mockReturnValue({});
-
- createMountedWrapper();
-
- await nextTick();
- expect(findDateTimePicker().props('value')).toEqual(defaultTimeRange);
-
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- expect.any(Object),
- );
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
-
- it('passes a fixed time range in the URL to the time range picker', async () => {
- const params = {
- start: '2019-01-01T00:00:00.000Z',
- end: '2019-01-10T00:00:00.000Z',
- };
-
- queryToObject.mockReturnValue(params);
-
- createMountedWrapper();
-
- await nextTick();
- expect(findDateTimePicker().props('value')).toEqual(params);
-
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setTimeRange', params);
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
-
- it('passes a rolling time range in the URL to the time range picker', async () => {
- queryToObject.mockReturnValue({
- duration_seconds: '120',
- });
-
- createMountedWrapper();
-
- await nextTick();
- const expectedTimeRange = {
- duration: { seconds: 60 * 2 },
- };
-
- expect(findDateTimePicker().props('value')).toMatchObject(expectedTimeRange);
-
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- expectedTimeRange,
- );
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
-
- it('shows an error message and loads a default time range if invalid url parameters are passed', async () => {
- queryToObject.mockReturnValue({
- start: '<script>alert("XSS")</script>',
- end: '<script>alert("XSS")</script>',
- });
-
- createMountedWrapper();
-
- await nextTick();
- expect(createAlert).toHaveBeenCalled();
-
- expect(findDateTimePicker().props('value')).toEqual(defaultTimeRange);
-
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/setTimeRange',
- defaultTimeRange,
- );
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
- });
-
- it('redirects to different time range', async () => {
- const toUrl = `${mockProjectDir}/-/metrics?environment=1`;
- removeParams.mockReturnValueOnce(toUrl);
-
- createMountedWrapper();
-
- await nextTick();
- findDateTimePicker().vm.$emit('input', {
- duration: { seconds: 120 },
- });
-
- // redirect to with new parameters
- expect(mergeUrlParams).toHaveBeenCalledWith({ duration_seconds: '120' }, toUrl);
- expect(redirectTo).toHaveBeenCalledTimes(1); // eslint-disable-line import/no-deprecated
- });
-
- it('changes the url when a panel moves the time slider', async () => {
- const timeRange = {
- start: '2020-01-01T00:00:00.000Z',
- end: '2020-01-01T01:00:00.000Z',
- };
-
- queryToObject.mockReturnValue(timeRange);
-
- createMountedWrapper();
-
- await nextTick();
- wrapper.vm.onTimeRangeZoom(timeRange);
-
- expect(updateHistory).toHaveBeenCalled();
- expect(wrapper.vm.selectedTimeRange.start.toString()).toBe(timeRange.start);
- expect(wrapper.vm.selectedTimeRange.end.toString()).toBe(timeRange.end);
- });
-});
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
deleted file mode 100644
index 3ccaa2d28ac..00000000000
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ /dev/null
@@ -1,170 +0,0 @@
-import { GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-
-import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
-
-import { dashboardGitResponse } from '../mock_data';
-
-const defaultBranch = 'main';
-const starredDashboards = dashboardGitResponse.filter(({ starred }) => starred);
-const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starred);
-
-describe('DashboardsDropdown', () => {
- let wrapper;
- let mockDashboards;
- let mockSelectedDashboard;
-
- function createComponent(props, opts = {}) {
- const storeOpts = {
- computed: {
- allDashboards: () => mockDashboards,
- selectedDashboard: () => mockSelectedDashboard,
- },
- };
-
- wrapper = shallowMount(DashboardsDropdown, {
- propsData: {
- ...props,
- defaultBranch,
- },
- ...storeOpts,
- ...opts,
- });
- }
-
- const findItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findItemAt = (i) => wrapper.findAllComponents(GlDropdownItem).at(i);
- const findSearchInput = () => wrapper.findComponent({ ref: 'monitorDashboardsDropdownSearch' });
- const findNoItemsMsg = () => wrapper.findComponent({ ref: 'monitorDashboardsDropdownMsg' });
- const findStarredListDivider = () => wrapper.findComponent({ ref: 'starredListDivider' });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- const setSearchTerm = (searchTerm) => wrapper.setData({ searchTerm });
-
- beforeEach(() => {
- mockDashboards = dashboardGitResponse;
- mockSelectedDashboard = null;
- });
-
- describe('when it receives dashboards data', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('displays an item for each dashboard', () => {
- expect(findItems().length).toEqual(dashboardGitResponse.length);
- });
-
- it('displays items with the dashboard display name, with starred dashboards first', () => {
- expect(findItemAt(0).text()).toBe(starredDashboards[0].display_name);
- expect(findItemAt(1).text()).toBe(notStarredDashboards[0].display_name);
- expect(findItemAt(2).text()).toBe(notStarredDashboards[1].display_name);
- });
-
- it('displays separator between starred and not starred dashboards', () => {
- expect(findStarredListDivider().exists()).toBe(true);
- });
-
- it('displays a search input', () => {
- expect(findSearchInput().isVisible()).toBe(true);
- });
-
- it('hides no message text by default', () => {
- expect(findNoItemsMsg().isVisible()).toBe(false);
- });
-
- it('filters dropdown items when searched for item exists in the list', async () => {
- const searchTerm = 'Overview';
- setSearchTerm(searchTerm);
- await nextTick();
-
- expect(findItems()).toHaveLength(1);
- });
-
- it('shows no items found message when searched for item does not exists in the list', async () => {
- const searchTerm = 'does-not-exist';
- setSearchTerm(searchTerm);
- await nextTick();
-
- expect(findNoItemsMsg().isVisible()).toBe(true);
- });
- });
-
- describe('when a dashboard is selected', () => {
- beforeEach(() => {
- [mockSelectedDashboard] = starredDashboards;
- createComponent();
- });
-
- it('dashboard item is selected', () => {
- expect(findItemAt(0).props('isChecked')).toBe(true);
- expect(findItemAt(1).props('isChecked')).toBe(false);
- });
- });
-
- describe('when the dashboard is missing a display name', () => {
- beforeEach(() => {
- mockDashboards = dashboardGitResponse.map((d) => ({ ...d, display_name: undefined }));
- createComponent();
- });
-
- it('displays items with the dashboard path, with starred dashboards first', () => {
- expect(findItemAt(0).text()).toBe(starredDashboards[0].path);
- expect(findItemAt(1).text()).toBe(notStarredDashboards[0].path);
- expect(findItemAt(2).text()).toBe(notStarredDashboards[1].path);
- });
- });
-
- describe('when it receives starred dashboards', () => {
- beforeEach(() => {
- mockDashboards = starredDashboards;
- createComponent();
- });
-
- it('displays an item for each dashboard', () => {
- expect(findItems().length).toEqual(starredDashboards.length);
- });
-
- it('displays a star icon', () => {
- const star = findItemAt(0).findComponent(GlIcon);
- expect(star.exists()).toBe(true);
- expect(star.attributes('name')).toBe('star');
- });
-
- it('displays no separator between starred and not starred dashboards', () => {
- expect(findStarredListDivider().exists()).toBe(false);
- });
- });
-
- describe('when it receives only not-starred dashboards', () => {
- beforeEach(() => {
- mockDashboards = notStarredDashboards;
- createComponent();
- });
-
- it('displays an item for each dashboard', () => {
- expect(findItems().length).toEqual(notStarredDashboards.length);
- });
-
- it('displays no star icon', () => {
- const star = findItemAt(0).findComponent(GlIcon);
- expect(star.exists()).toBe(false);
- });
-
- it('displays no separator between starred and not starred dashboards', () => {
- expect(findStarredListDivider().exists()).toBe(false);
- });
- });
-
- describe('when a dashboard gets selected by the user', () => {
- beforeEach(() => {
- createComponent();
- findItemAt(1).vm.$emit('click');
- });
-
- 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
deleted file mode 100644
index b54ca926dae..00000000000
--- a/spec/frontend/monitoring/components/duplicate_dashboard_form_spec.js
+++ /dev/null
@@ -1,166 +0,0 @@
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
-
-import { dashboardGitResponse } from '../mock_data';
-
-let wrapper;
-
-const createMountedWrapper = (props = {}) => {
- // Use `mount` to render native input elements
- wrapper = mount(DuplicateDashboardForm, {
- propsData: { ...props },
- // We need to attach to document, so that `document.activeElement` is properly set in jsdom
- attachTo: document.body,
- });
-};
-
-describe('DuplicateDashboardForm', () => {
- const defaultBranch = 'main';
-
- const findByRef = (ref) => wrapper.findComponent({ ref });
- const setValue = (ref, val) => {
- findByRef(ref).setValue(val);
- };
- const setChecked = (value) => {
- const input = wrapper.find(`.custom-control-input[value="${value}"]`);
- input.element.checked = true;
- input.trigger('click');
- input.trigger('change');
- };
-
- beforeEach(() => {
- createMountedWrapper({ dashboard: dashboardGitResponse[0], defaultBranch });
- });
-
- it('renders correctly', () => {
- expect(wrapper.exists()).toEqual(true);
- });
-
- it('renders form elements', () => {
- expect(findByRef('fileName').exists()).toEqual(true);
- expect(findByRef('branchName').exists()).toEqual(true);
- expect(findByRef('branchOption').exists()).toEqual(true);
- expect(findByRef('commitMessage').exists()).toEqual(true);
- });
-
- describe('validates the file name', () => {
- const findInvalidFeedback = () => findByRef('fileNameFormGroup').find('.invalid-feedback');
-
- it('when is empty', async () => {
- setValue('fileName', '');
- await nextTick();
-
- expect(findByRef('fileNameFormGroup').classes()).toContain('is-valid');
- expect(findInvalidFeedback().exists()).toBe(false);
- });
-
- it('when is valid', async () => {
- setValue('fileName', 'my_dashboard.yml');
- await nextTick();
-
- expect(findByRef('fileNameFormGroup').classes()).toContain('is-valid');
- expect(findInvalidFeedback().exists()).toBe(false);
- });
-
- it('when is not valid', async () => {
- setValue('fileName', 'my_dashboard.exe');
- await nextTick();
-
- expect(findByRef('fileNameFormGroup').classes()).toContain('is-invalid');
- expect(findInvalidFeedback().text()).toBe('The file name should have a .yml extension');
- });
- });
-
- describe('emits `change` event', () => {
- const lastChange = () =>
- nextTick().then(() => {
- wrapper.find('form').trigger('change');
-
- // Resolves to the last emitted change
- const changes = wrapper.emitted().change;
- return changes[changes.length - 1][0];
- });
-
- it('with the inital form values', () => {
- expect(wrapper.emitted().change).toHaveLength(1);
-
- return expect(lastChange()).resolves.toEqual({
- branch: '',
- commitMessage: expect.any(String),
- dashboard: dashboardGitResponse[0].path,
- fileName: 'common_metrics.yml',
- });
- });
-
- it('containing an inputted file name', () => {
- setValue('fileName', 'my_dashboard.yml');
-
- return expect(lastChange()).resolves.toMatchObject({
- fileName: 'my_dashboard.yml',
- });
- });
-
- it('containing a default commit message when no message is set', () => {
- setValue('commitMessage', '');
-
- return expect(lastChange()).resolves.toMatchObject({
- commitMessage: expect.stringContaining('Create custom dashboard'),
- });
- });
-
- it('containing an inputted commit message', () => {
- setValue('commitMessage', 'My commit message');
-
- return expect(lastChange()).resolves.toMatchObject({
- commitMessage: expect.stringContaining('My commit message'),
- });
- });
-
- it('containing an inputted branch name', () => {
- setValue('branchName', 'a-new-branch');
-
- return expect(lastChange()).resolves.toMatchObject({
- branch: 'a-new-branch',
- });
- });
-
- it('when a `default` branch option is set, branch input is invisible and ignored', () => {
- setChecked(wrapper.vm.$options.radioVals.DEFAULT);
- setValue('branchName', 'a-new-branch');
-
- return Promise.all([
- expect(lastChange()).resolves.toMatchObject({
- branch: defaultBranch,
- }),
- nextTick(() => {
- expect(findByRef('branchName').isVisible()).toBe(false);
- }),
- ]);
- });
-
- it('when `new` branch option is chosen, focuses on the branch name input', async () => {
- setChecked(wrapper.vm.$options.radioVals.NEW);
-
- await nextTick();
-
- wrapper.find('form').trigger('change');
- expect(document.activeElement).toBe(findByRef('branchName').element);
- });
- });
-});
-
-describe('DuplicateDashboardForm escapes elements', () => {
- const branchToEscape = "<img/src='x'onerror=alert(document.domain)>";
-
- beforeEach(() => {
- createMountedWrapper({ dashboard: dashboardGitResponse[0], defaultBranch: branchToEscape });
- });
-
- it('should escape branch name data', () => {
- const branchOptionHtml = wrapper.vm.branchOptions[0].html;
- const escapedBranch = '&lt;img/src=&#39;x&#39;onerror=alert(document.domain)&gt';
-
- expect(branchOptionHtml).toEqual(expect.stringContaining(escapedBranch));
- });
-});
diff --git a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js b/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
deleted file mode 100644
index d83a9192876..00000000000
--- a/spec/frontend/monitoring/components/duplicate_dashboard_modal_spec.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import { GlAlert, GlLoadingIcon, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-
-import waitForPromises from 'helpers/wait_for_promises';
-
-import DuplicateDashboardForm from '~/monitoring/components/duplicate_dashboard_form.vue';
-import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue';
-
-import { dashboardGitResponse } from '../mock_data';
-
-Vue.use(Vuex);
-
-describe('duplicate dashboard modal', () => {
- let wrapper;
- let mockDashboards;
- let mockSelectedDashboard;
- let duplicateDashboardAction;
- let okEvent;
-
- function createComponent() {
- const store = new Vuex.Store({
- modules: {
- monitoringDashboard: {
- namespaced: true,
- actions: {
- duplicateSystemDashboard: duplicateDashboardAction,
- },
- getters: {
- allDashboards: () => mockDashboards,
- selectedDashboard: () => mockSelectedDashboard,
- },
- },
- },
- });
-
- return shallowMount(DuplicateDashboardModal, {
- propsData: {
- defaultBranch: 'main',
- modalId: 'id',
- },
- store,
- });
- }
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findModal = () => wrapper.findComponent(GlModal);
- const findDuplicateDashboardForm = () => wrapper.findComponent(DuplicateDashboardForm);
-
- beforeEach(() => {
- mockDashboards = dashboardGitResponse;
- [mockSelectedDashboard] = dashboardGitResponse;
-
- duplicateDashboardAction = jest.fn().mockResolvedValue();
-
- okEvent = {
- preventDefault: jest.fn(),
- };
-
- wrapper = createComponent();
-
- wrapper.vm.$refs.duplicateDashboardModal.hide = jest.fn();
- });
-
- it('contains a form to duplicate a dashboard', () => {
- expect(findDuplicateDashboardForm().exists()).toBe(true);
- });
-
- it('saves a new dashboard', async () => {
- findModal().vm.$emit('ok', okEvent);
-
- await waitForPromises();
- expect(okEvent.preventDefault).toHaveBeenCalled();
- 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();
- expect(findAlert().exists()).toBe(false);
- });
-
- it('handles error when a new dashboard is not saved', async () => {
- const errMsg = 'An error occurred';
-
- duplicateDashboardAction.mockRejectedValueOnce(errMsg);
- findModal().vm.$emit('ok', okEvent);
-
- await waitForPromises();
-
- expect(okEvent.preventDefault).toHaveBeenCalled();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(errMsg);
-
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.vm.$refs.duplicateDashboardModal.hide).not.toHaveBeenCalled();
- });
-
- it('updates the form on changes', () => {
- const formVals = {
- dashboard: 'common_metrics.yml',
- commitMessage: 'A commit message',
- };
-
- findModal().findComponent(DuplicateDashboardForm).vm.$emit('change', formVals);
-
- // Binding's second argument contains the modal id
- expect(wrapper.vm.form).toEqual(formVals);
- });
-});
diff --git a/spec/frontend/monitoring/components/embeds/embed_group_spec.js b/spec/frontend/monitoring/components/embeds/embed_group_spec.js
deleted file mode 100644
index beb698c838f..00000000000
--- a/spec/frontend/monitoring/components/embeds/embed_group_spec.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import { GlButton, GlCard } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import Vuex from 'vuex';
-import { TEST_HOST } from 'helpers/test_constants';
-import EmbedGroup from '~/monitoring/components/embeds/embed_group.vue';
-import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
-import {
- addModuleAction,
- initialEmbedGroupState,
- singleEmbedProps,
- dashboardEmbedProps,
- multipleEmbedProps,
-} from './mock_data';
-
-Vue.use(Vuex);
-
-describe('Embed Group', () => {
- let wrapper;
- let store;
- const metricsWithDataGetter = jest.fn();
-
- function mountComponent({ urls = [TEST_HOST], shallow = true, stubs } = {}) {
- const mountMethod = shallow ? shallowMount : mount;
- wrapper = mountMethod(EmbedGroup, {
- store,
- propsData: {
- urls,
- },
- stubs,
- });
- }
-
- beforeEach(() => {
- store = new Vuex.Store({
- modules: {
- embedGroup: {
- namespaced: true,
- actions: { addModule: jest.fn() },
- getters: { metricsWithData: metricsWithDataGetter },
- state: initialEmbedGroupState,
- },
- },
- });
- store.registerModule = jest.fn();
- jest.spyOn(store, 'dispatch');
- });
-
- afterEach(() => {
- metricsWithDataGetter.mockReset();
- });
-
- describe('interactivity', () => {
- it('hides the component when no chart data is loaded', () => {
- metricsWithDataGetter.mockReturnValue([]);
- mountComponent();
-
- expect(wrapper.findComponent(GlCard).isVisible()).toBe(false);
- });
-
- it('shows the component when chart data is loaded', () => {
- metricsWithDataGetter.mockReturnValue([1]);
- mountComponent();
-
- expect(wrapper.findComponent(GlCard).isVisible()).toBe(true);
- });
-
- it('is expanded by default', () => {
- metricsWithDataGetter.mockReturnValue([1]);
- mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
-
- expect(wrapper.find('.gl-card-body').classes()).not.toContain('d-none');
- });
-
- it('collapses when clicked', async () => {
- metricsWithDataGetter.mockReturnValue([1]);
- mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
-
- wrapper.findComponent(GlButton).trigger('click');
-
- await nextTick();
- expect(wrapper.find('.gl-card-body').classes()).toContain('d-none');
- });
- });
-
- describe('single metrics', () => {
- beforeEach(() => {
- metricsWithDataGetter.mockReturnValue([1]);
- mountComponent();
- });
-
- it('renders an Embed component', () => {
- expect(wrapper.findComponent(MetricEmbed).exists()).toBe(true);
- });
-
- it('passes the correct props to the Embed component', () => {
- expect(wrapper.findComponent(MetricEmbed).props()).toEqual(singleEmbedProps());
- });
-
- it('adds the monitoring dashboard module', () => {
- expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/0');
- });
- });
-
- describe('dashboard metrics', () => {
- beforeEach(() => {
- metricsWithDataGetter.mockReturnValue([2]);
- mountComponent();
- });
-
- it('passes the correct props to the dashboard Embed component', () => {
- expect(wrapper.findComponent(MetricEmbed).props()).toEqual(dashboardEmbedProps());
- });
-
- it('adds the monitoring dashboard module', () => {
- expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/0');
- });
- });
-
- describe('multiple metrics', () => {
- beforeEach(() => {
- metricsWithDataGetter.mockReturnValue([1, 1]);
- mountComponent({ urls: [TEST_HOST, TEST_HOST] });
- });
-
- it('creates Embed components', () => {
- expect(wrapper.findAllComponents(MetricEmbed)).toHaveLength(2);
- });
-
- it('passes the correct props to the Embed components', () => {
- expect(wrapper.findAllComponents(MetricEmbed).wrappers.map((item) => item.props())).toEqual(
- multipleEmbedProps(),
- );
- });
-
- it('adds multiple monitoring dashboard modules', () => {
- expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/0');
- expect(store.dispatch).toHaveBeenCalledWith(addModuleAction, 'monitoringDashboard/1');
- });
- });
-
- describe('button text', () => {
- it('has a singular label when there is one embed', () => {
- metricsWithDataGetter.mockReturnValue([1]);
- mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
-
- expect(wrapper.findComponent(GlButton).text()).toBe('Hide chart');
- });
-
- it('has a plural label when there are multiple embeds', () => {
- metricsWithDataGetter.mockReturnValue([2]);
- mountComponent({ shallow: false, stubs: { MetricEmbed: true } });
-
- expect(wrapper.findComponent(GlButton).text()).toBe('Hide charts');
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
deleted file mode 100644
index db25d524592..00000000000
--- a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { setHTMLFixture } from 'helpers/fixtures';
-import { TEST_HOST } from 'helpers/test_constants';
-import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
-import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
-import { groups, initialState, metricsData, metricsWithData } from './mock_data';
-
-Vue.use(Vuex);
-
-describe('MetricEmbed', () => {
- let wrapper;
- let store;
- let actions;
- let metricsWithDataGetter;
-
- function mountComponent() {
- wrapper = shallowMount(MetricEmbed, {
- store,
- propsData: {
- dashboardUrl: TEST_HOST,
- },
- });
- }
-
- beforeEach(() => {
- setHTMLFixture('<div class="layout-page"></div>');
-
- actions = {
- setInitialState: jest.fn(),
- setShowErrorBanner: jest.fn(),
- setTimeRange: jest.fn(),
- fetchDashboard: jest.fn(),
- };
-
- metricsWithDataGetter = jest.fn();
-
- store = new Vuex.Store({
- modules: {
- monitoringDashboard: {
- namespaced: true,
- actions,
- getters: {
- metricsWithData: () => metricsWithDataGetter,
- },
- state: initialState,
- },
- },
- });
- });
-
- afterEach(() => {
- metricsWithDataGetter.mockClear();
- });
-
- describe('no metrics are available yet', () => {
- beforeEach(() => {
- mountComponent();
- });
-
- it('shows an empty state when no metrics are present', () => {
- expect(wrapper.find('.metrics-embed').exists()).toBe(true);
- expect(wrapper.findComponent(DashboardPanel).exists()).toBe(false);
- });
- });
-
- describe('metrics are available', () => {
- beforeEach(() => {
- store.state.monitoringDashboard.dashboard.panelGroups = groups;
- store.state.monitoringDashboard.dashboard.panelGroups[0].panels = metricsData;
-
- metricsWithDataGetter.mockReturnValue(metricsWithData);
-
- mountComponent();
- });
-
- it('calls actions to fetch data', () => {
- const expectedTimeRangePayload = expect.objectContaining({
- start: expect.any(String),
- end: expect.any(String),
- });
-
- expect(actions.setTimeRange).toHaveBeenCalledTimes(1);
- expect(actions.setTimeRange.mock.calls[0][1]).toEqual(expectedTimeRangePayload);
-
- expect(actions.fetchDashboard).toHaveBeenCalled();
- });
-
- it('shows a chart when metrics are present', () => {
- expect(wrapper.find('.metrics-embed').exists()).toBe(true);
- expect(wrapper.findComponent(DashboardPanel).exists()).toBe(true);
- expect(wrapper.findAllComponents(DashboardPanel).length).toBe(2);
- });
-
- it('includes groupId with dashboardUrl', () => {
- expect(wrapper.findComponent(DashboardPanel).props('groupId')).toBe(TEST_HOST);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/embeds/mock_data.js b/spec/frontend/monitoring/components/embeds/mock_data.js
deleted file mode 100644
index e32e1a08cdb..00000000000
--- a/spec/frontend/monitoring/components/embeds/mock_data.js
+++ /dev/null
@@ -1,86 +0,0 @@
-import { TEST_HOST } from 'helpers/test_constants';
-
-export const metricsWithData = ['15_metric_a', '16_metric_b'];
-
-export const groups = [
- {
- panels: [
- {
- title: 'Memory Usage (Total)',
- type: 'area-chart',
- y_label: 'Total Memory Used',
- metrics: null,
- },
- ],
- },
-];
-
-const result = [
- {
- values: [
- ['Mon', 1220],
- ['Tue', 932],
- ['Wed', 901],
- ['Thu', 934],
- ['Fri', 1290],
- ['Sat', 1330],
- ['Sun', 1320],
- ],
- },
-];
-
-export const metricsData = [
- {
- metrics: [
- {
- metricId: '15_metric_a',
- result,
- },
- ],
- },
- {
- metrics: [
- {
- metricId: '16_metric_b',
- result,
- },
- ],
- },
-];
-
-export const initialState = () => ({
- dashboard: {
- panel_groups: [],
- },
-});
-
-export const initialEmbedGroupState = () => ({
- modules: [],
-});
-
-export const singleEmbedProps = () => ({
- dashboardUrl: TEST_HOST,
- containerClass: 'col-lg-12',
- namespace: 'monitoringDashboard/0',
-});
-
-export const dashboardEmbedProps = () => ({
- dashboardUrl: TEST_HOST,
- containerClass: 'col-lg-6',
- namespace: 'monitoringDashboard/0',
-});
-
-export const multipleEmbedProps = () => [
- {
- dashboardUrl: TEST_HOST,
- containerClass: 'col-lg-6',
- namespace: 'monitoringDashboard/0',
- },
- {
- dashboardUrl: TEST_HOST,
- containerClass: 'col-lg-6',
- namespace: 'monitoringDashboard/1',
- },
-];
-
-export const addModuleAction = 'embedGroup/addModule';
diff --git a/spec/frontend/monitoring/components/empty_state_spec.js b/spec/frontend/monitoring/components/empty_state_spec.js
deleted file mode 100644
index ddefa8c5cd0..00000000000
--- a/spec/frontend/monitoring/components/empty_state_spec.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import EmptyState from '~/monitoring/components/empty_state.vue';
-import { dashboardEmptyStates } from '~/monitoring/constants';
-
-function createComponent(props) {
- return shallowMount(EmptyState, {
- propsData: {
- settingsPath: '/settingsPath',
- clustersPath: '/clustersPath',
- documentationPath: '/documentationPath',
- emptyGettingStartedSvgPath: '/path/to/getting-started.svg',
- emptyLoadingSvgPath: '/path/to/loading.svg',
- emptyNoDataSvgPath: '/path/to/no-data.svg',
- emptyNoDataSmallSvgPath: '/path/to/no-data-small.svg',
- emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
- ...props,
- },
- });
-}
-
-describe('EmptyState', () => {
- it('shows loading state with a loading icon', () => {
- const wrapper = createComponent({
- selectedState: dashboardEmptyStates.LOADING,
- });
-
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.findComponent(GlEmptyState).exists()).toBe(false);
- });
-
- it('shows gettingStarted state', () => {
- const wrapper = createComponent({
- selectedState: dashboardEmptyStates.GETTING_STARTED,
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('shows unableToConnect state', () => {
- const wrapper = createComponent({
- selectedState: dashboardEmptyStates.UNABLE_TO_CONNECT,
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('shows noData state', () => {
- const wrapper = createComponent({
- selectedState: dashboardEmptyStates.NO_DATA,
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-});
diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js
deleted file mode 100644
index 593d832f297..00000000000
--- a/spec/frontend/monitoring/components/graph_group_spec.js
+++ /dev/null
@@ -1,144 +0,0 @@
-import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import GraphGroup from '~/monitoring/components/graph_group.vue';
-
-describe('Graph group component', () => {
- let wrapper;
-
- const findGroup = () => wrapper.findComponent({ ref: 'graph-group' });
- const findContent = () => wrapper.findComponent({ ref: 'graph-group-content' });
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findCaretIcon = () => wrapper.findComponent(GlIcon);
- const findToggleButton = () => wrapper.find('[data-testid="group-toggle-button"]');
-
- const createComponent = (propsData) => {
- wrapper = shallowMount(GraphGroup, {
- propsData,
- });
- };
-
- describe('When group is not collapsed', () => {
- beforeEach(() => {
- createComponent({
- name: 'panel',
- collapseGroup: false,
- });
- });
-
- it('should not show a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
-
- it('should show the chevron-lg-down caret icon', () => {
- expect(findContent().isVisible()).toBe(true);
- expect(findCaretIcon().props('name')).toBe('chevron-lg-down');
- });
-
- it('should show the chevron-lg-right caret icon when the user collapses the group', async () => {
- findToggleButton().trigger('click');
-
- await nextTick();
- expect(findContent().isVisible()).toBe(false);
- expect(findCaretIcon().props('name')).toBe('chevron-lg-right');
- });
-
- it('should contain a tab index for the collapse button', () => {
- const groupToggle = findToggleButton();
-
- expect(groupToggle.attributes('tabindex')).toBeDefined();
- });
-
- it('should show the open the group when collapseGroup is set to true', async () => {
- wrapper.setProps({
- collapseGroup: true,
- });
-
- await nextTick();
- expect(findContent().isVisible()).toBe(true);
- expect(findCaretIcon().props('name')).toBe('chevron-lg-down');
- });
- });
-
- describe('When group is collapsed', () => {
- beforeEach(() => {
- createComponent({
- name: 'panel',
- collapseGroup: true,
- });
- });
-
- it('should show the chevron-lg-down caret icon when collapseGroup is true', () => {
- expect(findCaretIcon().props('name')).toBe('chevron-lg-right');
- });
-
- it('should show the chevron-lg-right caret icon when collapseGroup is false', async () => {
- findToggleButton().trigger('click');
-
- await nextTick();
- expect(findCaretIcon().props('name')).toBe('chevron-lg-down');
- });
-
- it('should call collapse the graph group content when enter is pressed on the caret icon', () => {
- const graphGroupContent = findContent();
- const button = findToggleButton();
-
- button.trigger('keyup.enter');
-
- expect(graphGroupContent.isVisible()).toBe(false);
- });
- });
-
- describe('When groups can not be collapsed', () => {
- beforeEach(() => {
- createComponent({
- name: 'panel',
- showPanels: false,
- collapseGroup: false,
- });
- });
-
- it('should not have a container when showPanels is false', () => {
- expect(findGroup().exists()).toBe(false);
- expect(findContent().exists()).toBe(true);
- });
- });
-
- describe('When group is loading', () => {
- beforeEach(() => {
- createComponent({
- name: 'panel',
- isLoading: true,
- });
- });
-
- it('should show a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- });
- });
-
- describe('When group does not show a panel heading', () => {
- beforeEach(() => {
- createComponent({
- name: 'panel',
- showPanels: false,
- collapseGroup: false,
- });
- });
-
- it('should collapse the panel content', () => {
- expect(findContent().isVisible()).toBe(true);
- expect(findCaretIcon().exists()).toBe(false);
- });
-
- it('should show the panel content when collapse is set to false', async () => {
- wrapper.setProps({
- collapseGroup: false,
- });
-
- await nextTick();
- expect(findContent().isVisible()).toBe(true);
- expect(findCaretIcon().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/group_empty_state_spec.js b/spec/frontend/monitoring/components/group_empty_state_spec.js
deleted file mode 100644
index d3a48be7939..00000000000
--- a/spec/frontend/monitoring/components/group_empty_state_spec.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import { GlEmptyState } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { stubComponent } from 'helpers/stub_component';
-import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
-import { metricStates } from '~/monitoring/constants';
-
-function createComponent(props) {
- return shallowMount(GroupEmptyState, {
- propsData: {
- ...props,
- documentationPath: '/path/to/docs',
- settingsPath: '/path/to/settings',
- svgPath: '/path/to/empty-group-illustration.svg',
- },
- stubs: {
- GlEmptyState: stubComponent(GlEmptyState, {
- template: '<div><slot name="description"></slot></div>',
- }),
- },
- });
-}
-
-describe('GroupEmptyState', () => {
- let wrapper;
-
- describe.each([
- metricStates.NO_DATA,
- metricStates.TIMEOUT,
- metricStates.CONNECTION_FAILED,
- metricStates.BAD_QUERY,
- metricStates.LOADING,
- metricStates.UNKNOWN_ERROR,
- 'FOO STATE', // does not fail with unknown states
- ])('given state %s', (selectedState) => {
- beforeEach(() => {
- wrapper = createComponent({ selectedState });
- });
-
- it('renders the slotted content', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('passes the expected props to GlEmptyState', () => {
- expect(wrapper.findComponent(GlEmptyState).props()).toMatchSnapshot();
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/links_section_spec.js b/spec/frontend/monitoring/components/links_section_spec.js
deleted file mode 100644
index 94938e7f459..00000000000
--- a/spec/frontend/monitoring/components/links_section_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { GlLink } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-
-import LinksSection from '~/monitoring/components/links_section.vue';
-import { createStore } from '~/monitoring/stores';
-
-describe('Links Section component', () => {
- let store;
- let wrapper;
-
- const createShallowWrapper = () => {
- wrapper = shallowMount(LinksSection, {
- store,
- });
- };
- const setState = (links) => {
- store.state.monitoringDashboard = {
- ...store.state.monitoringDashboard,
- emptyState: null,
- links,
- };
- };
- const findLinks = () => wrapper.findAllComponents(GlLink);
-
- beforeEach(() => {
- store = createStore();
- createShallowWrapper();
- });
-
- it('does not render a section if no links are present', async () => {
- setState();
-
- await nextTick();
-
- expect(findLinks().length).toBe(0);
- });
-
- it('renders a link inside a section', async () => {
- setState([
- {
- title: 'GitLab Website',
- url: 'https://gitlab.com',
- },
- ]);
-
- await nextTick();
- expect(findLinks()).toHaveLength(1);
- const firstLink = findLinks().at(0);
-
- expect(firstLink.attributes('href')).toBe('https://gitlab.com');
- expect(firstLink.text()).toBe('GitLab Website');
- });
-
- it('renders multiple links inside a section', async () => {
- const links = new Array(10)
- .fill(null)
- .map((_, i) => ({ title: `Title ${i}`, url: `https://gitlab.com/projects/${i}` }));
- setState(links);
-
- await nextTick();
- expect(findLinks()).toHaveLength(10);
- });
-});
diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js
deleted file mode 100644
index f6cc6789b1f..00000000000
--- a/spec/frontend/monitoring/components/refresh_button_spec.js
+++ /dev/null
@@ -1,139 +0,0 @@
-import { GlDropdown, GlDropdownItem, GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Visibility from 'visibilityjs';
-import { nextTick } from 'vue';
-import RefreshButton from '~/monitoring/components/refresh_button.vue';
-import { createStore } from '~/monitoring/stores';
-
-describe('RefreshButton', () => {
- let wrapper;
- let store;
- let dispatch;
- let documentHidden;
-
- const createWrapper = (options = {}) => {
- wrapper = shallowMount(RefreshButton, { store, ...options });
- };
-
- const findRefreshBtn = () => wrapper.findComponent(GlButton);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findOptions = () => findDropdown().findAllComponents(GlDropdownItem);
- const findOptionAt = (index) => findOptions().at(index);
-
- const expectFetchDataToHaveBeenCalledTimes = (times) => {
- const refreshCalls = dispatch.mock.calls.filter(([action, payload]) => {
- return action === 'monitoringDashboard/fetchDashboardData' && payload === undefined;
- });
- expect(refreshCalls).toHaveLength(times);
- };
-
- beforeEach(() => {
- store = createStore();
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- dispatch = store.dispatch;
-
- documentHidden = false;
- jest.spyOn(Visibility, 'hidden').mockImplementation(() => documentHidden);
-
- createWrapper();
- });
-
- afterEach(() => {
- dispatch.mockReset();
- // eslint-disable-next-line @gitlab/vtu-no-explicit-wrapper-destroy
- wrapper.destroy();
- });
-
- it('refreshes data when "refresh" is clicked', () => {
- findRefreshBtn().vm.$emit('click');
- expectFetchDataToHaveBeenCalledTimes(1);
- });
-
- it('refresh rate is "Off" in the dropdown', () => {
- expect(findDropdown().props('text')).toBe('Off');
- });
-
- describe('refresh rate options', () => {
- it('presents multiple options', () => {
- expect(findOptions().length).toBeGreaterThan(1);
- });
-
- it('presents an "Off" option as the default option', () => {
- expect(findOptionAt(0).text()).toBe('Off');
- expect(findOptionAt(0).props('isChecked')).toBe(true);
- });
- });
-
- describe('when a refresh rate is chosen', () => {
- const optIndex = 2; // Other option than "Off"
-
- beforeEach(async () => {
- findOptionAt(optIndex).vm.$emit('click');
- await nextTick();
- });
-
- it('refresh rate appears in the dropdown', () => {
- expect(findDropdown().props('text')).toBe('10s');
- });
-
- it('refresh rate option is checked', () => {
- expect(findOptionAt(0).props('isChecked')).toBe(false);
- expect(findOptionAt(optIndex).props('isChecked')).toBe(true);
- });
-
- it('refreshes data when a new refresh rate is chosen', () => {
- expectFetchDataToHaveBeenCalledTimes(1);
- });
-
- it('refreshes data after two intervals of time have passed', async () => {
- jest.runOnlyPendingTimers();
- expectFetchDataToHaveBeenCalledTimes(2);
-
- await nextTick();
-
- jest.runOnlyPendingTimers();
- expectFetchDataToHaveBeenCalledTimes(3);
- });
-
- it('does not refresh data if the document is hidden', async () => {
- documentHidden = true;
-
- jest.runOnlyPendingTimers();
- expectFetchDataToHaveBeenCalledTimes(1);
-
- await nextTick();
-
- jest.runOnlyPendingTimers();
- expectFetchDataToHaveBeenCalledTimes(1);
- });
-
- it('data is not refreshed anymore after component is destroyed', () => {
- expect(jest.getTimerCount()).toBe(1);
-
- wrapper.destroy();
-
- expect(jest.getTimerCount()).toBe(0);
- });
-
- describe('when "Off" refresh rate is chosen', () => {
- beforeEach(async () => {
- findOptionAt(0).vm.$emit('click');
- await nextTick();
- });
-
- it('refresh rate is "Off" in the dropdown', () => {
- expect(findDropdown().props('text')).toBe('Off');
- });
-
- it('refresh rate option is appears selected', () => {
- expect(findOptionAt(0).props('isChecked')).toBe(true);
- expect(findOptionAt(optIndex).props('isChecked')).toBe(false);
- });
-
- it('stops refreshing data', () => {
- jest.runOnlyPendingTimers();
- expectFetchDataToHaveBeenCalledTimes(1);
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
deleted file mode 100644
index e6c5569fa19..00000000000
--- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
-
-describe('Custom variable component', () => {
- let wrapper;
-
- const defaultProps = {
- name: 'env',
- label: 'Select environment',
- value: 'Production',
- options: {
- values: [
- { text: 'Production', value: 'prod' },
- { text: 'Canary', value: 'canary' },
- ],
- },
- };
-
- const createShallowWrapper = (props) => {
- wrapper = shallowMount(DropdownField, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- });
- };
-
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
-
- it('renders dropdown element when all necessary props are passed', () => {
- createShallowWrapper();
-
- expect(findDropdown().exists()).toBe(true);
- });
-
- it('renders dropdown element with a text', () => {
- createShallowWrapper();
-
- expect(findDropdown().attributes('text')).toBe(defaultProps.value);
- });
-
- it('renders all the dropdown items', () => {
- createShallowWrapper();
-
- expect(findDropdownItems()).toHaveLength(defaultProps.options.values.length);
- });
-
- it('renders dropdown when values are missing', () => {
- createShallowWrapper({ options: {} });
-
- expect(findDropdown().exists()).toBe(true);
- });
-
- it('changing dropdown items triggers update', () => {
- createShallowWrapper();
- findDropdownItems().at(1).vm.$emit('click');
-
- expect(wrapper.emitted('input')).toEqual([['canary']]);
- });
-});
diff --git a/spec/frontend/monitoring/components/variables/text_field_spec.js b/spec/frontend/monitoring/components/variables/text_field_spec.js
deleted file mode 100644
index 20e1937c5ac..00000000000
--- a/spec/frontend/monitoring/components/variables/text_field_spec.js
+++ /dev/null
@@ -1,55 +0,0 @@
-import { GlFormInput } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import TextField from '~/monitoring/components/variables/text_field.vue';
-
-describe('Text variable component', () => {
- let wrapper;
- const propsData = {
- name: 'pod',
- label: 'Select pod',
- value: 'test-pod',
- };
- const createShallowWrapper = () => {
- wrapper = shallowMount(TextField, {
- propsData,
- });
- };
-
- const findInput = () => wrapper.findComponent(GlFormInput);
-
- it('renders a text input when all props are passed', () => {
- createShallowWrapper();
-
- expect(findInput().exists()).toBe(true);
- });
-
- it('always has a default value', async () => {
- createShallowWrapper();
-
- await nextTick();
- expect(findInput().attributes('value')).toBe(propsData.value);
- });
-
- it('triggers keyup enter', async () => {
- createShallowWrapper();
-
- findInput().element.value = 'prod-pod';
- findInput().trigger('input');
- findInput().trigger('keyup.enter');
-
- await nextTick();
- expect(wrapper.emitted('input')).toEqual([['prod-pod']]);
- });
-
- it('triggers blur enter', async () => {
- createShallowWrapper();
-
- findInput().element.value = 'canary-pod';
- findInput().trigger('input');
- findInput().trigger('blur');
-
- await nextTick();
- expect(wrapper.emitted('input')).toEqual([['canary-pod']]);
- });
-});
diff --git a/spec/frontend/monitoring/components/variables_section_spec.js b/spec/frontend/monitoring/components/variables_section_spec.js
deleted file mode 100644
index d6f8aac99aa..00000000000
--- a/spec/frontend/monitoring/components/variables_section_spec.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vuex from 'vuex';
-import { nextTick } from 'vue';
-import { updateHistory, mergeUrlParams } from '~/lib/utils/url_utility';
-import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
-import TextField from '~/monitoring/components/variables/text_field.vue';
-import VariablesSection from '~/monitoring/components/variables_section.vue';
-import { createStore } from '~/monitoring/stores';
-import { convertVariablesForURL } from '~/monitoring/utils';
-import { storeVariables } from '../mock_data';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- updateHistory: jest.fn(),
- mergeUrlParams: jest.fn(),
-}));
-
-describe('Metrics dashboard/variables section component', () => {
- let store;
- let wrapper;
-
- const createShallowWrapper = () => {
- wrapper = shallowMount(VariablesSection, {
- store,
- });
- };
-
- const findTextInputs = () => wrapper.findAllComponents(TextField);
- const findCustomInputs = () => wrapper.findAllComponents(DropdownField);
-
- beforeEach(() => {
- store = createStore();
-
- store.state.monitoringDashboard.emptyState = null;
- });
-
- it('does not show the variables section', () => {
- createShallowWrapper();
- const allInputs = findTextInputs().length + findCustomInputs().length;
-
- expect(allInputs).toBe(0);
- });
-
- describe('when variables are set', () => {
- beforeEach(async () => {
- store.state.monitoringDashboard.variables = storeVariables;
- createShallowWrapper();
-
- await nextTick();
- });
-
- it('shows the variables section', () => {
- const allInputs = findTextInputs().length + findCustomInputs().length;
-
- expect(allInputs).toBe(storeVariables.length);
- });
-
- it('shows the right custom variable inputs', () => {
- const customInputs = findCustomInputs();
-
- expect(customInputs.at(0).props('name')).toBe('customSimple');
- expect(customInputs.at(1).props('name')).toBe('customAdvanced');
- });
- });
-
- describe('when changing the variable inputs', () => {
- const updateVariablesAndFetchData = jest.fn();
-
- beforeEach(() => {
- store = new Vuex.Store({
- modules: {
- monitoringDashboard: {
- namespaced: true,
- state: {
- emptyState: null,
- variables: storeVariables,
- },
- actions: {
- updateVariablesAndFetchData,
- },
- },
- },
- });
-
- createShallowWrapper();
- });
-
- it('merges the url params and refreshes the dashboard when a text-based variables inputs are updated', async () => {
- const firstInput = findTextInputs().at(0);
-
- firstInput.vm.$emit('input', 'test');
-
- await nextTick();
- expect(updateVariablesAndFetchData).toHaveBeenCalled();
- expect(mergeUrlParams).toHaveBeenCalledWith(
- convertVariablesForURL(storeVariables),
- window.location.href,
- );
- expect(updateHistory).toHaveBeenCalled();
- });
-
- it('merges the url params and refreshes the dashboard when a custom-based variables inputs are updated', async () => {
- const firstInput = findCustomInputs().at(0);
-
- firstInput.vm.$emit('input', 'test');
-
- await nextTick();
- expect(updateVariablesAndFetchData).toHaveBeenCalled();
- expect(mergeUrlParams).toHaveBeenCalledWith(
- convertVariablesForURL(storeVariables),
- window.location.href,
- );
- expect(updateHistory).toHaveBeenCalled();
- });
-
- it('does not merge the url params and refreshes the dashboard if the value entered is not different that is what currently stored', () => {
- const firstInput = findTextInputs().at(0);
-
- firstInput.vm.$emit('input', 'My default value');
-
- expect(updateVariablesAndFetchData).not.toHaveBeenCalled();
- expect(mergeUrlParams).not.toHaveBeenCalled();
- expect(updateHistory).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/monitoring/csv_export_spec.js b/spec/frontend/monitoring/csv_export_spec.js
deleted file mode 100644
index 42d19c21a7b..00000000000
--- a/spec/frontend/monitoring/csv_export_spec.js
+++ /dev/null
@@ -1,126 +0,0 @@
-import { graphDataToCsv } from '~/monitoring/csv_export';
-import { timeSeriesGraphData } from './graph_data';
-
-describe('monitoring export_csv', () => {
- describe('graphDataToCsv', () => {
- const expectCsvToMatchLines = (csv, lines) => expect(`${lines.join('\r\n')}\r\n`).toEqual(csv);
-
- it('should return a csv with 0 metrics', () => {
- const data = timeSeriesGraphData({}, { metricCount: 0 });
-
- expect(graphDataToCsv(data)).toEqual('');
- });
-
- it('should return a csv with 1 metric with no data', () => {
- const data = timeSeriesGraphData({}, { metricCount: 1 });
-
- // When state is NO_DATA, result is null
- data.metrics[0].result = null;
-
- expect(graphDataToCsv(data)).toEqual('');
- });
-
- it('should return a csv with 1 metric', () => {
- const data = timeSeriesGraphData({}, { metricCount: 1 });
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > Metric 1"`,
- '2015-07-01T20:10:50.000Z,1',
- '2015-07-01T20:12:50.000Z,2',
- '2015-07-01T20:14:50.000Z,3',
- ]);
- });
-
- it('should return a csv with multiple metrics and one with no data', () => {
- const data = timeSeriesGraphData({}, { metricCount: 2 });
-
- // When state is NO_DATA, result is null
- data.metrics[0].result = null;
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > Metric 2"`,
- '2015-07-01T20:10:50.000Z,1',
- '2015-07-01T20:12:50.000Z,2',
- '2015-07-01T20:14:50.000Z,3',
- ]);
- });
-
- it('should return a csv when not all metrics have the same timestamps', () => {
- const data = timeSeriesGraphData({}, { metricCount: 3 });
-
- // Add an "odd" timestamp that is not in the dataset
- Object.assign(data.metrics[2].result[0], {
- value: ['2016-01-01T00:00:00.000Z', 9],
- values: [['2016-01-01T00:00:00.000Z', 9]],
- });
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > Metric 1","Y Axis > Metric 2","Y Axis > Metric 3"`,
- '2015-07-01T20:10:50.000Z,1,1,',
- '2015-07-01T20:12:50.000Z,2,2,',
- '2015-07-01T20:14:50.000Z,3,3,',
- '2016-01-01T00:00:00.000Z,,,9',
- ]);
- });
-
- it('should escape double quotes in metric labels with two double quotes ("")', () => {
- const data = timeSeriesGraphData({}, { metricCount: 1 });
-
- data.metrics[0].label = 'My "quoted" metric';
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > My ""quoted"" metric"`,
- '2015-07-01T20:10:50.000Z,1',
- '2015-07-01T20:12:50.000Z,2',
- '2015-07-01T20:14:50.000Z,3',
- ]);
- });
-
- it('should return a csv with multiple metrics', () => {
- const data = timeSeriesGraphData({}, { metricCount: 3 });
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > Metric 1","Y Axis > Metric 2","Y Axis > Metric 3"`,
- '2015-07-01T20:10:50.000Z,1,1,1',
- '2015-07-01T20:12:50.000Z,2,2,2',
- '2015-07-01T20:14:50.000Z,3,3,3',
- ]);
- });
-
- it('should return a csv with 1 metric and multiple series with labels', () => {
- const data = timeSeriesGraphData({}, { isMultiSeries: true });
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > Metric 1","Y Axis > Metric 1"`,
- '2015-07-01T20:10:50.000Z,1,4',
- '2015-07-01T20:12:50.000Z,2,5',
- '2015-07-01T20:14:50.000Z,3,6',
- ]);
- });
-
- it('should return a csv with 1 metric and multiple series', () => {
- const data = timeSeriesGraphData({}, { isMultiSeries: true, withLabels: false });
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091"`,
- '2015-07-01T20:10:50.000Z,1,4',
- '2015-07-01T20:12:50.000Z,2,5',
- '2015-07-01T20:14:50.000Z,3,6',
- ]);
- });
-
- it('should return a csv with multiple metrics and multiple series', () => {
- const data = timeSeriesGraphData(
- {},
- { metricCount: 3, isMultiSeries: true, withLabels: false },
- );
-
- expectCsvToMatchLines(graphDataToCsv(data), [
- `timestamp,"Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091","Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091","Y Axis > __name__: up, job: prometheus, instance: localhost:9090","Y Axis > __name__: up, job: node, instance: localhost:9091"`,
- '2015-07-01T20:10:50.000Z,1,4,1,4,1,4',
- '2015-07-01T20:12:50.000Z,2,5,2,5,2,5',
- '2015-07-01T20:14:50.000Z,3,6,3,6,3,6',
- ]);
- });
- });
-});
diff --git a/spec/frontend/monitoring/fixture_data.js b/spec/frontend/monitoring/fixture_data.js
deleted file mode 100644
index f4062adea81..00000000000
--- a/spec/frontend/monitoring/fixture_data.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import fixture from 'test_fixtures/metrics_dashboard/environment_metrics_dashboard.json';
-import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { metricStates } from '~/monitoring/constants';
-import { mapToDashboardViewModel } from '~/monitoring/stores/utils';
-import { stateAndPropsFromDataset } from '~/monitoring/utils';
-
-import { metricsResult } from './mock_data';
-
-export const metricsDashboardResponse = fixture;
-
-export const metricsDashboardPayload = metricsDashboardResponse.dashboard;
-
-const datasetState = stateAndPropsFromDataset(
- convertObjectPropsToCamelCase(metricsDashboardResponse.metrics_data),
-);
-
-// new properties like addDashboardDocumentationPath prop
-// was recently added to dashboard.vue component this needs to be
-// added to fixtures data
-// https://gitlab.com/gitlab-org/gitlab/-/issues/229256
-export const dashboardProps = {
- ...datasetState.dataProps,
-};
-
-export const metricsDashboardViewModel = mapToDashboardViewModel(metricsDashboardPayload);
-
-export const metricsDashboardPanelCount = 22;
-
-// Graph data
-
-const firstPanel = metricsDashboardViewModel.panelGroups[0].panels[0];
-
-export const graphData = {
- ...firstPanel,
- metrics: firstPanel.metrics.map((metric) => ({
- ...metric,
- result: metricsResult,
- state: metricStates.OK,
- })),
-};
-
-export const graphDataEmpty = {
- ...firstPanel,
- metrics: firstPanel.metrics.map((metric) => ({
- ...metric,
- result: [],
- state: metricStates.NO_DATA,
- })),
-};
diff --git a/spec/frontend/monitoring/graph_data.js b/spec/frontend/monitoring/graph_data.js
deleted file mode 100644
index 981955efebb..00000000000
--- a/spec/frontend/monitoring/graph_data.js
+++ /dev/null
@@ -1,274 +0,0 @@
-import { panelTypes, metricStates } from '~/monitoring/constants';
-import { mapPanelToViewModel, normalizeQueryResponseData } from '~/monitoring/stores/utils';
-
-const initTime = 1435781450; // "Wed, 01 Jul 2015 20:10:50 GMT"
-const intervalSeconds = 120;
-
-const makeValue = (val) => [initTime, val];
-const makeValues = (vals) => vals.map((val, i) => [initTime + intervalSeconds * i, val]);
-
-// Raw Promethues Responses
-
-export const prometheusMatrixMultiResult = ({
- values1 = ['1', '2', '3'],
- values2 = ['4', '5', '6'],
-} = {}) => ({
- resultType: 'matrix',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- values: makeValues(values1),
- },
- {
- metric: {
- __name__: 'up',
- job: 'node',
- instance: 'localhost:9091',
- },
- values: makeValues(values2),
- },
- ],
-});
-
-// Normalized Prometheus Responses
-
-const scalarResult = ({ value = '1' } = {}) =>
- normalizeQueryResponseData({
- resultType: 'scalar',
- result: makeValue(value),
- });
-
-const vectorResult = ({ value1 = '1', value2 = '2' } = {}) =>
- normalizeQueryResponseData({
- resultType: 'vector',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- value: makeValue(value1),
- },
- {
- metric: {
- __name__: 'up',
- job: 'node',
- instance: 'localhost:9100',
- },
- value: makeValue(value2),
- },
- ],
- });
-
-const matrixSingleResult = ({ values = ['1', '2', '3'] } = {}) =>
- normalizeQueryResponseData({
- resultType: 'matrix',
- result: [
- {
- metric: {},
- values: makeValues(values),
- },
- ],
- });
-
-const matrixMultiResult = ({ values1 = ['1', '2', '3'], values2 = ['4', '5', '6'] } = {}) =>
- normalizeQueryResponseData({
- resultType: 'matrix',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- values: makeValues(values1),
- },
- {
- metric: {
- __name__: 'up',
- job: 'node',
- instance: 'localhost:9091',
- },
- values: makeValues(values2),
- },
- ],
- });
-
-// GraphData factory
-
-/**
- * Generate mock graph data according to options
- *
- * @param {Object} panelOptions - Panel options as in YML.
- * @param {Object} dataOptions
- * @param {Object} dataOptions.metricCount
- * @param {Object} dataOptions.isMultiSeries
- */
-export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => {
- const { metricCount = 1, isMultiSeries = false, withLabels = true } = dataOptions;
-
- return mapPanelToViewModel({
- title: 'Time Series Panel',
- type: panelTypes.LINE_CHART,
- x_label: 'X Axis',
- y_label: 'Y Axis',
- metrics: Array.from(Array(metricCount), (_, i) => ({
- label: withLabels ? `Metric ${i + 1}` : undefined,
- state: metricStates.OK,
- result: isMultiSeries ? matrixMultiResult() : matrixSingleResult(),
- })),
- ...panelOptions,
- });
-};
-
-/**
- * Generate mock graph data according to options
- *
- * @param {Object} panelOptions - Panel options as in YML.
- * @param {Object} dataOptions
- * @param {Object} dataOptions.unit
- * @param {Object} dataOptions.value
- * @param {Object} dataOptions.isVector
- */
-export const singleStatGraphData = (panelOptions = {}, dataOptions = {}) => {
- const { unit, value = '1', isVector = false } = dataOptions;
-
- return mapPanelToViewModel({
- title: 'Single Stat Panel',
- type: panelTypes.SINGLE_STAT,
- metrics: [
- {
- label: 'Metric Label',
- state: metricStates.OK,
- result: isVector ? vectorResult({ value }) : scalarResult({ value }),
- unit,
- },
- ],
- ...panelOptions,
- });
-};
-
-/**
- * Generate mock graph data according to options
- *
- * @param {Object} panelOptions - Panel options as in YML.
- * @param {Object} dataOptions
- * @param {Array} dataOptions.values - Metric values
- * @param {Array} dataOptions.upper - Upper boundary values
- * @param {Array} dataOptions.lower - Lower boundary values
- */
-export const anomalyGraphData = (panelOptions = {}, dataOptions = {}) => {
- const { values, upper, lower } = dataOptions;
-
- return mapPanelToViewModel({
- title: 'Anomaly Panel',
- type: panelTypes.ANOMALY_CHART,
- x_label: 'X Axis',
- y_label: 'Y Axis',
- metrics: [
- {
- label: `Metric`,
- state: metricStates.OK,
- result: matrixSingleResult({ values }),
- },
- {
- label: `Upper boundary`,
- state: metricStates.OK,
- result: matrixSingleResult({ values: upper }),
- },
- {
- label: `Lower boundary`,
- state: metricStates.OK,
- result: matrixSingleResult({ values: lower }),
- },
- ],
- ...panelOptions,
- });
-};
-
-/**
- * Generate mock graph data for heatmaps according to options
- */
-export const heatmapGraphData = (panelOptions = {}, dataOptions = {}) => {
- const { metricCount = 1 } = dataOptions;
-
- return mapPanelToViewModel({
- title: 'Heatmap Panel',
- type: panelTypes.HEATMAP,
- x_label: 'X Axis',
- y_label: 'Y Axis',
- metrics: Array.from(Array(metricCount), (_, i) => ({
- label: `Metric ${i + 1}`,
- state: metricStates.OK,
- result: matrixMultiResult(),
- })),
- ...panelOptions,
- });
-};
-
-/**
- * Generate gauge chart mock graph data according to options
- *
- * @param {Object} panelOptions - Panel options as in YML.
- *
- */
-export const gaugeChartGraphData = (panelOptions = {}) => {
- const {
- minValue = 100,
- maxValue = 1000,
- split = 20,
- thresholds = {
- mode: 'absolute',
- values: [500, 800],
- },
- format = 'kilobytes',
- } = panelOptions;
-
- return mapPanelToViewModel({
- title: 'Gauge Chart Panel',
- type: panelTypes.GAUGE_CHART,
- min_value: minValue,
- max_value: maxValue,
- split,
- thresholds,
- format,
- metrics: [
- {
- label: `Metric`,
- state: metricStates.OK,
- result: matrixSingleResult(),
- },
- ],
- });
-};
-
-/**
- * Generates stacked mock graph data according to options
- *
- * @param {Object} panelOptions - Panel options as in YML.
- * @param {Object} dataOptions
- */
-export const stackedColumnGraphData = (panelOptions = {}, dataOptions = {}) => {
- return {
- ...timeSeriesGraphData(panelOptions, dataOptions),
- type: panelTypes.STACKED_COLUMN,
- };
-};
-
-/**
- * Generates bar mock graph data according to options
- *
- * @param {Object} panelOptions - Panel options as in YML.
- * @param {Object} dataOptions
- */
-export const barGraphData = (panelOptions = {}, dataOptions = {}) => {
- return {
- ...timeSeriesGraphData(panelOptions, dataOptions),
- type: panelTypes.BAR,
- };
-};
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
deleted file mode 100644
index 1d23190e586..00000000000
--- a/spec/frontend/monitoring/mock_data.js
+++ /dev/null
@@ -1,574 +0,0 @@
-// The path below needs to be relative because we import the mock-data to karma
-import invalidUrl from '~/lib/utils/invalid_url';
-import { TEST_HOST } from '../__helpers__/test_constants';
-// This import path needs to be relative for now because this mock data is used in
-// Karma specs too, where the helpers/test_constants alias can not be resolved
-
-export const mockProjectDir = '/frontend-fixtures/environments-project';
-export const mockApiEndpoint = `${TEST_HOST}/monitoring/mock`;
-
-export const customDashboardBasePath = '.gitlab/dashboards';
-
-const customDashboardsData = new Array(30).fill(null).map((_, idx) => ({
- default: false,
- display_name: `Custom Dashboard ${idx}`,
- can_edit: true,
- system_dashboard: false,
- out_of_the_box_dashboard: false,
- project_blob_path: `${mockProjectDir}/blob/main/dashboards/.gitlab/dashboards/dashboard_${idx}.yml`,
- path: `.gitlab/dashboards/dashboard_${idx}.yml`,
- starred: false,
-}));
-
-export const mockDashboardsErrorResponse = {
- all_dashboards: customDashboardsData,
- message: "Each 'panel_group' must define an array :panels",
- status: 'error',
-};
-
-export const anomalyDeploymentData = [
- {
- id: 111,
- iid: 3,
- sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- ref: {
- name: 'main',
- },
- created_at: '2019-08-19T22:00:00.000Z',
- deployed_at: '2019-08-19T22:01:00.000Z',
- tag: false,
- 'last?': true,
- },
- {
- id: 110,
- iid: 2,
- sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- ref: {
- name: 'main',
- },
- created_at: '2019-08-19T23:00:00.000Z',
- deployed_at: '2019-08-19T23:00:00.000Z',
- tag: false,
- 'last?': false,
- },
-];
-
-export const deploymentData = [
- {
- id: 111,
- iid: 3,
- sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- commitUrl:
- 'http://test.host/frontend-fixtures/environments-project/-/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- ref: {
- name: 'main',
- },
- created_at: '2019-07-16T10:14:25.589Z',
- tag: false,
- tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
- 'last?': true,
- },
- {
- id: 110,
- iid: 2,
- sha: 'f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- commitUrl:
- 'http://test.host/frontend-fixtures/environments-project/-/commit/f5bcd1d9dac6fa4137e2510b9ccd134ef2e84187',
- ref: {
- name: 'main',
- },
- created_at: '2019-07-16T11:14:25.589Z',
- tag: false,
- tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
- 'last?': false,
- },
- {
- id: 109,
- iid: 1,
- sha: '6511e58faafaa7ad2228990ec57f19d66f7db7c2',
- commitUrl:
- 'http://test.host/frontend-fixtures/environments-project/-/commit/6511e58faafaa7ad2228990ec57f19d66f7db7c2',
- ref: {
- name: 'update2-readme',
- },
- created_at: '2019-07-16T12:14:25.589Z',
- tag: false,
- tagUrl: 'http://test.host/frontend-fixtures/environments-project/tags/false',
- 'last?': false,
- },
-];
-
-export const annotationsData = [
- {
- id: 'gid://gitlab/Metrics::Dashboard::Annotation/1',
- startingAt: '2020-04-12 12:51:53 UTC',
- endingAt: null,
- panelId: null,
- description: 'This is a test annotation',
- },
- {
- id: 'gid://gitlab/Metrics::Dashboard::Annotation/2',
- description: 'test annotation 2',
- startingAt: '2020-04-13 12:51:53 UTC',
- endingAt: null,
- panelId: null,
- },
- {
- id: 'gid://gitlab/Metrics::Dashboard::Annotation/3',
- description: 'test annotation 3',
- startingAt: '2020-04-16 12:51:53 UTC',
- endingAt: null,
- panelId: null,
- },
-];
-
-const extraEnvironmentData = new Array(15).fill(null).map((_, idx) => ({
- id: `gid://gitlab/Environments/${150 + idx}`,
- name: `no-deployment/noop-branch-${idx}`,
- state: 'available',
- created_at: '2018-07-04T18:39:41.702Z',
- updated_at: '2018-07-04T18:44:54.010Z',
-}));
-
-export const environmentData = [
- {
- id: 'gid://gitlab/Environments/34',
- name: 'production',
- state: 'available',
- external_url: 'http://root-autodevops-deploy.my-fake-domain.com',
- environment_type: null,
- stop_action: false,
- metrics_path: '/root/hello-prometheus/environments/34/metrics',
- environment_path: '/root/hello-prometheus/environments/34',
- stop_path: '/root/hello-prometheus/environments/34/stop',
- terminal_path: '/root/hello-prometheus/environments/34/terminal',
- folder_path: '/root/hello-prometheus/environments/folders/production',
- created_at: '2018-06-29T16:53:38.301Z',
- updated_at: '2018-06-29T16:57:09.825Z',
- last_deployment: {
- id: 127,
- },
- },
- {
- id: 'gid://gitlab/Environments/35',
- name: 'review/noop-branch',
- state: 'available',
- external_url: 'http://root-autodevops-deploy-review-noop-branc-die93w.my-fake-domain.com',
- environment_type: 'review',
- stop_action: true,
- metrics_path: '/root/hello-prometheus/environments/35/metrics',
- environment_path: '/root/hello-prometheus/environments/35',
- stop_path: '/root/hello-prometheus/environments/35/stop',
- terminal_path: '/root/hello-prometheus/environments/35/terminal',
- folder_path: '/root/hello-prometheus/environments/folders/review',
- created_at: '2018-07-03T18:39:41.702Z',
- updated_at: '2018-07-03T18:44:54.010Z',
- last_deployment: {
- id: 128,
- },
- },
-].concat(extraEnvironmentData);
-
-export const dashboardGitResponse = [
- {
- default: true,
- display_name: 'Overview',
- can_edit: false,
- system_dashboard: true,
- out_of_the_box_dashboard: true,
- project_blob_path: null,
- path: 'config/prometheus/common_metrics.yml',
- starred: false,
- user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/common_metrics.yml`,
- },
- {
- default: false,
- display_name: 'dashboard.yml',
- can_edit: true,
- system_dashboard: false,
- out_of_the_box_dashboard: false,
- project_blob_path: `${mockProjectDir}/-/blob/main/.gitlab/dashboards/dashboard.yml`,
- path: '.gitlab/dashboards/dashboard.yml',
- starred: true,
- user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=.gitlab/dashboards/dashboard.yml`,
- },
- {
- default: false,
- display_name: 'Pod Health',
- can_edit: false,
- system_dashboard: false,
- out_of_the_box_dashboard: true,
- project_blob_path: null,
- path: 'config/prometheus/pod_metrics.yml',
- starred: false,
- user_starred_path: `${mockProjectDir}/metrics_user_starred_dashboards?dashboard_path=config/prometheus/pod_metrics.yml`,
- },
- ...customDashboardsData,
-];
-
-// Metrics mocks
-
-export const metricsResult = [
- {
- metric: {},
- values: [
- [1563272065.589, '10.396484375'],
- [1563272125.589, '10.333984375'],
- [1563272185.589, '10.333984375'],
- [1563272245.589, '10.333984375'],
- ],
- },
-];
-
-export const barMockData = {
- title: 'SLA Trends - Primary Services',
- type: 'bar',
- xLabel: 'service',
- y_label: 'percentile',
- metrics: [
- {
- id: 'sla_trends_primary_services',
- series_name: 'group 1',
- metricId: 'NO_DB_sla_trends_primary_services',
- query_range:
- 'avg(avg_over_time(slo_observation_status{environment="gprd", stage=~"main|", type=~"api|web|git|registry|sidekiq|ci-runners"}[1d])) by (type)',
- unit: 'Percentile',
- label: 'SLA',
- prometheus_endpoint_path:
- '/gitlab-com/metrics-dogfooding/-/environments/266/prometheus/api/v1/query_range?query=clamp_min%28clamp_max%28avg%28avg_over_time%28slo_observation_status%7Benvironment%3D%22gprd%22%2C+stage%3D~%22main%7C%22%2C+type%3D~%22api%7Cweb%7Cgit%7Cregistry%7Csidekiq%7Cci-runners%22%7D%5B1d%5D%29%29+by+%28type%29%2C1%29%2C0%29',
- result: [
- {
- metric: { type: 'api' },
- values: [[1583995208, '0.9935198135198128']],
- },
- {
- metric: { type: 'git' },
- values: [[1583995208, '0.9975296513504401']],
- },
- {
- metric: { type: 'registry' },
- values: [[1583995208, '0.9994716394716395']],
- },
- {
- metric: { type: 'sidekiq' },
- values: [[1583995208, '0.9948251748251747']],
- },
- {
- metric: { type: 'web' },
- values: [[1583995208, '0.9535664335664336']],
- },
- {
- metric: { type: 'postgresql_database' },
- values: [[1583995208, '0.9335664335664336']],
- },
- ],
- },
- ],
-};
-
-export const baseNamespace = 'monitoringDashboard';
-
-export const mockNamespace = `${baseNamespace}/1`;
-
-export const mockNamespaces = [`${baseNamespace}/1`, `${baseNamespace}/2`];
-
-export const mockTimeRange = { duration: { seconds: 120 } };
-
-export const mockFixedTimeRange = {
- start: '2020-06-17T19:59:08.659Z',
- end: '2020-07-17T19:59:08.659Z',
-};
-
-export const mockNamespacedData = {
- mockDeploymentData: ['mockDeploymentData'],
- mockProjectPath: '/mockProjectPath',
-};
-
-export const mockLogsPath = '/mockLogsPath';
-
-export const mockLogsHref = `${mockLogsPath}?duration_seconds=${mockTimeRange.duration.seconds}`;
-
-export const mockLinks = [
- {
- title: 'Job',
- url: 'http://intel.com/bibendum/felis/sed/interdum/venenatis.png',
- },
- {
- title: 'Solarbreeze',
- url: 'http://ebay.co.uk/primis/in/faucibus.jsp',
- },
- {
- title: 'Bentosanzap',
- url: 'http://cargocollective.com/sociis/natoque/penatibus/et/magnis/dis.js',
- },
- {
- title: 'Wrapsafe',
- url: 'https://bloomberg.com/tempus/vel/pede/morbi.aspx',
- },
- {
- title: 'Stronghold',
- url: 'https://networkadvertising.org/primis/in/faucibus/orci/luctus/et/ultrices.html',
- },
- {
- title: 'Lotstring',
- url:
- 'https://huffingtonpost.com/sapien/a/libero.aspx?et=lacus&ultrices=at&posuere=velit&cubilia=vivamus&curae=vel&duis=nulla&faucibus=eget&accumsan=eros&odio=elementum&curabitur=pellentesque&convallis=quisque&duis=porta&consequat=volutpat&dui=erat&nec=quisque&nisi=erat&volutpat=eros&eleifend=viverra&donec=eget&ut=congue&dolor=eget&morbi=semper&vel=rutrum&lectus=nulla&in=nunc&quam=purus&fringilla=phasellus&rhoncus=in&mauris=felis&enim=donec&leo=semper&rhoncus=sapien&sed=a&vestibulum=libero&sit=nam&amet=dui&cursus=proin&id=leo&turpis=odio&integer=porttitor&aliquet=id&massa=consequat&id=in&lobortis=consequat&convallis=ut&tortor=nulla&risus=sed&dapibus=accumsan&augue=felis&vel=ut&accumsan=at&tellus=dolor&nisi=quis&eu=odio',
- },
- {
- title: 'Cardify',
- url:
- 'http://nature.com/imperdiet/et/commodo/vulputate/justo/in/blandit.json?tempus=posuere&semper=felis&est=sed&quam=lacus&pharetra=morbi&magna=sem&ac=mauris&consequat=laoreet&metus=ut&sapien=rhoncus&ut=aliquet&nunc=pulvinar&vestibulum=sed&ante=nisl&ipsum=nunc&primis=rhoncus&in=dui&faucibus=vel&orci=sem&luctus=sed&et=sagittis&ultrices=nam&posuere=congue&cubilia=risus&curae=semper&mauris=porta&viverra=volutpat&diam=quam&vitae=pede&quam=lobortis&suspendisse=ligula&potenti=sit&nullam=amet&porttitor=eleifend&lacus=pede&at=libero&turpis=quis',
- },
- {
- title: 'Ventosanzap',
- url:
- 'http://stanford.edu/augue/vestibulum/ante/ipsum/primis/in/faucibus.xml?metus=morbi&sapien=quis&ut=tortor&nunc=id&vestibulum=nulla&ante=ultrices&ipsum=aliquet&primis=maecenas&in=leo&faucibus=odio&orci=condimentum&luctus=id&et=luctus&ultrices=nec&posuere=molestie&cubilia=sed&curae=justo&mauris=pellentesque&viverra=viverra&diam=pede&vitae=ac&quam=diam&suspendisse=cras&potenti=pellentesque&nullam=volutpat&porttitor=dui&lacus=maecenas&at=tristique&turpis=est&donec=et&posuere=tempus&metus=semper&vitae=est&ipsum=quam&aliquam=pharetra&non=magna&mauris=ac&morbi=consequat&non=metus',
- },
- {
- title: 'Cardguard',
- url:
- 'https://google.com.hk/lacinia/eget/tincidunt/eget/tempus/vel.js?at=eget&turpis=nunc&a=donec',
- },
- {
- title: 'Namfix',
- url:
- 'https://fotki.com/eget/rutrum/at/lorem.jsp?at=id&vulputate=nulla&vitae=ultrices&nisl=aliquet&aenean=maecenas&lectus=leo&pellentesque=odio&eget=condimentum&nunc=id&donec=luctus&quis=nec&orci=molestie&eget=sed&orci=justo&vehicula=pellentesque&condimentum=viverra&curabitur=pede&in=ac&libero=diam&ut=cras&massa=pellentesque&volutpat=volutpat&convallis=dui&morbi=maecenas&odio=tristique&odio=est&elementum=et&eu=tempus&interdum=semper&eu=est&tincidunt=quam&in=pharetra&leo=magna&maecenas=ac&pulvinar=consequat&lobortis=metus&est=sapien&phasellus=ut&sit=nunc&amet=vestibulum&erat=ante&nulla=ipsum&tempus=primis&vivamus=in&in=faucibus&felis=orci&eu=luctus&sapien=et&cursus=ultrices&vestibulum=posuere&proin=cubilia&eu=curae&mi=mauris&nulla=viverra&ac=diam&enim=vitae&in=quam&tempor=suspendisse&turpis=potenti&nec=nullam&euismod=porttitor&scelerisque=lacus&quam=at&turpis=turpis&adipiscing=donec&lorem=posuere&vitae=metus&mattis=vitae&nibh=ipsum&ligula=aliquam&nec=non&sem=mauris&duis=morbi&aliquam=non&convallis=lectus&nunc=aliquam&proin=sit&at=amet',
- },
- {
- title: 'Alpha',
- url:
- 'http://bravesites.com/tempus/vel.jpg?risus=est&auctor=phasellus&sed=sit&tristique=amet&in=erat&tempus=nulla&sit=tempus&amet=vivamus&sem=in&fusce=felis&consequat=eu&nulla=sapien&nisl=cursus&nunc=vestibulum&nisl=proin&duis=eu&bibendum=mi&felis=nulla&sed=ac&interdum=enim&venenatis=in&turpis=tempor&enim=turpis&blandit=nec&mi=euismod&in=scelerisque&porttitor=quam&pede=turpis&justo=adipiscing&eu=lorem&massa=vitae&donec=mattis&dapibus=nibh&duis=ligula',
- },
- {
- title: 'Sonsing',
- url:
- 'http://microsoft.com/blandit.js?quis=ante&lectus=vestibulum&suspendisse=ante&potenti=ipsum&in=primis&eleifend=in&quam=faucibus&a=orci&odio=luctus&in=et&hac=ultrices&habitasse=posuere&platea=cubilia&dictumst=curae&maecenas=duis&ut=faucibus&massa=accumsan&quis=odio&augue=curabitur&luctus=convallis&tincidunt=duis&nulla=consequat&mollis=dui&molestie=nec&lorem=nisi&quisque=volutpat&ut=eleifend&erat=donec&curabitur=ut&gravida=dolor&nisi=morbi&at=vel&nibh=lectus&in=in&hac=quam&habitasse=fringilla&platea=rhoncus&dictumst=mauris&aliquam=enim&augue=leo&quam=rhoncus&sollicitudin=sed&vitae=vestibulum&consectetuer=sit&eget=amet&rutrum=cursus&at=id&lorem=turpis&integer=integer&tincidunt=aliquet&ante=massa&vel=id&ipsum=lobortis&praesent=convallis&blandit=tortor&lacinia=risus&erat=dapibus&vestibulum=augue&sed=vel&magna=accumsan&at=tellus&nunc=nisi&commodo=eu&placerat=orci&praesent=mauris&blandit=lacinia&nam=sapien&nulla=quis&integer=libero',
- },
- {
- title: 'Fintone',
- url:
- 'https://linkedin.com/duis/bibendum/felis/sed/interdum/venenatis.json?ut=justo&suscipit=sollicitudin&a=ut&feugiat=suscipit&et=a&eros=feugiat&vestibulum=et&ac=eros&est=vestibulum&lacinia=ac&nisi=est&venenatis=lacinia&tristique=nisi&fusce=venenatis&congue=tristique&diam=fusce&id=congue&ornare=diam&imperdiet=id&sapien=ornare&urna=imperdiet&pretium=sapien&nisl=urna&ut=pretium&volutpat=nisl&sapien=ut&arcu=volutpat&sed=sapien&augue=arcu&aliquam=sed&erat=augue&volutpat=aliquam&in=erat&congue=volutpat&etiam=in&justo=congue&etiam=etiam&pretium=justo&iaculis=etiam&justo=pretium&in=iaculis&hac=justo&habitasse=in&platea=hac&dictumst=habitasse&etiam=platea&faucibus=dictumst&cursus=etiam&urna=faucibus&ut=cursus&tellus=urna&nulla=ut&ut=tellus&erat=nulla&id=ut&mauris=erat&vulputate=id&elementum=mauris&nullam=vulputate&varius=elementum&nulla=nullam&facilisi=varius&cras=nulla&non=facilisi&velit=cras&nec=non&nisi=velit&vulputate=nec&nonummy=nisi&maecenas=vulputate&tincidunt=nonummy&lacus=maecenas&at=tincidunt&velit=lacus&vivamus=at&vel=velit&nulla=vivamus&eget=vel&eros=nulla&elementum=eget',
- },
- {
- title: 'Fix San',
- url:
- 'http://pinterest.com/mi/in/porttitor/pede.png?varius=nibh&integer=quisque&ac=id&leo=justo&pellentesque=sit&ultrices=amet&mattis=sapien&odio=dignissim&donec=vestibulum&vitae=vestibulum&nisi=ante&nam=ipsum&ultrices=primis&libero=in&non=faucibus&mattis=orci&pulvinar=luctus&nulla=et&pede=ultrices&ullamcorper=posuere&augue=cubilia&a=curae&suscipit=nulla&nulla=dapibus&elit=dolor&ac=vel&nulla=est&sed=donec&vel=odio&enim=justo&sit=sollicitudin&amet=ut&nunc=suscipit&viverra=a&dapibus=feugiat&nulla=et&suscipit=eros&ligula=vestibulum&in=ac&lacus=est&curabitur=lacinia&at=nisi&ipsum=venenatis&ac=tristique&tellus=fusce&semper=congue&interdum=diam&mauris=id&ullamcorper=ornare&purus=imperdiet&sit=sapien&amet=urna&nulla=pretium&quisque=nisl&arcu=ut&libero=volutpat&rutrum=sapien&ac=arcu&lobortis=sed&vel=augue&dapibus=aliquam&at=erat&diam=volutpat&nam=in&tristique=congue&tortor=etiam',
- },
- {
- title: 'Ronstring',
- url:
- 'https://ebay.com/ut/erat.aspx?nulla=sed&eget=nisl&eros=nunc&elementum=rhoncus&pellentesque=dui&quisque=vel&porta=sem&volutpat=sed&erat=sagittis&quisque=nam&erat=congue&eros=risus&viverra=semper&eget=porta&congue=volutpat&eget=quam&semper=pede&rutrum=lobortis&nulla=ligula',
- },
- {
- title: 'It',
- url:
- 'http://symantec.com/tortor/sollicitudin/mi/sit/amet.json?in=nullam&libero=varius&ut=nulla&massa=facilisi&volutpat=cras&convallis=non&morbi=velit&odio=nec&odio=nisi&elementum=vulputate&eu=nonummy&interdum=maecenas&eu=tincidunt&tincidunt=lacus&in=at&leo=velit&maecenas=vivamus&pulvinar=vel&lobortis=nulla&est=eget&phasellus=eros&sit=elementum&amet=pellentesque&erat=quisque&nulla=porta&tempus=volutpat&vivamus=erat&in=quisque&felis=erat&eu=eros&sapien=viverra&cursus=eget&vestibulum=congue&proin=eget&eu=semper',
- },
- {
- title: 'Andalax',
- url:
- 'https://acquirethisname.com/tortor/eu.js?volutpat=mauris&dui=laoreet&maecenas=ut&tristique=rhoncus&est=aliquet&et=pulvinar&tempus=sed&semper=nisl&est=nunc&quam=rhoncus&pharetra=dui&magna=vel&ac=sem&consequat=sed&metus=sagittis&sapien=nam&ut=congue&nunc=risus&vestibulum=semper&ante=porta&ipsum=volutpat&primis=quam&in=pede&faucibus=lobortis&orci=ligula&luctus=sit&et=amet&ultrices=eleifend&posuere=pede&cubilia=libero&curae=quis&mauris=orci&viverra=nullam&diam=molestie&vitae=nibh&quam=in&suspendisse=lectus&potenti=pellentesque&nullam=at&porttitor=nulla&lacus=suspendisse&at=potenti&turpis=cras&donec=in&posuere=purus&metus=eu&vitae=magna&ipsum=vulputate&aliquam=luctus&non=cum&mauris=sociis&morbi=natoque&non=penatibus&lectus=et&aliquam=magnis&sit=dis&amet=parturient&diam=montes&in=nascetur&magna=ridiculus&bibendum=mus',
- },
-];
-
-export const templatingVariablesExamples = {
- text: {
- textSimple: 'My default value',
- textAdvanced: {
- label: 'Advanced text variable',
- type: 'text',
- options: {
- default_value: 'A default value',
- },
- },
- },
- custom: {
- customSimple: ['value1', 'value2', 'value3'],
- customAdvanced: {
- label: 'Advanced Var',
- type: 'custom',
- options: {
- values: [
- { value: 'value1', text: 'Var 1 Option 1' },
- {
- value: 'value2',
- text: 'Var 1 Option 2',
- default: true,
- },
- ],
- },
- },
- customAdvancedWithoutOpts: {
- type: 'custom',
- options: {},
- },
- customAdvancedWithoutLabel: {
- type: 'custom',
- options: {
- values: [
- { value: 'value1', text: 'Var 1 Option 1' },
- {
- value: 'value2',
- text: 'Var 1 Option 2',
- default: true,
- },
- ],
- },
- },
- customAdvancedWithoutType: {
- label: 'Variable 2',
- options: {
- values: [
- { value: 'value1', text: 'Var 1 Option 1' },
- {
- value: 'value2',
- text: 'Var 1 Option 2',
- default: true,
- },
- ],
- },
- },
- customAdvancedWithoutOptText: {
- label: 'Options without text',
- type: 'custom',
- options: {
- values: [
- { value: 'value1' },
- {
- value: 'value2',
- default: true,
- },
- ],
- },
- },
- },
- metricLabelValues: {
- metricLabelValuesSimple: {
- label: 'Metric Label Values',
- type: 'metric_label_values',
- options: {
- prometheus_endpoint_path: '/series',
- series_selector: 'backend:haproxy_backend_availability:ratio{env="{{env}}"}',
- label: 'backend',
- },
- },
- },
-};
-
-export const storeTextVariables = [
- {
- type: 'text',
- name: 'textSimple',
- label: 'textSimple',
- value: 'My default value',
- },
- {
- type: 'text',
- name: 'textAdvanced',
- label: 'Advanced text variable',
- value: 'A default value',
- },
-];
-
-export const storeCustomVariables = [
- {
- type: 'custom',
- name: 'customSimple',
- label: 'customSimple',
- options: {
- values: [
- { default: false, text: 'value1', value: 'value1' },
- { default: false, text: 'value2', value: 'value2' },
- { default: false, text: 'value3', value: 'value3' },
- ],
- },
- value: 'value1',
- },
- {
- type: 'custom',
- name: 'customAdvanced',
- label: 'Advanced Var',
- options: {
- values: [
- { default: false, text: 'Var 1 Option 1', value: 'value1' },
- { default: true, text: 'Var 1 Option 2', value: 'value2' },
- ],
- },
- value: 'value2',
- },
- {
- type: 'custom',
- name: 'customAdvancedWithoutOpts',
- label: 'customAdvancedWithoutOpts',
- options: { values: [] },
- value: null,
- },
- {
- type: 'custom',
- name: 'customAdvancedWithoutLabel',
- label: 'customAdvancedWithoutLabel',
- value: 'value2',
- options: {
- values: [
- { default: false, text: 'Var 1 Option 1', value: 'value1' },
- { default: true, text: 'Var 1 Option 2', value: 'value2' },
- ],
- },
- },
- {
- type: 'custom',
- name: 'customAdvancedWithoutOptText',
- label: 'Options without text',
- options: {
- values: [
- { default: false, text: 'value1', value: 'value1' },
- { default: true, text: 'value2', value: 'value2' },
- ],
- },
- value: 'value2',
- },
-];
-
-export const storeMetricLabelValuesVariables = [
- {
- type: 'metric_label_values',
- name: 'metricLabelValuesSimple',
- label: 'Metric Label Values',
- options: { prometheusEndpointPath: '/series', label: 'backend', values: [] },
- value: null,
- },
-];
-
-export const storeVariables = [
- ...storeTextVariables,
- ...storeCustomVariables,
- ...storeMetricLabelValuesVariables,
-];
-
-export const dashboardHeaderProps = {
- defaultBranch: 'main',
- isRearrangingPanels: false,
- selectedTimeRange: {
- start: '2020-01-01T00:00:00.000Z',
- end: '2020-01-01T01:00:00.000Z',
- },
-};
-
-export const dashboardActionsMenuProps = {
- defaultBranch: 'main',
- addingMetricsAvailable: true,
- customMetricsPath: 'https://path/to/customMetrics',
- validateQueryPath: 'https://path/to/validateQuery',
- isOotbDashboard: true,
-};
-
-export const mockAlert = {
- alert_path: 'alert_path',
- id: 8,
- metricId: 'mock_metric_id',
- operator: '>',
- query: 'testQuery',
- runbookUrl: invalidUrl,
- threshold: 5,
- title: 'alert title',
-};
diff --git a/spec/frontend/monitoring/pages/dashboard_page_spec.js b/spec/frontend/monitoring/pages/dashboard_page_spec.js
deleted file mode 100644
index 7fcb7607772..00000000000
--- a/spec/frontend/monitoring/pages/dashboard_page_spec.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
-import { createStore } from '~/monitoring/stores';
-import { assertProps } from 'helpers/assert_props';
-import { dashboardProps } from '../fixture_data';
-
-describe('monitoring/pages/dashboard_page', () => {
- let wrapper;
- let store;
- let $route;
-
- const buildRouter = () => {
- const dashboard = {};
- $route = {
- params: { dashboard },
- query: { dashboard },
- };
- };
-
- const buildWrapper = (props = {}) => {
- wrapper = shallowMount(DashboardPage, {
- store,
- propsData: {
- ...props,
- },
- mocks: {
- $route,
- },
- });
- };
-
- const findDashboardComponent = () => wrapper.findComponent(Dashboard);
-
- beforeEach(() => {
- buildRouter();
- store = createStore();
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- });
-
- it('throws errors if dashboard props are not passed', () => {
- expect(() => assertProps(DashboardPage, {})).toThrow('Missing required prop: "dashboardProps"');
- });
-
- it('renders the dashboard page with dashboard component', () => {
- buildWrapper({ dashboardProps });
-
- const allProps = {
- ...dashboardProps,
- // default props values
- rearrangePanelsAvailable: false,
- showHeader: true,
- showPanels: true,
- smallEmptyState: false,
- };
-
- expect(findDashboardComponent().exists()).toBe(true);
- expect(allProps).toMatchObject(findDashboardComponent().props());
- });
-});
diff --git a/spec/frontend/monitoring/pages/panel_new_page_spec.js b/spec/frontend/monitoring/pages/panel_new_page_spec.js
deleted file mode 100644
index 98ee6c1cb29..00000000000
--- a/spec/frontend/monitoring/pages/panel_new_page_spec.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue';
-import PanelNewPage from '~/monitoring/pages/panel_new_page.vue';
-import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
-import { createStore } from '~/monitoring/stores';
-
-const dashboard = 'dashboard.yml';
-
-// Button stub that can accept `to` as router links do
-// https://bootstrap-vue.org/docs/components/button#comp-ref-b-button-props
-const GlButtonStub = {
- extends: GlButton,
- props: {
- to: [String, Object],
- },
-};
-
-describe('monitoring/pages/panel_new_page', () => {
- let store;
- let wrapper;
- let $route;
- let $router;
-
- const mountComponent = (propsData = {}, route) => {
- $route = route ?? { name: PANEL_NEW_PAGE, params: { dashboard } };
- $router = {
- push: jest.fn(),
- };
-
- wrapper = shallowMount(PanelNewPage, {
- propsData,
- store,
- stubs: {
- GlButton: GlButtonStub,
- },
- mocks: {
- $router,
- $route,
- },
- });
- };
-
- const findBackButton = () => wrapper.findComponent(GlButtonStub);
- const findPanelBuilder = () => wrapper.findComponent(DashboardPanelBuilder);
-
- beforeEach(() => {
- store = createStore();
- mountComponent();
- });
-
- describe('back to dashboard button', () => {
- it('is rendered', () => {
- expect(findBackButton().exists()).toBe(true);
- expect(findBackButton().props('icon')).toBe('go-back');
- });
-
- it('links back to the dashboard', () => {
- expect(findBackButton().props('to')).toEqual({
- name: DASHBOARD_PAGE,
- params: { dashboard },
- });
- });
-
- it('links back to the dashboard while preserving query params', () => {
- $route = {
- name: PANEL_NEW_PAGE,
- params: { dashboard },
- query: { another: 'param' },
- };
-
- mountComponent({}, $route);
-
- expect(findBackButton().props('to')).toEqual({
- name: DASHBOARD_PAGE,
- params: { dashboard },
- query: { another: 'param' },
- });
- });
- });
-
- describe('dashboard panel builder', () => {
- it('is rendered', () => {
- expect(findPanelBuilder().exists()).toBe(true);
- });
- });
-
- describe('page routing', () => {
- it('route is not updated by default', () => {
- expect($router.push).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/monitoring/requests/index_spec.js b/spec/frontend/monitoring/requests/index_spec.js
deleted file mode 100644
index 308895768a4..00000000000
--- a/spec/frontend/monitoring/requests/index_spec.js
+++ /dev/null
@@ -1,157 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import { backoffMockImplementation } from 'helpers/backoff_helper';
-import axios from '~/lib/utils/axios_utils';
-import * as commonUtils from '~/lib/utils/common_utils';
-import {
- HTTP_STATUS_BAD_REQUEST,
- HTTP_STATUS_INTERNAL_SERVER_ERROR,
- HTTP_STATUS_NO_CONTENT,
- HTTP_STATUS_OK,
- HTTP_STATUS_SERVICE_UNAVAILABLE,
- HTTP_STATUS_UNAUTHORIZED,
- HTTP_STATUS_UNPROCESSABLE_ENTITY,
-} from '~/lib/utils/http_status';
-import { getDashboard, getPrometheusQueryData } from '~/monitoring/requests';
-import { metricsDashboardResponse } from '../fixture_data';
-
-describe('monitoring metrics_requests', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation);
- });
-
- afterEach(() => {
- mock.reset();
-
- commonUtils.backOff.mockReset();
- });
-
- describe('getDashboard', () => {
- const response = metricsDashboardResponse;
- const dashboardEndpoint = '/dashboard';
- const params = {
- start_time: 'start_time',
- end_time: 'end_time',
- };
-
- it('returns a dashboard response', () => {
- mock.onGet(dashboardEndpoint).reply(HTTP_STATUS_OK, response);
-
- return getDashboard(dashboardEndpoint, params).then((data) => {
- expect(data).toEqual(metricsDashboardResponse);
- });
- });
-
- it('returns a dashboard response after retrying twice', () => {
- mock.onGet(dashboardEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(dashboardEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(dashboardEndpoint).reply(HTTP_STATUS_OK, response);
-
- return getDashboard(dashboardEndpoint, params).then((data) => {
- expect(data).toEqual(metricsDashboardResponse);
- expect(mock.history.get).toHaveLength(3);
- });
- });
-
- it('rejects after getting an error', () => {
- mock.onGet(dashboardEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- return getDashboard(dashboardEndpoint, params).catch((error) => {
- expect(error).toEqual(expect.any(Error));
- expect(mock.history.get).toHaveLength(1);
- });
- });
- });
-
- describe('getPrometheusQueryData', () => {
- const response = {
- status: 'success',
- data: {
- resultType: 'matrix',
- result: [],
- },
- };
- const prometheusEndpoint = '/query_range';
- const params = {
- start_time: 'start_time',
- end_time: 'end_time',
- };
-
- it('returns a dashboard response', () => {
- mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_OK, response);
-
- return getPrometheusQueryData(prometheusEndpoint, params).then((data) => {
- expect(data).toEqual(response.data);
- });
- });
-
- it('returns a dashboard response after retrying twice', () => {
- // Mock multiple attempts while the cache is filling up
- mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_OK, response); // 3rd attempt
-
- return getPrometheusQueryData(prometheusEndpoint, params).then((data) => {
- expect(data).toEqual(response.data);
- expect(mock.history.get).toHaveLength(3);
- });
- });
-
- it('rejects after getting an HTTP 500 error', () => {
- mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {
- status: 'error',
- error: 'An error occurred',
- });
-
- return getPrometheusQueryData(prometheusEndpoint, params).catch((error) => {
- expect(error).toEqual(new Error('Request failed with status code 500'));
- });
- });
-
- it('rejects after retrying twice and getting an HTTP 401 error', () => {
- // Mock multiple attempts while the cache is filling up and fails
- mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_UNAUTHORIZED, {
- status: 'error',
- error: 'An error occurred',
- });
-
- return getPrometheusQueryData(prometheusEndpoint, params).catch((error) => {
- expect(error).toEqual(new Error('Request failed with status code 401'));
- });
- });
-
- it('rejects after retrying twice and getting an HTTP 500 error', () => {
- // Mock multiple attempts while the cache is filling up and fails
- mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {
- status: 'error',
- error: 'An error occurred',
- }); // 3rd attempt
-
- return getPrometheusQueryData(prometheusEndpoint, params).catch((error) => {
- expect(error).toEqual(new Error('Request failed with status code 500'));
- expect(mock.history.get).toHaveLength(3);
- });
- });
-
- it.each`
- code | reason
- ${HTTP_STATUS_BAD_REQUEST} | ${'Parameters are missing or incorrect'}
- ${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
- ${HTTP_STATUS_SERVICE_UNAVAILABLE} | ${'Query timed out or aborted'}
- `('rejects with details: "$reason" after getting an HTTP $code error', ({ code, reason }) => {
- mock.onGet(prometheusEndpoint).reply(code, {
- status: 'error',
- error: reason,
- });
-
- return getPrometheusQueryData(prometheusEndpoint, params).catch((error) => {
- expect(error).toEqual(new Error(reason));
- expect(mock.history.get).toHaveLength(1);
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/router_spec.js b/spec/frontend/monitoring/router_spec.js
deleted file mode 100644
index 368bd955fb3..00000000000
--- a/spec/frontend/monitoring/router_spec.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import { mount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueRouter from 'vue-router';
-import Dashboard from '~/monitoring/components/dashboard.vue';
-import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
-import PanelNewPage from '~/monitoring/pages/panel_new_page.vue';
-import createRouter from '~/monitoring/router';
-import { createStore } from '~/monitoring/stores';
-import { dashboardProps } from './fixture_data';
-import { dashboardHeaderProps } from './mock_data';
-
-const LEGACY_BASE_PATH = '/project/my-group/test-project/-/environments/71146/metrics';
-const BASE_PATH = '/project/my-group/test-project/-/metrics';
-
-const MockApp = {
- data() {
- return {
- dashboardProps: { ...dashboardProps, ...dashboardHeaderProps },
- };
- },
- template: `<router-view :dashboard-props="dashboardProps"/>`,
-};
-
-describe('Monitoring router', () => {
- let router;
- let store;
-
- const createWrapper = (basePath, routeArg) => {
- Vue.use(VueRouter);
-
- router = createRouter(basePath);
- if (routeArg !== undefined) {
- router.push(routeArg);
- }
-
- return mount(MockApp, {
- store,
- router,
- });
- };
-
- beforeEach(() => {
- store = createStore();
- jest.spyOn(store, 'dispatch').mockResolvedValue();
- });
-
- afterEach(() => {
- window.location.hash = '';
- });
-
- describe('support legacy URLs with full dashboard path to visit dashboard page', () => {
- it.each`
- path | currentDashboard
- ${'/dashboard.yml'} | ${'dashboard.yml'}
- ${'/folder1/dashboard.yml'} | ${'folder1/dashboard.yml'}
- ${'/?dashboard=dashboard.yml'} | ${'dashboard.yml'}
- `('"$path" renders page with dashboard "$currentDashboard"', ({ path, currentDashboard }) => {
- const wrapper = createWrapper(LEGACY_BASE_PATH, path);
-
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setCurrentDashboard', {
- currentDashboard,
- });
-
- expect(wrapper.findComponent(DashboardPage).exists()).toBe(true);
- expect(wrapper.findComponent(DashboardPage).findComponent(Dashboard).exists()).toBe(true);
- });
- });
-
- describe('supports URLs to visit dashboard page', () => {
- it.each`
- path | currentDashboard
- ${'/'} | ${null}
- ${'/dashboard.yml'} | ${'dashboard.yml'}
- ${'/folder1/dashboard.yml'} | ${'folder1/dashboard.yml'}
- ${'/folder1%2Fdashboard.yml'} | ${'folder1/dashboard.yml'}
- ${'/dashboard.yml'} | ${'dashboard.yml'}
- ${'/config/prometheus/common_metrics.yml'} | ${'config/prometheus/common_metrics.yml'}
- ${'/config/prometheus/pod_metrics.yml'} | ${'config/prometheus/pod_metrics.yml'}
- ${'/config%2Fprometheus%2Fpod_metrics.yml'} | ${'config/prometheus/pod_metrics.yml'}
- `('"$path" renders page with dashboard "$currentDashboard"', ({ path, currentDashboard }) => {
- const wrapper = createWrapper(BASE_PATH, path);
-
- expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setCurrentDashboard', {
- currentDashboard,
- });
-
- expect(wrapper.findComponent(DashboardPage).exists()).toBe(true);
- expect(wrapper.findComponent(DashboardPage).findComponent(Dashboard).exists()).toBe(true);
- });
- });
-
- describe('supports URLs to visit new panel page', () => {
- it.each`
- path | currentDashboard
- ${'/panel/new'} | ${undefined}
- ${'/dashboard.yml/panel/new'} | ${'dashboard.yml'}
- ${'/config/prometheus/common_metrics.yml/panel/new'} | ${'config/prometheus/common_metrics.yml'}
- ${'/config%2Fprometheus%2Fcommon_metrics.yml/panel/new'} | ${'config/prometheus/common_metrics.yml'}
- `('"$path" renders page with dashboard "$currentDashboard"', ({ path, currentDashboard }) => {
- const wrapper = createWrapper(BASE_PATH, path);
-
- expect(wrapper.vm.$route.params.dashboard).toBe(currentDashboard);
- expect(wrapper.findComponent(PanelNewPage).exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
deleted file mode 100644
index b3b198d6b51..00000000000
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ /dev/null
@@ -1,1167 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import { backoffMockImplementation } from 'helpers/backoff_helper';
-import testAction from 'helpers/vuex_action_helper';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import * as commonUtils from '~/lib/utils/common_utils';
-import {
- HTTP_STATUS_BAD_REQUEST,
- HTTP_STATUS_CREATED,
- HTTP_STATUS_INTERNAL_SERVER_ERROR,
- HTTP_STATUS_OK,
- HTTP_STATUS_UNPROCESSABLE_ENTITY,
-} from '~/lib/utils/http_status';
-import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
-
-import getAnnotations from '~/monitoring/queries/get_annotations.query.graphql';
-import getDashboardValidationWarnings from '~/monitoring/queries/get_dashboard_validation_warnings.query.graphql';
-import getEnvironments from '~/monitoring/queries/get_environments.query.graphql';
-import { createStore } from '~/monitoring/stores';
-import {
- setGettingStartedEmptyState,
- setInitialState,
- setExpandedPanel,
- clearExpandedPanel,
- filterEnvironments,
- fetchData,
- fetchDashboard,
- receiveMetricsDashboardSuccess,
- fetchDashboardData,
- fetchPrometheusMetric,
- fetchDeploymentsData,
- fetchEnvironmentsData,
- fetchAnnotations,
- fetchDashboardValidationWarnings,
- toggleStarredValue,
- duplicateSystemDashboard,
- updateVariablesAndFetchData,
- fetchVariableMetricLabelValues,
- fetchPanelPreview,
-} from '~/monitoring/stores/actions';
-import * as getters from '~/monitoring/stores/getters';
-import * as types from '~/monitoring/stores/mutation_types';
-import storeState from '~/monitoring/stores/state';
-import {
- gqClient,
- parseEnvironmentsResponse,
- parseAnnotationsResponse,
-} from '~/monitoring/stores/utils';
-import Tracking from '~/tracking';
-import { defaultTimeRange } from '~/vue_shared/constants';
-import {
- metricsDashboardResponse,
- metricsDashboardViewModel,
- metricsDashboardPanelCount,
-} from '../fixture_data';
-import {
- deploymentData,
- environmentData,
- annotationsData,
- dashboardGitResponse,
- mockDashboardsErrorResponse,
-} from '../mock_data';
-
-jest.mock('~/alert');
-
-describe('Monitoring store actions', () => {
- const { convertObjectPropsToCamelCase } = commonUtils;
-
- let mock;
- let store;
- let state;
-
- let dispatch;
- let commit;
-
- beforeEach(() => {
- store = createStore({ getters });
- state = store.state.monitoringDashboard;
- mock = new MockAdapter(axios);
-
- commit = jest.fn();
- dispatch = jest.fn();
-
- jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation);
- });
-
- afterEach(() => {
- mock.reset();
-
- commonUtils.backOff.mockReset();
- createAlert.mockReset();
- });
-
- // Setup
-
- describe('setGettingStartedEmptyState', () => {
- it('should commit SET_GETTING_STARTED_EMPTY_STATE mutation', () => {
- return testAction(
- setGettingStartedEmptyState,
- null,
- state,
- [
- {
- type: types.SET_GETTING_STARTED_EMPTY_STATE,
- },
- ],
- [],
- );
- });
- });
-
- describe('setInitialState', () => {
- it('should commit SET_INITIAL_STATE mutation', () => {
- return testAction(
- setInitialState,
- {
- currentDashboard: '.gitlab/dashboards/dashboard.yml',
- deploymentsEndpoint: 'deployments.json',
- },
- state,
- [
- {
- type: types.SET_INITIAL_STATE,
- payload: {
- currentDashboard: '.gitlab/dashboards/dashboard.yml',
- deploymentsEndpoint: 'deployments.json',
- },
- },
- ],
- [],
- );
- });
- });
-
- describe('setExpandedPanel', () => {
- it('Sets a panel as expanded', () => {
- const group = 'group_1';
- const panel = { title: 'A Panel' };
-
- return testAction(
- setExpandedPanel,
- { group, panel },
- state,
- [{ type: types.SET_EXPANDED_PANEL, payload: { group, panel } }],
- [],
- );
- });
- });
-
- describe('clearExpandedPanel', () => {
- it('Clears a panel as expanded', () => {
- return testAction(
- clearExpandedPanel,
- undefined,
- state,
- [{ type: types.SET_EXPANDED_PANEL, payload: { group: null, panel: null } }],
- [],
- );
- });
- });
-
- // All Data
-
- describe('fetchData', () => {
- it('dispatches fetchEnvironmentsData and fetchEnvironmentsData', () => {
- return testAction(
- fetchData,
- null,
- state,
- [],
- [
- { type: 'fetchEnvironmentsData' },
- { type: 'fetchDashboard' },
- { type: 'fetchAnnotations' },
- ],
- );
- });
-
- it('dispatches when feature metricsDashboardAnnotations is on', () => {
- window.gon = { features: { metricsDashboardAnnotations: true } };
-
- return testAction(
- fetchData,
- null,
- state,
- [],
- [
- { type: 'fetchEnvironmentsData' },
- { type: 'fetchDashboard' },
- { type: 'fetchAnnotations' },
- ],
- );
- });
- });
-
- // Metrics dashboard
-
- describe('fetchDashboard', () => {
- const response = metricsDashboardResponse;
- beforeEach(() => {
- state.dashboardEndpoint = '/dashboard';
- });
-
- it('on success, dispatches receive and success actions, then fetches dashboard warnings', () => {
- document.body.dataset.page = 'projects:environments:metrics';
- mock.onGet(state.dashboardEndpoint).reply(HTTP_STATUS_OK, response);
-
- return testAction(
- fetchDashboard,
- null,
- state,
- [],
- [
- { type: 'requestMetricsDashboard' },
- {
- type: 'receiveMetricsDashboardSuccess',
- payload: { response },
- },
- { type: 'fetchDashboardValidationWarnings' },
- ],
- );
- });
-
- describe('on failure', () => {
- let result;
- beforeEach(() => {
- const params = {};
- const localGetters = {
- fullDashboardPath: store.getters['monitoringDashboard/fullDashboardPath'],
- };
- result = () => {
- mock
- .onGet(state.dashboardEndpoint)
- .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, mockDashboardsErrorResponse);
- return fetchDashboard({ state, commit, dispatch, getters: localGetters }, params);
- };
- });
-
- it('dispatches a failure', async () => {
- await result();
- expect(commit).toHaveBeenCalledWith(
- types.SET_ALL_DASHBOARDS,
- mockDashboardsErrorResponse.all_dashboards,
- );
- expect(dispatch).toHaveBeenCalledWith(
- 'receiveMetricsDashboardFailure',
- new Error('Request failed with status code 500'),
- );
- expect(createAlert).toHaveBeenCalled();
- });
-
- it('dispatches a failure action when a message is returned', async () => {
- await result();
- expect(dispatch).toHaveBeenCalledWith(
- 'receiveMetricsDashboardFailure',
- new Error('Request failed with status code 500'),
- );
- expect(createAlert).toHaveBeenCalledWith({
- message: expect.stringContaining(mockDashboardsErrorResponse.message),
- });
- });
-
- it('does not show an alert when showErrorBanner is disabled', async () => {
- state.showErrorBanner = false;
-
- await result();
- expect(dispatch).toHaveBeenCalledWith(
- 'receiveMetricsDashboardFailure',
- new Error('Request failed with status code 500'),
- );
- expect(createAlert).not.toHaveBeenCalled();
- });
- });
- });
-
- describe('receiveMetricsDashboardSuccess', () => {
- it('stores groups', () => {
- const response = metricsDashboardResponse;
- receiveMetricsDashboardSuccess({ state, commit, dispatch }, { response });
- expect(commit).toHaveBeenCalledWith(
- types.RECEIVE_METRICS_DASHBOARD_SUCCESS,
-
- metricsDashboardResponse.dashboard,
- );
- expect(dispatch).toHaveBeenCalledWith('fetchDashboardData');
- });
-
- it('sets the dashboards loaded from the repository', () => {
- const params = {};
- const response = metricsDashboardResponse;
- response.all_dashboards = dashboardGitResponse;
- receiveMetricsDashboardSuccess(
- {
- state,
- commit,
- dispatch,
- },
- {
- response,
- params,
- },
- );
- expect(commit).toHaveBeenCalledWith(types.SET_ALL_DASHBOARDS, dashboardGitResponse);
- });
- });
-
- // Metrics
-
- describe('fetchDashboardData', () => {
- beforeEach(() => {
- jest.spyOn(Tracking, 'event');
-
- state.timeRange = defaultTimeRange;
- });
-
- it('commits empty state when state.groups is empty', async () => {
- const localGetters = {
- metricsWithData: () => [],
- };
- await fetchDashboardData({ state, commit, dispatch, getters: localGetters });
- expect(Tracking.event).toHaveBeenCalledWith(document.body.dataset.page, 'dashboard_fetch', {
- label: 'custom_metrics_dashboard',
- property: 'count',
- value: 0,
- });
- expect(dispatch).toHaveBeenCalledTimes(2);
- expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
- expect(dispatch).toHaveBeenCalledWith('fetchVariableMetricLabelValues', {
- defaultQueryParams: {
- start_time: expect.any(String),
- end_time: expect.any(String),
- step: expect.any(Number),
- },
- });
-
- expect(createAlert).not.toHaveBeenCalled();
- });
-
- it('dispatches fetchPrometheusMetric for each panel query', async () => {
- state.dashboard.panelGroups = convertObjectPropsToCamelCase(
- metricsDashboardResponse.dashboard.panel_groups,
- );
-
- const [metric] = state.dashboard.panelGroups[0].panels[0].metrics;
- const localGetters = {
- metricsWithData: () => [metric.id],
- };
-
- await fetchDashboardData({ state, commit, dispatch, getters: localGetters });
- expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
- metric,
- defaultQueryParams: {
- start_time: expect.any(String),
- end_time: expect.any(String),
- step: expect.any(Number),
- },
- });
-
- expect(Tracking.event).toHaveBeenCalledWith(document.body.dataset.page, 'dashboard_fetch', {
- label: 'custom_metrics_dashboard',
- property: 'count',
- value: 1,
- });
- });
-
- it('dispatches fetchPrometheusMetric for each panel query, handles an error', async () => {
- state.dashboard.panelGroups = metricsDashboardViewModel.panelGroups;
- const metric = state.dashboard.panelGroups[0].panels[0].metrics[0];
-
- dispatch.mockResolvedValueOnce(); // fetchDeploymentsData
- dispatch.mockResolvedValueOnce(); // fetchVariableMetricLabelValues
- // Mock having one out of four metrics failing
- dispatch.mockRejectedValueOnce(new Error('Error fetching this metric'));
- dispatch.mockResolvedValue();
-
- await fetchDashboardData({ state, commit, dispatch });
- const defaultQueryParams = {
- start_time: expect.any(String),
- end_time: expect.any(String),
- step: expect.any(Number),
- };
-
- expect(dispatch).toHaveBeenCalledTimes(metricsDashboardPanelCount + 2); // plus 1 for deployments
- expect(dispatch).toHaveBeenCalledWith('fetchDeploymentsData');
- expect(dispatch).toHaveBeenCalledWith('fetchVariableMetricLabelValues', {
- defaultQueryParams,
- });
- expect(dispatch).toHaveBeenCalledWith('fetchPrometheusMetric', {
- metric,
- defaultQueryParams,
- });
-
- expect(createAlert).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('fetchPrometheusMetric', () => {
- const defaultQueryParams = {
- start_time: '2019-08-06T12:40:02.184Z',
- end_time: '2019-08-06T20:40:02.184Z',
- step: 60,
- };
- let metric;
- let data;
- let prometheusEndpointPath;
-
- beforeEach(() => {
- state = storeState();
- [metric] = metricsDashboardViewModel.panelGroups[0].panels[0].metrics;
-
- prometheusEndpointPath = metric.prometheusEndpointPath;
-
- data = {
- metricId: metric.metricId,
- result: [1582065167.353, 5, 1582065599.353],
- };
- });
-
- it('commits result', () => {
- mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_OK, { data }); // One attempt
-
- return testAction(
- fetchPrometheusMetric,
- { metric, defaultQueryParams },
- state,
- [
- {
- type: types.REQUEST_METRIC_RESULT,
- payload: {
- metricId: metric.metricId,
- },
- },
- {
- type: types.RECEIVE_METRIC_RESULT_SUCCESS,
- payload: {
- metricId: metric.metricId,
- data,
- },
- },
- ],
- [],
- );
- });
-
- describe('without metric defined step', () => {
- const expectedParams = {
- start_time: '2019-08-06T12:40:02.184Z',
- end_time: '2019-08-06T20:40:02.184Z',
- step: 60,
- };
-
- it('uses calculated step', async () => {
- mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_OK, { data }); // One attempt
-
- await testAction(
- fetchPrometheusMetric,
- { metric, defaultQueryParams },
- state,
- [
- {
- type: types.REQUEST_METRIC_RESULT,
- payload: {
- metricId: metric.metricId,
- },
- },
- {
- type: types.RECEIVE_METRIC_RESULT_SUCCESS,
- payload: {
- metricId: metric.metricId,
- data,
- },
- },
- ],
- [],
- );
- expect(mock.history.get[0].params).toEqual(expectedParams);
- });
- });
-
- describe('with metric defined step', () => {
- beforeEach(() => {
- metric.step = 7;
- });
-
- const expectedParams = {
- start_time: '2019-08-06T12:40:02.184Z',
- end_time: '2019-08-06T20:40:02.184Z',
- step: 7,
- };
-
- it('uses metric step', async () => {
- mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_OK, { data }); // One attempt
-
- await testAction(
- fetchPrometheusMetric,
- { metric, defaultQueryParams },
- state,
- [
- {
- type: types.REQUEST_METRIC_RESULT,
- payload: {
- metricId: metric.metricId,
- },
- },
- {
- type: types.RECEIVE_METRIC_RESULT_SUCCESS,
- payload: {
- metricId: metric.metricId,
- data,
- },
- },
- ],
- [],
- );
- expect(mock.history.get[0].params).toEqual(expectedParams);
- });
- });
-
- it('commits failure, when waiting for results and getting a server error', async () => {
- mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- const error = new Error('Request failed with status code 500');
-
- await expect(
- testAction(
- fetchPrometheusMetric,
- { metric, defaultQueryParams },
- state,
- [
- {
- type: types.REQUEST_METRIC_RESULT,
- payload: {
- metricId: metric.metricId,
- },
- },
- {
- type: types.RECEIVE_METRIC_RESULT_FAILURE,
- payload: {
- metricId: metric.metricId,
- error,
- },
- },
- ],
- [],
- ),
- ).rejects.toEqual(error);
- });
- });
-
- // Deployments
-
- describe('fetchDeploymentsData', () => {
- it('dispatches receiveDeploymentsDataSuccess on success', () => {
- state.deploymentsEndpoint = '/success';
- mock.onGet(state.deploymentsEndpoint).reply(HTTP_STATUS_OK, {
- deployments: deploymentData,
- });
-
- return testAction(
- fetchDeploymentsData,
- null,
- state,
- [],
- [{ type: 'receiveDeploymentsDataSuccess', payload: deploymentData }],
- );
- });
- it('dispatches receiveDeploymentsDataFailure on error', () => {
- state.deploymentsEndpoint = '/error';
- mock.onGet(state.deploymentsEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- return testAction(
- fetchDeploymentsData,
- null,
- state,
- [],
- [{ type: 'receiveDeploymentsDataFailure' }],
- () => {
- expect(createAlert).toHaveBeenCalled();
- },
- );
- });
- });
-
- // Environments
-
- describe('fetchEnvironmentsData', () => {
- beforeEach(() => {
- state.projectPath = 'gitlab-org/gitlab-test';
- });
-
- it('setting SET_ENVIRONMENTS_FILTER should dispatch fetchEnvironmentsData', () => {
- jest.spyOn(gqClient, 'mutate').mockReturnValue({
- data: {
- project: {
- data: {
- environments: [],
- },
- },
- },
- });
-
- return testAction(
- filterEnvironments,
- {},
- state,
- [
- {
- type: 'SET_ENVIRONMENTS_FILTER',
- payload: {},
- },
- ],
- [
- {
- type: 'fetchEnvironmentsData',
- },
- ],
- );
- });
-
- it('fetch environments data call takes in search param', () => {
- const mockMutate = jest.spyOn(gqClient, 'mutate');
- const searchTerm = 'Something';
- const mutationVariables = {
- mutation: getEnvironments,
- variables: {
- projectPath: state.projectPath,
- search: searchTerm,
- states: [ENVIRONMENT_AVAILABLE_STATE],
- },
- };
- state.environmentsSearchTerm = searchTerm;
- mockMutate.mockResolvedValue({});
-
- return testAction(
- fetchEnvironmentsData,
- null,
- state,
- [],
- [
- { type: 'requestEnvironmentsData' },
- { type: 'receiveEnvironmentsDataSuccess', payload: [] },
- ],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
-
- it('dispatches receiveEnvironmentsDataSuccess on success', () => {
- jest.spyOn(gqClient, 'mutate').mockResolvedValue({
- data: {
- project: {
- data: {
- environments: environmentData,
- },
- },
- },
- });
-
- return testAction(
- fetchEnvironmentsData,
- null,
- state,
- [],
- [
- { type: 'requestEnvironmentsData' },
- {
- type: 'receiveEnvironmentsDataSuccess',
- payload: parseEnvironmentsResponse(environmentData, state.projectPath),
- },
- ],
- );
- });
-
- it('dispatches receiveEnvironmentsDataFailure on error', () => {
- jest.spyOn(gqClient, 'mutate').mockRejectedValue({});
-
- return testAction(
- fetchEnvironmentsData,
- null,
- state,
- [],
- [{ type: 'requestEnvironmentsData' }, { type: 'receiveEnvironmentsDataFailure' }],
- );
- });
- });
-
- describe('fetchAnnotations', () => {
- beforeEach(() => {
- state.timeRange = {
- start: '2020-04-15T12:54:32.137Z',
- end: '2020-08-15T12:54:32.137Z',
- };
- state.projectPath = 'gitlab-org/gitlab-test';
- state.currentEnvironmentName = 'production';
- state.currentDashboard = '.gitlab/dashboards/custom_dashboard.yml';
- // testAction doesn't have access to getters. The state is passed in as getters
- // instead of the actual getters inside the testAction method implementation.
- // All methods downstream that needs access to getters will throw and error.
- // For that reason, the result of the getter is set as a state variable.
- state.fullDashboardPath = store.getters['monitoringDashboard/fullDashboardPath'];
- });
-
- it('fetches annotations data and dispatches receiveAnnotationsSuccess', () => {
- const mockMutate = jest.spyOn(gqClient, 'mutate');
- const mutationVariables = {
- mutation: getAnnotations,
- variables: {
- projectPath: state.projectPath,
- environmentName: state.currentEnvironmentName,
- dashboardPath: state.currentDashboard,
- startingFrom: state.timeRange.start,
- },
- };
- const parsedResponse = parseAnnotationsResponse(annotationsData);
-
- mockMutate.mockResolvedValue({
- data: {
- project: {
- environments: {
- nodes: [
- {
- metricsDashboard: {
- annotations: {
- nodes: parsedResponse,
- },
- },
- },
- ],
- },
- },
- },
- });
-
- return testAction(
- fetchAnnotations,
- null,
- state,
- [],
- [{ type: 'receiveAnnotationsSuccess', payload: parsedResponse }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
-
- it('dispatches receiveAnnotationsFailure if the annotations API call fails', () => {
- const mockMutate = jest.spyOn(gqClient, 'mutate');
- const mutationVariables = {
- mutation: getAnnotations,
- variables: {
- projectPath: state.projectPath,
- environmentName: state.currentEnvironmentName,
- dashboardPath: state.currentDashboard,
- startingFrom: state.timeRange.start,
- },
- };
-
- mockMutate.mockRejectedValue({});
-
- return testAction(
- fetchAnnotations,
- null,
- state,
- [],
- [{ type: 'receiveAnnotationsFailure' }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
- });
-
- describe('fetchDashboardValidationWarnings', () => {
- let mockMutate;
- let mutationVariables;
-
- beforeEach(() => {
- state.projectPath = 'gitlab-org/gitlab-test';
- state.currentEnvironmentName = 'production';
- state.currentDashboard = '.gitlab/dashboards/dashboard_with_warnings.yml';
- // testAction doesn't have access to getters. The state is passed in as getters
- // instead of the actual getters inside the testAction method implementation.
- // All methods downstream that needs access to getters will throw and error.
- // For that reason, the result of the getter is set as a state variable.
- state.fullDashboardPath = store.getters['monitoringDashboard/fullDashboardPath'];
-
- mockMutate = jest.spyOn(gqClient, 'mutate');
- mutationVariables = {
- mutation: getDashboardValidationWarnings,
- variables: {
- projectPath: state.projectPath,
- environmentName: state.currentEnvironmentName,
- dashboardPath: state.fullDashboardPath,
- },
- };
- });
-
- it('dispatches receiveDashboardValidationWarningsSuccess with true payload when there are warnings', () => {
- mockMutate.mockResolvedValue({
- data: {
- project: {
- id: 'gid://gitlab/Project/29',
- environments: {
- nodes: [
- {
- name: 'production',
- metricsDashboard: {
- path: '.gitlab/dashboards/dashboard_errors_test.yml',
- schemaValidationWarnings: ["unit: can't be blank"],
- },
- },
- ],
- },
- },
- },
- });
-
- return testAction(
- fetchDashboardValidationWarnings,
- null,
- state,
- [],
- [{ type: 'receiveDashboardValidationWarningsSuccess', payload: true }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
-
- it('dispatches receiveDashboardValidationWarningsSuccess with false payload when there are no warnings', () => {
- mockMutate.mockResolvedValue({
- data: {
- project: {
- id: 'gid://gitlab/Project/29',
- environments: {
- nodes: [
- {
- name: 'production',
- metricsDashboard: {
- path: '.gitlab/dashboards/dashboard_errors_test.yml',
- schemaValidationWarnings: [],
- },
- },
- ],
- },
- },
- },
- });
-
- return testAction(
- fetchDashboardValidationWarnings,
- null,
- state,
- [],
- [{ type: 'receiveDashboardValidationWarningsSuccess', payload: false }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
-
- it('dispatches receiveDashboardValidationWarningsSuccess with false payload when the response is empty', () => {
- mockMutate.mockResolvedValue({
- data: {
- project: null,
- },
- });
-
- return testAction(
- fetchDashboardValidationWarnings,
- null,
- state,
- [],
- [{ type: 'receiveDashboardValidationWarningsSuccess', payload: false }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
-
- it('dispatches receiveDashboardValidationWarningsFailure if the warnings API call fails', () => {
- mockMutate.mockRejectedValue({});
-
- return testAction(
- fetchDashboardValidationWarnings,
- null,
- state,
- [],
- [{ type: 'receiveDashboardValidationWarningsFailure' }],
- () => {
- expect(mockMutate).toHaveBeenCalledWith(mutationVariables);
- },
- );
- });
- });
-
- // Dashboard manipulation
-
- describe('toggleStarredValue', () => {
- let unstarredDashboard;
- let starredDashboard;
-
- beforeEach(() => {
- state.isUpdatingStarredValue = false;
- [unstarredDashboard, starredDashboard] = dashboardGitResponse;
- });
-
- it('performs no changes if no dashboard is selected', () => {
- return testAction(toggleStarredValue, null, state, [], []);
- });
-
- it('performs no changes if already changing starred value', () => {
- state.selectedDashboard = unstarredDashboard;
- state.isUpdatingStarredValue = true;
- return testAction(toggleStarredValue, null, state, [], []);
- });
-
- it('stars dashboard if it is not starred', () => {
- state.selectedDashboard = unstarredDashboard;
- mock.onPost(unstarredDashboard.user_starred_path).reply(HTTP_STATUS_OK);
-
- return testAction(toggleStarredValue, null, state, [
- { type: types.REQUEST_DASHBOARD_STARRING },
- {
- type: types.RECEIVE_DASHBOARD_STARRING_SUCCESS,
- payload: {
- newStarredValue: true,
- selectedDashboard: unstarredDashboard,
- },
- },
- ]);
- });
-
- it('unstars dashboard if it is starred', () => {
- state.selectedDashboard = starredDashboard;
- mock.onPost(starredDashboard.user_starred_path).reply(HTTP_STATUS_OK);
-
- return testAction(toggleStarredValue, null, state, [
- { type: types.REQUEST_DASHBOARD_STARRING },
- { type: types.RECEIVE_DASHBOARD_STARRING_FAILURE },
- ]);
- });
- });
-
- describe('duplicateSystemDashboard', () => {
- beforeEach(() => {
- state.dashboardsEndpoint = '/dashboards.json';
- });
-
- it('Succesful POST request resolves', async () => {
- mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_CREATED, {
- dashboard: dashboardGitResponse[1],
- });
-
- await testAction(duplicateSystemDashboard, {}, state, [], []);
- expect(mock.history.post).toHaveLength(1);
- });
-
- it('Succesful POST request resolves to a dashboard', async () => {
- const mockCreatedDashboard = dashboardGitResponse[1];
-
- const params = {
- dashboard: 'my-dashboard',
- fileName: 'file-name.yml',
- branch: 'my-new-branch',
- commitMessage: 'A new commit message',
- };
-
- const expectedPayload = JSON.stringify({
- dashboard: 'my-dashboard',
- file_name: 'file-name.yml',
- branch: 'my-new-branch',
- commit_message: 'A new commit message',
- });
-
- mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_CREATED, {
- dashboard: mockCreatedDashboard,
- });
-
- const result = await testAction(duplicateSystemDashboard, params, state, [], []);
- expect(mock.history.post).toHaveLength(1);
- expect(mock.history.post[0].data).toEqual(expectedPayload);
- expect(result).toEqual(mockCreatedDashboard);
- });
-
- it('Failed POST request throws an error', async () => {
- mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_BAD_REQUEST);
-
- await expect(testAction(duplicateSystemDashboard, {}, state, [], [])).rejects.toEqual(
- 'There was an error creating the dashboard.',
- );
- expect(mock.history.post).toHaveLength(1);
- });
-
- it('Failed POST request throws an error with a description', async () => {
- const backendErrorMsg = 'This file already exists!';
-
- mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_BAD_REQUEST, {
- error: backendErrorMsg,
- });
-
- await expect(testAction(duplicateSystemDashboard, {}, state, [], [])).rejects.toEqual(
- `There was an error creating the dashboard. ${backendErrorMsg}`,
- );
- expect(mock.history.post).toHaveLength(1);
- });
- });
-
- // Variables manipulation
-
- describe('updateVariablesAndFetchData', () => {
- it('should commit UPDATE_VARIABLE_VALUE mutation and fetch data', () => {
- return testAction(
- updateVariablesAndFetchData,
- { pod: 'POD' },
- state,
- [
- {
- type: types.UPDATE_VARIABLE_VALUE,
- payload: { pod: 'POD' },
- },
- ],
- [
- {
- type: 'fetchDashboardData',
- },
- ],
- );
- });
- });
-
- describe('fetchVariableMetricLabelValues', () => {
- const variable = {
- type: 'metric_label_values',
- name: 'label1',
- options: {
- prometheusEndpointPath: '/series?match[]=metric_name',
- label: 'job',
- },
- };
-
- const defaultQueryParams = {
- start_time: '2019-08-06T12:40:02.184Z',
- end_time: '2019-08-06T20:40:02.184Z',
- };
-
- beforeEach(() => {
- state = {
- ...state,
- timeRange: defaultTimeRange,
- variables: [variable],
- };
- });
-
- it('should commit UPDATE_VARIABLE_METRIC_LABEL_VALUES mutation and fetch data', () => {
- const data = [
- {
- __name__: 'up',
- job: 'prometheus',
- },
- {
- __name__: 'up',
- job: 'POD',
- },
- ];
-
- mock.onGet('/series?match[]=metric_name').reply(HTTP_STATUS_OK, {
- status: 'success',
- data,
- });
-
- return testAction(
- fetchVariableMetricLabelValues,
- { defaultQueryParams },
- state,
- [
- {
- type: types.UPDATE_VARIABLE_METRIC_LABEL_VALUES,
- payload: { variable, label: 'job', data },
- },
- ],
- [],
- );
- });
-
- it('should notify the user that dynamic options were not loaded', () => {
- mock.onGet('/series?match[]=metric_name').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- return testAction(fetchVariableMetricLabelValues, { defaultQueryParams }, state, [], []).then(
- () => {
- expect(createAlert).toHaveBeenCalledTimes(1);
- expect(createAlert).toHaveBeenCalledWith({
- message: expect.stringContaining('error getting options for variable "label1"'),
- });
- },
- );
- });
- });
-
- describe('fetchPanelPreview', () => {
- const panelPreviewEndpoint = '/builder.json';
- const mockYmlContent = 'mock yml content';
-
- beforeEach(() => {
- state.panelPreviewEndpoint = panelPreviewEndpoint;
- });
-
- it('should not commit or dispatch if payload is empty', () => {
- testAction(fetchPanelPreview, '', state, [], []);
- });
-
- it('should store the panel and fetch metric results', () => {
- const mockPanel = {
- title: 'Go heap size',
- type: 'area-chart',
- };
-
- mock
- .onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
- .reply(HTTP_STATUS_OK, mockPanel);
-
- testAction(
- fetchPanelPreview,
- mockYmlContent,
- state,
- [
- { type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
- { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
- { type: types.RECEIVE_PANEL_PREVIEW_SUCCESS, payload: mockPanel },
- ],
- [{ type: 'fetchPanelPreviewMetrics' }],
- );
- });
-
- it('should display a validation error when the backend cannot process the yml', () => {
- const mockErrorMsg = 'Each "metric" must define one of :query or :query_range';
-
- mock
- .onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
- .reply(HTTP_STATUS_UNPROCESSABLE_ENTITY, {
- message: mockErrorMsg,
- });
-
- testAction(fetchPanelPreview, mockYmlContent, state, [
- { type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
- { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
- { type: types.RECEIVE_PANEL_PREVIEW_FAILURE, payload: mockErrorMsg },
- ]);
- });
-
- it('should display a generic error when the backend fails', () => {
- mock
- .onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
- .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- testAction(fetchPanelPreview, mockYmlContent, state, [
- { type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
- { type: types.REQUEST_PANEL_PREVIEW, payload: mockYmlContent },
- {
- type: types.RECEIVE_PANEL_PREVIEW_FAILURE,
- payload: 'Request failed with status code 500',
- },
- ]);
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/embed_group/actions_spec.js b/spec/frontend/monitoring/store/embed_group/actions_spec.js
deleted file mode 100644
index 5bdfc506cff..00000000000
--- a/spec/frontend/monitoring/store/embed_group/actions_spec.js
+++ /dev/null
@@ -1,16 +0,0 @@
-// import store from '~/monitoring/stores/embed_group';
-import * as actions from '~/monitoring/stores/embed_group/actions';
-import * as types from '~/monitoring/stores/embed_group/mutation_types';
-import { mockNamespace } from '../../mock_data';
-
-describe('Embed group actions', () => {
- describe('addModule', () => {
- it('adds a module to the store', () => {
- const commit = jest.fn();
-
- actions.addModule({ commit }, mockNamespace);
-
- expect(commit).toHaveBeenCalledWith(types.ADD_MODULE, mockNamespace);
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/embed_group/getters_spec.js b/spec/frontend/monitoring/store/embed_group/getters_spec.js
deleted file mode 100644
index e3241e41f5e..00000000000
--- a/spec/frontend/monitoring/store/embed_group/getters_spec.js
+++ /dev/null
@@ -1,19 +0,0 @@
-import { metricsWithData } from '~/monitoring/stores/embed_group/getters';
-import { mockNamespaces } from '../../mock_data';
-
-describe('Embed group getters', () => {
- describe('metricsWithData', () => {
- it('correctly sums the number of metrics with data', () => {
- const mockMetric = {};
- const state = {
- modules: mockNamespaces,
- };
- const rootGetters = {
- [`${mockNamespaces[0]}/metricsWithData`]: () => [mockMetric],
- [`${mockNamespaces[1]}/metricsWithData`]: () => [mockMetric, mockMetric],
- };
-
- expect(metricsWithData(state, null, null, rootGetters)).toEqual([1, 2]);
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/embed_group/mutations_spec.js b/spec/frontend/monitoring/store/embed_group/mutations_spec.js
deleted file mode 100644
index 2f8d7687aad..00000000000
--- a/spec/frontend/monitoring/store/embed_group/mutations_spec.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import * as types from '~/monitoring/stores/embed_group/mutation_types';
-import mutations from '~/monitoring/stores/embed_group/mutations';
-import state from '~/monitoring/stores/embed_group/state';
-import { mockNamespace } from '../../mock_data';
-
-describe('Embed group mutations', () => {
- describe('ADD_MODULE', () => {
- it('should add a module', () => {
- const stateCopy = state();
-
- mutations[types.ADD_MODULE](stateCopy, mockNamespace);
-
- expect(stateCopy.modules).toEqual([mockNamespace]);
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
deleted file mode 100644
index c7f3bdbf1f8..00000000000
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ /dev/null
@@ -1,457 +0,0 @@
-import _ from 'lodash';
-import { metricStates } from '~/monitoring/constants';
-import * as getters from '~/monitoring/stores/getters';
-import * as types from '~/monitoring/stores/mutation_types';
-import mutations from '~/monitoring/stores/mutations';
-import { metricsDashboardPayload } from '../fixture_data';
-import {
- customDashboardBasePath,
- environmentData,
- metricsResult,
- dashboardGitResponse,
- storeVariables,
- mockLinks,
-} from '../mock_data';
-
-describe('Monitoring store Getters', () => {
- let state;
-
- const getMetric = ({ group = 0, panel = 0, metric = 0 } = {}) =>
- state.dashboard.panelGroups[group].panels[panel].metrics[metric];
-
- const setMetricSuccess = ({ group, panel, metric, result = metricsResult } = {}) => {
- const { metricId } = getMetric({ group, panel, metric });
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, {
- metricId,
- data: {
- resultType: 'matrix',
- result,
- },
- });
- };
-
- const setMetricFailure = ({ group, panel, metric } = {}) => {
- const { metricId } = getMetric({ group, panel, metric });
- mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
- metricId,
- });
- };
-
- describe('getMetricStates', () => {
- let setupState;
- let getMetricStates;
-
- beforeEach(() => {
- setupState = (initState = {}) => {
- state = initState;
- getMetricStates = getters.getMetricStates(state);
- };
- });
-
- it('has method-style access', () => {
- setupState();
-
- expect(getMetricStates).toEqual(expect.any(Function));
- });
-
- it('when dashboard has no panel groups, returns empty', () => {
- setupState({
- dashboard: {
- panelGroups: [],
- },
- });
-
- expect(getMetricStates()).toEqual([]);
- });
-
- describe('when the dashboard is set', () => {
- let groups;
- beforeEach(() => {
- setupState({
- dashboard: { panelGroups: [] },
- });
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- groups = state.dashboard.panelGroups;
- });
-
- it('no loaded metric returns empty', () => {
- expect(getMetricStates()).toEqual([]);
- });
-
- it('on an empty metric with no result, returns NO_DATA', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricSuccess({ group: 2, result: [] });
-
- expect(getMetricStates()).toEqual([metricStates.NO_DATA]);
- });
-
- it('on a metric with a result, returns OK', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricSuccess({ group: 1 });
-
- expect(getMetricStates()).toEqual([metricStates.OK]);
- });
-
- it('on a metric with an error, returns an error', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricFailure({});
-
- expect(getMetricStates()).toEqual([metricStates.UNKNOWN_ERROR]);
- });
-
- it('on multiple metrics with results, returns OK', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
-
- setMetricSuccess({ group: 1 });
- setMetricSuccess({ group: 1, panel: 1 });
-
- expect(getMetricStates()).toEqual([metricStates.OK]);
-
- // Filtered by groups
- expect(getMetricStates(state.dashboard.panelGroups[1].key)).toEqual([metricStates.OK]);
- expect(getMetricStates(state.dashboard.panelGroups[2].key)).toEqual([]);
- });
- it('on multiple metrics errors', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
-
- setMetricFailure({});
- setMetricFailure({ group: 1 });
-
- // Entire dashboard fails
- expect(getMetricStates()).toEqual([metricStates.UNKNOWN_ERROR]);
- expect(getMetricStates(groups[0].key)).toEqual([metricStates.UNKNOWN_ERROR]);
- expect(getMetricStates(groups[1].key)).toEqual([metricStates.UNKNOWN_ERROR]);
- });
-
- it('on multiple metrics with errors', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
-
- // An success in 1 group
- setMetricSuccess({ group: 1 });
-
- // An error in 2 groups
- setMetricFailure({ group: 1, panel: 1 });
- setMetricFailure({ group: 2, panel: 0 });
-
- expect(getMetricStates()).toEqual([metricStates.OK, metricStates.UNKNOWN_ERROR]);
- expect(getMetricStates(groups[1].key)).toEqual([
- metricStates.OK,
- metricStates.UNKNOWN_ERROR,
- ]);
- expect(getMetricStates(groups[2].key)).toEqual([metricStates.UNKNOWN_ERROR]);
- });
- });
- });
-
- describe('metricsWithData', () => {
- let metricsWithData;
- let setupState;
-
- beforeEach(() => {
- setupState = (initState = {}) => {
- state = initState;
- metricsWithData = getters.metricsWithData(state);
- };
- });
-
- afterEach(() => {
- state = null;
- });
-
- it('has method-style access', () => {
- setupState();
-
- expect(metricsWithData).toEqual(expect.any(Function));
- });
-
- it('when dashboard has no panel groups, returns empty', () => {
- setupState({
- dashboard: {
- panelGroups: [],
- },
- });
-
- expect(metricsWithData()).toEqual([]);
- });
-
- describe('when the dashboard is set', () => {
- beforeEach(() => {
- setupState({
- dashboard: { panelGroups: [] },
- });
- });
-
- it('no loaded metric returns empty', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
-
- expect(metricsWithData()).toEqual([]);
- });
-
- it('an empty metric, returns empty', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricSuccess({ result: [] });
-
- expect(metricsWithData()).toEqual([]);
- });
-
- it('a metric with results, it returns a metric', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricSuccess();
-
- expect(metricsWithData()).toEqual([getMetric().metricId]);
- });
-
- it('multiple metrics with results, it return multiple metrics', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricSuccess({ panel: 0 });
- setMetricSuccess({ panel: 1 });
-
- expect(metricsWithData()).toEqual([
- getMetric({ panel: 0 }).metricId,
- getMetric({ panel: 1 }).metricId,
- ]);
- });
-
- it('multiple metrics with results, it returns metrics filtered by group', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
-
- setMetricSuccess({ group: 1 });
- setMetricSuccess({ group: 1, panel: 1 });
-
- // First group has metrics
- expect(metricsWithData(state.dashboard.panelGroups[1].key)).toEqual([
- getMetric({ group: 1 }).metricId,
- getMetric({ group: 1, panel: 1 }).metricId,
- ]);
-
- // Second group has no metrics
- expect(metricsWithData(state.dashboard.panelGroups[2].key)).toEqual([]);
- });
- });
- });
-
- describe('filteredEnvironments', () => {
- const setupState = (initState = {}) => {
- state = {
- ...state,
- ...initState,
- };
- };
-
- beforeAll(() => {
- setupState({
- environments: environmentData,
- });
- });
-
- afterAll(() => {
- state = null;
- });
-
- [
- {
- input: '',
- output: 17,
- },
- {
- input: ' ',
- output: 17,
- },
- {
- input: null,
- output: 17,
- },
- {
- input: 'does-not-exist',
- output: 0,
- },
- {
- input: 'noop-branch-',
- output: 15,
- },
- {
- input: 'noop-branch-9',
- output: 1,
- },
- ].forEach(({ input, output }) => {
- it(`filteredEnvironments returns ${output} items for ${input}`, () => {
- setupState({
- environmentsSearchTerm: input,
- });
- expect(getters.filteredEnvironments(state).length).toBe(output);
- });
- });
- });
-
- describe('metricsSavedToDb', () => {
- let metricsSavedToDb;
- let mockData;
-
- beforeEach(() => {
- mockData = _.cloneDeep(metricsDashboardPayload);
- state = {
- dashboard: {
- panelGroups: [],
- },
- };
- });
-
- it('return no metrics when dashboard is not persisted', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, mockData);
- metricsSavedToDb = getters.metricsSavedToDb(state);
-
- expect(metricsSavedToDb).toEqual([]);
- });
-
- it('return a metric id when one metric is persisted', () => {
- const id = 99;
-
- const [metric] = mockData.panel_groups[0].panels[0].metrics;
-
- metric.metric_id = id;
-
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, mockData);
- metricsSavedToDb = getters.metricsSavedToDb(state);
-
- expect(metricsSavedToDb).toEqual([`${id}_${metric.id}`]);
- });
-
- it('return a metric id when two metrics are persisted', () => {
- const id1 = 101;
- const id2 = 102;
-
- const [metric1] = mockData.panel_groups[0].panels[0].metrics;
- const [metric2] = mockData.panel_groups[0].panels[1].metrics;
-
- // database persisted 2 metrics
- metric1.metric_id = id1;
- metric2.metric_id = id2;
-
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, mockData);
- metricsSavedToDb = getters.metricsSavedToDb(state);
-
- expect(metricsSavedToDb).toEqual([`${id1}_${metric1.id}`, `${id2}_${metric2.id}`]);
- });
- });
-
- describe('getCustomVariablesParams', () => {
- beforeEach(() => {
- state = {
- variables: {},
- };
- });
-
- it('transforms the variables object to an array in the [variable, variable_value] format for all variable types', () => {
- state.variables = storeVariables;
- const variablesArray = getters.getCustomVariablesParams(state);
-
- expect(variablesArray).toEqual({
- 'variables[textSimple]': 'My default value',
- 'variables[textAdvanced]': 'A default value',
- 'variables[customSimple]': 'value1',
- 'variables[customAdvanced]': 'value2',
- 'variables[customAdvancedWithoutLabel]': 'value2',
- 'variables[customAdvancedWithoutOptText]': 'value2',
- });
- });
-
- it('transforms the variables object to an empty array when no keys are present', () => {
- state.variables = [];
- const variablesArray = getters.getCustomVariablesParams(state);
-
- expect(variablesArray).toEqual({});
- });
- });
-
- describe('selectedDashboard', () => {
- const { selectedDashboard } = getters;
- const localGetters = (localState) => ({
- fullDashboardPath: getters.fullDashboardPath(localState),
- });
-
- it('returns a dashboard', () => {
- const localState = {
- allDashboards: dashboardGitResponse,
- currentDashboard: dashboardGitResponse[0].path,
- customDashboardBasePath,
- };
- expect(selectedDashboard(localState, localGetters(localState))).toEqual(
- dashboardGitResponse[0],
- );
- });
-
- it('returns a dashboard different from the overview dashboard', () => {
- const localState = {
- allDashboards: dashboardGitResponse,
- currentDashboard: dashboardGitResponse[1].path,
- customDashboardBasePath,
- };
- expect(selectedDashboard(localState, localGetters(localState))).toEqual(
- dashboardGitResponse[1],
- );
- });
-
- it('returns the overview dashboard when no dashboard is selected', () => {
- const localState = {
- allDashboards: dashboardGitResponse,
- currentDashboard: null,
- customDashboardBasePath,
- };
- expect(selectedDashboard(localState, localGetters(localState))).toEqual(
- dashboardGitResponse[0],
- );
- });
-
- it('returns the overview dashboard when dashboard cannot be found', () => {
- const localState = {
- allDashboards: dashboardGitResponse,
- currentDashboard: 'wrong_path',
- customDashboardBasePath,
- };
- expect(selectedDashboard(localState, localGetters(localState))).toEqual(
- dashboardGitResponse[0],
- );
- });
-
- it('returns null when no dashboards are present', () => {
- const localState = {
- allDashboards: [],
- currentDashboard: dashboardGitResponse[0].path,
- customDashboardBasePath,
- };
- expect(selectedDashboard(localState, localGetters(localState))).toEqual(null);
- });
- });
-
- describe('linksWithMetadata', () => {
- const setupState = (initState = {}) => {
- state = {
- ...state,
- ...initState,
- };
- };
-
- beforeAll(() => {
- setupState({
- links: mockLinks,
- });
- });
-
- afterAll(() => {
- state = null;
- });
-
- it.each`
- timeRange | output
- ${{}} | ${''}
- ${{ start: '2020-01-01T00:00:00.000Z', end: '2020-01-31T23:59:00.000Z' }} | ${'start=2020-01-01T00%3A00%3A00.000Z&end=2020-01-31T23%3A59%3A00.000Z'}
- ${{ duration: { seconds: 86400 } }} | ${'duration_seconds=86400'}
- `('linksWithMetadata returns URLs with time range', ({ timeRange, output }) => {
- setupState({ timeRange });
- const links = getters.linksWithMetadata(state);
- links.forEach(({ url }) => {
- expect(url).toMatch(output);
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/index_spec.js b/spec/frontend/monitoring/store/index_spec.js
deleted file mode 100644
index 4184687eec8..00000000000
--- a/spec/frontend/monitoring/store/index_spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import { createStore } from '~/monitoring/stores';
-
-describe('Monitoring Store Index', () => {
- it('creates store with a `monitoringDashboard` namespace', () => {
- expect(createStore().state).toEqual({
- monitoringDashboard: expect.any(Object),
- });
- });
-
- it('creates store with initial values', () => {
- const defaults = {
- deploymentsEndpoint: '/mock/deployments',
- dashboardEndpoint: '/mock/dashboard',
- dashboardsEndpoint: '/mock/dashboards',
- };
-
- const { state } = createStore(defaults);
-
- expect(state).toEqual({
- monitoringDashboard: expect.objectContaining(defaults),
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
deleted file mode 100644
index 3baef743f42..00000000000
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ /dev/null
@@ -1,586 +0,0 @@
-import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_SERVICE_UNAVAILABLE } from '~/lib/utils/http_status';
-import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
-import * as types from '~/monitoring/stores/mutation_types';
-import mutations from '~/monitoring/stores/mutations';
-import state from '~/monitoring/stores/state';
-import { metricsDashboardPayload } from '../fixture_data';
-import { prometheusMatrixMultiResult } from '../graph_data';
-import { deploymentData, dashboardGitResponse, storeTextVariables } from '../mock_data';
-
-describe('Monitoring mutations', () => {
- let stateCopy;
-
- beforeEach(() => {
- stateCopy = state();
- });
-
- describe('REQUEST_METRICS_DASHBOARD', () => {
- it('sets an empty loading state', () => {
- mutations[types.REQUEST_METRICS_DASHBOARD](stateCopy);
-
- expect(stateCopy.emptyState).toBe(dashboardEmptyStates.LOADING);
- });
- });
-
- describe('RECEIVE_METRICS_DASHBOARD_SUCCESS', () => {
- let payload;
- const getGroups = () => stateCopy.dashboard.panelGroups;
-
- beforeEach(() => {
- stateCopy.dashboard.panelGroups = [];
- payload = metricsDashboardPayload;
- });
- it('sets an empty noData state when the dashboard is empty', () => {
- const emptyDashboardPayload = {
- ...payload,
- panel_groups: [],
- };
-
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, emptyDashboardPayload);
- const groups = getGroups();
-
- expect(groups).toEqual([]);
- expect(stateCopy.emptyState).toBe(dashboardEmptyStates.NO_DATA);
- });
- it('adds a key to the group', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, payload);
- const groups = getGroups();
-
- expect(groups[0].key).toBe('system-metrics-kubernetes-0');
- expect(groups[1].key).toBe('response-metrics-nginx-ingress-vts-1');
- expect(groups[2].key).toBe('response-metrics-nginx-ingress-2');
- });
- it('normalizes values', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, payload);
- const expectedLabel = 'Pod average (MB)';
-
- const { label, queryRange } = getGroups()[0].panels[2].metrics[0];
- expect(label).toEqual(expectedLabel);
- expect(queryRange.length).toBeGreaterThan(0);
- });
- it('contains six groups, with panels with a metric each', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, payload);
-
- const groups = getGroups();
-
- expect(groups).toBeDefined();
- expect(groups).toHaveLength(6);
-
- expect(groups[0].panels).toHaveLength(7);
- expect(groups[0].panels[0].metrics).toHaveLength(1);
- expect(groups[0].panels[1].metrics).toHaveLength(1);
- expect(groups[0].panels[2].metrics).toHaveLength(1);
-
- expect(groups[1].panels).toHaveLength(3);
- expect(groups[1].panels[0].metrics).toHaveLength(1);
- });
- it('assigns metrics a metric id', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, payload);
-
- const groups = getGroups();
-
- expect(groups[0].panels[0].metrics[0].metricId).toEqual(
- 'NO_DB_system_metrics_kubernetes_container_memory_total',
- );
- expect(groups[1].panels[0].metrics[0].metricId).toEqual(
- 'NO_DB_response_metrics_nginx_ingress_throughput_status_code',
- );
- expect(groups[2].panels[0].metrics[0].metricId).toEqual(
- 'NO_DB_response_metrics_nginx_ingress_16_throughput_status_code',
- );
- });
- });
-
- describe('RECEIVE_METRICS_DASHBOARD_FAILURE', () => {
- it('sets an empty noData state when an empty error occurs', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_FAILURE](stateCopy);
-
- expect(stateCopy.emptyState).toBe(dashboardEmptyStates.NO_DATA);
- });
-
- it('sets an empty unableToConnect state when an error occurs', () => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_FAILURE](stateCopy, 'myerror');
-
- expect(stateCopy.emptyState).toBe(dashboardEmptyStates.UNABLE_TO_CONNECT);
- });
- });
-
- describe('Dashboard starring mutations', () => {
- it('REQUEST_DASHBOARD_STARRING', () => {
- stateCopy = { isUpdatingStarredValue: false };
- mutations[types.REQUEST_DASHBOARD_STARRING](stateCopy);
-
- expect(stateCopy.isUpdatingStarredValue).toBe(true);
- });
-
- describe('RECEIVE_DASHBOARD_STARRING_SUCCESS', () => {
- let allDashboards;
-
- beforeEach(() => {
- allDashboards = [...dashboardGitResponse];
- stateCopy = {
- allDashboards,
- currentDashboard: allDashboards[1].path,
- isUpdatingStarredValue: true,
- };
- });
-
- it('sets a dashboard as starred', () => {
- mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, {
- selectedDashboard: stateCopy.allDashboards[1],
- newStarredValue: true,
- });
-
- expect(stateCopy.isUpdatingStarredValue).toBe(false);
- expect(stateCopy.allDashboards[1].starred).toBe(true);
- });
-
- it('sets a dashboard as unstarred', () => {
- mutations[types.RECEIVE_DASHBOARD_STARRING_SUCCESS](stateCopy, {
- selectedDashboard: stateCopy.allDashboards[1],
- newStarredValue: false,
- });
-
- expect(stateCopy.isUpdatingStarredValue).toBe(false);
- expect(stateCopy.allDashboards[1].starred).toBe(false);
- });
- });
-
- it('RECEIVE_DASHBOARD_STARRING_FAILURE', () => {
- stateCopy = { isUpdatingStarredValue: true };
- mutations[types.RECEIVE_DASHBOARD_STARRING_FAILURE](stateCopy);
-
- expect(stateCopy.isUpdatingStarredValue).toBe(false);
- });
- });
-
- describe('RECEIVE_DEPLOYMENTS_DATA_SUCCESS', () => {
- it('stores the deployment data', () => {
- stateCopy.deploymentData = [];
- mutations[types.RECEIVE_DEPLOYMENTS_DATA_SUCCESS](stateCopy, deploymentData);
- expect(stateCopy.deploymentData).toBeDefined();
- expect(stateCopy.deploymentData).toHaveLength(3);
- expect(typeof stateCopy.deploymentData[0]).toEqual('object');
- });
- });
-
- describe('SET_INITIAL_STATE', () => {
- it('should set all the endpoints', () => {
- mutations[types.SET_INITIAL_STATE](stateCopy, {
- deploymentsEndpoint: 'deployments.json',
- dashboardEndpoint: 'dashboard.json',
- projectPath: '/gitlab-org/gitlab-foss',
- currentEnvironmentName: 'production',
- });
- expect(stateCopy.deploymentsEndpoint).toEqual('deployments.json');
- expect(stateCopy.dashboardEndpoint).toEqual('dashboard.json');
- expect(stateCopy.projectPath).toEqual('/gitlab-org/gitlab-foss');
- expect(stateCopy.currentEnvironmentName).toEqual('production');
- });
-
- it('should not remove previously set properties', () => {
- mutations[types.SET_INITIAL_STATE](stateCopy, {
- dashboardEndpoint: 'dashboard.json',
- });
- mutations[types.SET_INITIAL_STATE](stateCopy, {
- projectPath: '/gitlab-org/gitlab-foss',
- });
- mutations[types.SET_INITIAL_STATE](stateCopy, {
- currentEnvironmentName: 'canary',
- });
-
- expect(stateCopy).toMatchObject({
- dashboardEndpoint: 'dashboard.json',
- projectPath: '/gitlab-org/gitlab-foss',
- currentEnvironmentName: 'canary',
- });
- });
-
- it('should not update unknown properties', () => {
- mutations[types.SET_INITIAL_STATE](stateCopy, {
- dashboardEndpoint: 'dashboard.json',
- someOtherProperty: 'some invalid value', // someOtherProperty is not allowed
- });
-
- expect(stateCopy.dashboardEndpoint).toBe('dashboard.json');
- expect(stateCopy.someOtherProperty).toBeUndefined();
- });
- });
-
- describe('SET_ENDPOINTS', () => {
- it('should set all the endpoints', () => {
- mutations[types.SET_ENDPOINTS](stateCopy, {
- deploymentsEndpoint: 'deployments.json',
- dashboardEndpoint: 'dashboard.json',
- projectPath: '/gitlab-org/gitlab-foss',
- });
- expect(stateCopy.deploymentsEndpoint).toEqual('deployments.json');
- expect(stateCopy.dashboardEndpoint).toEqual('dashboard.json');
- expect(stateCopy.projectPath).toEqual('/gitlab-org/gitlab-foss');
- });
-
- it('should not remove previously set properties', () => {
- mutations[types.SET_ENDPOINTS](stateCopy, {
- dashboardEndpoint: 'dashboard.json',
- });
- mutations[types.SET_ENDPOINTS](stateCopy, {
- projectPath: '/gitlab-org/gitlab-foss',
- });
-
- expect(stateCopy).toMatchObject({
- dashboardEndpoint: 'dashboard.json',
- projectPath: '/gitlab-org/gitlab-foss',
- });
- });
-
- it('should not update unknown properties', () => {
- mutations[types.SET_ENDPOINTS](stateCopy, {
- dashboardEndpoint: 'dashboard.json',
- someOtherProperty: 'some invalid value', // someOtherProperty is not allowed
- });
-
- expect(stateCopy.dashboardEndpoint).toBe('dashboard.json');
- expect(stateCopy.someOtherProperty).toBeUndefined();
- });
- });
-
- describe('Individual panel/metric results', () => {
- const metricId = 'NO_DB_response_metrics_nginx_ingress_throughput_status_code';
-
- const dashboard = metricsDashboardPayload;
- const getMetric = () => stateCopy.dashboard.panelGroups[1].panels[0].metrics[0];
-
- describe('REQUEST_METRIC_RESULT', () => {
- beforeEach(() => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, dashboard);
- });
- it('stores a loading state on a metric', () => {
- mutations[types.REQUEST_METRIC_RESULT](stateCopy, {
- metricId,
- });
-
- expect(getMetric()).toEqual(
- expect.objectContaining({
- loading: true,
- }),
- );
- });
- });
-
- describe('RECEIVE_METRIC_RESULT_SUCCESS', () => {
- beforeEach(() => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, dashboard);
- });
-
- it('adds results to the store', () => {
- const data = prometheusMatrixMultiResult();
-
- expect(getMetric().result).toBe(null);
-
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](stateCopy, {
- metricId,
- data,
- });
-
- expect(getMetric().result).toHaveLength(data.result.length);
- expect(getMetric()).toEqual(
- expect.objectContaining({
- loading: false,
- state: metricStates.OK,
- }),
- );
- });
- });
-
- describe('RECEIVE_METRIC_RESULT_FAILURE', () => {
- beforeEach(() => {
- mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](stateCopy, dashboard);
- });
-
- it('stores a timeout error in a metric', () => {
- mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
- metricId,
- error: { message: 'BACKOFF_TIMEOUT' },
- });
-
- expect(getMetric()).toEqual(
- expect.objectContaining({
- loading: false,
- result: null,
- state: metricStates.TIMEOUT,
- }),
- );
- });
-
- it('stores a connection failed error in a metric', () => {
- mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
- metricId,
- error: {
- response: {
- status: HTTP_STATUS_SERVICE_UNAVAILABLE,
- },
- },
- });
- expect(getMetric()).toEqual(
- expect.objectContaining({
- loading: false,
- result: null,
- state: metricStates.CONNECTION_FAILED,
- }),
- );
- });
-
- it('stores a bad data error in a metric', () => {
- mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
- metricId,
- error: {
- response: {
- status: HTTP_STATUS_BAD_REQUEST,
- },
- },
- });
-
- expect(getMetric()).toEqual(
- expect.objectContaining({
- loading: false,
- result: null,
- state: metricStates.BAD_QUERY,
- }),
- );
- });
-
- it('stores an unknown error in a metric', () => {
- mutations[types.RECEIVE_METRIC_RESULT_FAILURE](stateCopy, {
- metricId,
- error: null, // no reason in response
- });
-
- expect(getMetric()).toEqual(
- expect.objectContaining({
- loading: false,
- result: null,
- state: metricStates.UNKNOWN_ERROR,
- }),
- );
- });
- });
- });
-
- describe('SET_ALL_DASHBOARDS', () => {
- it('stores `undefined` dashboards as an empty array', () => {
- mutations[types.SET_ALL_DASHBOARDS](stateCopy, undefined);
-
- expect(stateCopy.allDashboards).toEqual([]);
- });
-
- it('stores `null` dashboards as an empty array', () => {
- mutations[types.SET_ALL_DASHBOARDS](stateCopy, null);
-
- expect(stateCopy.allDashboards).toEqual([]);
- });
-
- it('stores dashboards loaded from the git repository', () => {
- mutations[types.SET_ALL_DASHBOARDS](stateCopy, dashboardGitResponse);
- expect(stateCopy.allDashboards).toEqual(dashboardGitResponse);
- });
- });
-
- describe('SET_EXPANDED_PANEL', () => {
- it('no expanded panel is set initally', () => {
- expect(stateCopy.expandedPanel.panel).toEqual(null);
- expect(stateCopy.expandedPanel.group).toEqual(null);
- });
-
- it('sets a panel id as the expanded panel', () => {
- const group = 'group_1';
- const panel = { title: 'A Panel' };
- mutations[types.SET_EXPANDED_PANEL](stateCopy, { group, panel });
-
- expect(stateCopy.expandedPanel).toEqual({ group, panel });
- });
-
- it('clears panel as the expanded panel', () => {
- mutations[types.SET_EXPANDED_PANEL](stateCopy, { group: null, panel: null });
-
- expect(stateCopy.expandedPanel.group).toEqual(null);
- expect(stateCopy.expandedPanel.panel).toEqual(null);
- });
- });
-
- describe('UPDATE_VARIABLE_VALUE', () => {
- it('updates only the value of the variable in variables', () => {
- stateCopy.variables = storeTextVariables;
- mutations[types.UPDATE_VARIABLE_VALUE](stateCopy, { name: 'textSimple', value: 'New Value' });
-
- expect(stateCopy.variables[0].value).toEqual('New Value');
- });
- });
-
- describe('UPDATE_VARIABLE_METRIC_LABEL_VALUES', () => {
- it('updates options in a variable', () => {
- const data = [
- {
- __name__: 'up',
- job: 'prometheus',
- env: 'prd',
- },
- {
- __name__: 'up',
- job: 'prometheus',
- env: 'stg',
- },
- {
- __name__: 'up',
- job: 'node',
- env: 'prod',
- },
- {
- __name__: 'up',
- job: 'node',
- env: 'stg',
- },
- ];
-
- const variable = {
- options: {},
- };
-
- mutations[types.UPDATE_VARIABLE_METRIC_LABEL_VALUES](stateCopy, {
- variable,
- label: 'job',
- data,
- });
-
- expect(variable.options).toEqual({
- values: [
- { text: 'prometheus', value: 'prometheus' },
- { text: 'node', value: 'node' },
- ],
- });
- });
- });
-
- describe('REQUEST_PANEL_PREVIEW', () => {
- it('saves yml content and resets other preview data', () => {
- const mockYmlContent = 'mock yml content';
- mutations[types.REQUEST_PANEL_PREVIEW](stateCopy, mockYmlContent);
-
- expect(stateCopy.panelPreviewIsLoading).toBe(true);
- expect(stateCopy.panelPreviewYml).toBe(mockYmlContent);
- expect(stateCopy.panelPreviewGraphData).toBe(null);
- expect(stateCopy.panelPreviewError).toBe(null);
- });
- });
-
- describe('RECEIVE_PANEL_PREVIEW_SUCCESS', () => {
- it('saves graph data', () => {
- mutations[types.RECEIVE_PANEL_PREVIEW_SUCCESS](stateCopy, {
- title: 'My Title',
- type: 'area-chart',
- });
-
- expect(stateCopy.panelPreviewIsLoading).toBe(false);
- expect(stateCopy.panelPreviewGraphData).toMatchObject({
- title: 'My Title',
- type: 'area-chart',
- });
- expect(stateCopy.panelPreviewError).toBe(null);
- });
- });
-
- describe('RECEIVE_PANEL_PREVIEW_FAILURE', () => {
- it('saves graph data', () => {
- mutations[types.RECEIVE_PANEL_PREVIEW_FAILURE](stateCopy, 'Error!');
-
- expect(stateCopy.panelPreviewIsLoading).toBe(false);
- expect(stateCopy.panelPreviewGraphData).toBe(null);
- expect(stateCopy.panelPreviewError).toBe('Error!');
- });
- });
-
- describe('panel preview metric', () => {
- const getPreviewMetricAt = (i) => stateCopy.panelPreviewGraphData.metrics[i];
-
- beforeEach(() => {
- stateCopy.panelPreviewGraphData = {
- title: 'Preview panel title',
- metrics: [
- {
- query: 'query',
- },
- ],
- };
- });
-
- describe('REQUEST_PANEL_PREVIEW_METRIC_RESULT', () => {
- it('sets the metric to loading for the first time', () => {
- mutations[types.REQUEST_PANEL_PREVIEW_METRIC_RESULT](stateCopy, { index: 0 });
-
- expect(getPreviewMetricAt(0).loading).toBe(true);
- expect(getPreviewMetricAt(0).state).toBe(metricStates.LOADING);
- });
-
- it('sets the metric to loading and keeps the result', () => {
- getPreviewMetricAt(0).result = [[0, 1]];
- getPreviewMetricAt(0).state = metricStates.OK;
-
- mutations[types.REQUEST_PANEL_PREVIEW_METRIC_RESULT](stateCopy, { index: 0 });
-
- expect(getPreviewMetricAt(0)).toMatchObject({
- loading: true,
- result: [[0, 1]],
- state: metricStates.OK,
- });
- });
- });
-
- describe('RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS', () => {
- it('saves the result in the metric', () => {
- const data = prometheusMatrixMultiResult();
-
- mutations[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_SUCCESS](stateCopy, {
- index: 0,
- data,
- });
-
- expect(getPreviewMetricAt(0)).toMatchObject({
- loading: false,
- state: metricStates.OK,
- result: expect.any(Array),
- });
- expect(getPreviewMetricAt(0).result).toHaveLength(data.result.length);
- });
- });
-
- describe('RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE', () => {
- it('stores an error in the metric', () => {
- mutations[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE](stateCopy, {
- index: 0,
- });
-
- expect(getPreviewMetricAt(0).loading).toBe(false);
- expect(getPreviewMetricAt(0).state).toBe(metricStates.UNKNOWN_ERROR);
- expect(getPreviewMetricAt(0).result).toBe(null);
-
- expect(getPreviewMetricAt(0)).toMatchObject({
- loading: false,
- result: null,
- state: metricStates.UNKNOWN_ERROR,
- });
- });
-
- it('stores a timeout error in a metric', () => {
- mutations[types.RECEIVE_PANEL_PREVIEW_METRIC_RESULT_FAILURE](stateCopy, {
- index: 0,
- error: { message: 'BACKOFF_TIMEOUT' },
- });
-
- expect(getPreviewMetricAt(0)).toMatchObject({
- loading: false,
- result: null,
- state: metricStates.TIMEOUT,
- });
- });
- });
- });
-});
diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js
deleted file mode 100644
index 54f9c59308e..00000000000
--- a/spec/frontend/monitoring/store/utils_spec.js
+++ /dev/null
@@ -1,893 +0,0 @@
-import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
-import * as urlUtils from '~/lib/utils/url_utility';
-import { NOT_IN_DB_PREFIX } from '~/monitoring/constants';
-import {
- uniqMetricsId,
- parseEnvironmentsResponse,
- parseAnnotationsResponse,
- removeLeadingSlash,
- mapToDashboardViewModel,
- normalizeQueryResponseData,
- convertToGrafanaTimeRange,
- addDashboardMetaDataToLink,
- normalizeCustomDashboardPath,
-} from '~/monitoring/stores/utils';
-import { annotationsData } from '../mock_data';
-
-const projectPath = 'gitlab-org/gitlab-test';
-
-describe('mapToDashboardViewModel', () => {
- it('maps an empty dashboard', () => {
- expect(mapToDashboardViewModel({})).toEqual({
- dashboard: '',
- panelGroups: [],
- links: [],
- variables: [],
- });
- });
-
- it('maps a simple dashboard', () => {
- const response = {
- dashboard: 'Dashboard Name',
- panel_groups: [
- {
- group: 'Group 1',
- panels: [
- {
- id: 'ID_ABC',
- title: 'Title A',
- xLabel: '',
- xAxis: {
- name: '',
- },
- type: 'chart-type',
- y_label: 'Y Label A',
- metrics: [],
- },
- ],
- },
- ],
- };
-
- expect(mapToDashboardViewModel(response)).toEqual({
- dashboard: 'Dashboard Name',
- links: [],
- variables: [],
- panelGroups: [
- {
- group: 'Group 1',
- key: 'group-1-0',
- panels: [
- {
- id: 'ID_ABC',
- title: 'Title A',
- type: 'chart-type',
- xLabel: '',
- xAxis: {
- name: '',
- },
- y_label: 'Y Label A',
- yAxis: {
- name: 'Y Label A',
- format: 'engineering',
- precision: 2,
- },
- links: [],
- metrics: [],
- },
- ],
- },
- ],
- });
- });
-
- describe('panel groups mapping', () => {
- it('key', () => {
- const response = {
- dashboard: 'Dashboard Name',
- links: [],
- variables: {},
- panel_groups: [
- {
- group: 'Group A',
- },
- {
- group: 'Group B',
- },
- {
- group: '',
- unsupported_property: 'This should be removed',
- },
- ],
- };
-
- expect(mapToDashboardViewModel(response).panelGroups).toEqual([
- {
- group: 'Group A',
- key: 'group-a-0',
- panels: [],
- },
- {
- group: 'Group B',
- key: 'group-b-1',
- panels: [],
- },
- {
- group: '',
- key: 'default-2',
- panels: [],
- },
- ]);
- });
- });
-
- describe('panel mapping', () => {
- const panelTitle = 'Panel Title';
- const yAxisName = 'Y Axis Name';
-
- let dashboard;
-
- const setupWithPanel = (panel) => {
- dashboard = {
- panel_groups: [
- {
- panels: [panel],
- },
- ],
- };
- };
-
- const getMappedPanel = () => mapToDashboardViewModel(dashboard).panelGroups[0].panels[0];
-
- it('panel with x_label', () => {
- setupWithPanel({
- id: 'ID_123',
- title: panelTitle,
- x_label: 'x label',
- });
-
- expect(getMappedPanel()).toEqual({
- id: 'ID_123',
- title: panelTitle,
- xLabel: 'x label',
- xAxis: {
- name: 'x label',
- },
- y_label: '',
- yAxis: {
- name: '',
- format: SUPPORTED_FORMATS.engineering,
- precision: 2,
- },
- links: [],
- metrics: [],
- });
- });
-
- it('group y_axis defaults', () => {
- setupWithPanel({
- id: 'ID_456',
- title: panelTitle,
- });
-
- expect(getMappedPanel()).toEqual({
- id: 'ID_456',
- title: panelTitle,
- xLabel: '',
- y_label: '',
- xAxis: {
- name: '',
- },
- yAxis: {
- name: '',
- format: SUPPORTED_FORMATS.engineering,
- precision: 2,
- },
- links: [],
- metrics: [],
- });
- });
-
- it('panel with y_axis.name', () => {
- setupWithPanel({
- y_axis: {
- name: yAxisName,
- },
- });
-
- expect(getMappedPanel().y_label).toBe(yAxisName);
- expect(getMappedPanel().yAxis.name).toBe(yAxisName);
- });
-
- it('panel with y_axis.name and y_label, displays y_axis.name', () => {
- setupWithPanel({
- y_label: 'Ignored Y Label',
- y_axis: {
- name: yAxisName,
- },
- });
-
- expect(getMappedPanel().y_label).toBe(yAxisName);
- expect(getMappedPanel().yAxis.name).toBe(yAxisName);
- });
-
- it('group y_label', () => {
- setupWithPanel({
- y_label: yAxisName,
- });
-
- expect(getMappedPanel().y_label).toBe(yAxisName);
- expect(getMappedPanel().yAxis.name).toBe(yAxisName);
- });
-
- it('group y_axis format and precision', () => {
- setupWithPanel({
- title: panelTitle,
- y_axis: {
- precision: 0,
- format: SUPPORTED_FORMATS.bytes,
- },
- });
-
- expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.bytes);
- expect(getMappedPanel().yAxis.precision).toBe(0);
- });
-
- it('group y_axis unsupported format defaults to number', () => {
- setupWithPanel({
- title: panelTitle,
- y_axis: {
- format: 'invalid_format',
- },
- });
-
- expect(getMappedPanel().yAxis.format).toBe(SUPPORTED_FORMATS.engineering);
- });
-
- // This property allows single_stat panels to render percentile values
- it('group maxValue', () => {
- setupWithPanel({
- max_value: 100,
- });
-
- expect(getMappedPanel().maxValue).toBe(100);
- });
-
- describe('panel with links', () => {
- const title = 'Example';
- const url = 'https://example.com';
-
- it('maps an empty link collection', () => {
- setupWithPanel({
- links: undefined,
- });
-
- expect(getMappedPanel().links).toEqual([]);
- });
-
- it('maps a link', () => {
- setupWithPanel({ links: [{ title, url }] });
-
- expect(getMappedPanel().links).toEqual([{ title, url }]);
- });
-
- it('maps a link without a title', () => {
- setupWithPanel({
- links: [{ url }],
- });
-
- expect(getMappedPanel().links).toEqual([{ title: url, url }]);
- });
-
- it('maps a link without a url', () => {
- setupWithPanel({
- links: [{ title }],
- });
-
- expect(getMappedPanel().links).toEqual([{ title, url: '#' }]);
- });
-
- it('maps a link without a url or title', () => {
- setupWithPanel({
- links: [{}],
- });
-
- expect(getMappedPanel().links).toEqual([{ title: 'null', url: '#' }]);
- });
-
- it('maps a link with an unsafe url safely', () => {
- // eslint-disable-next-line no-script-url
- const unsafeUrl = 'javascript:alert("XSS")';
-
- setupWithPanel({
- links: [
- {
- title,
- url: unsafeUrl,
- },
- ],
- });
-
- expect(getMappedPanel().links).toEqual([{ title, url: '#' }]);
- });
-
- it('maps multple links', () => {
- setupWithPanel({
- links: [{ title, url }, { url }, { title }],
- });
-
- expect(getMappedPanel().links).toEqual([
- { title, url },
- { title: url, url },
- { title, url: '#' },
- ]);
- });
- });
- });
-
- describe('metrics mapping', () => {
- const defaultLabel = 'Panel Label';
- const dashboardWithMetric = (metric, label = defaultLabel) => ({
- panel_groups: [
- {
- panels: [
- {
- y_label: label,
- metrics: [metric],
- },
- ],
- },
- ],
- });
-
- const getMappedMetric = (dashboard) => {
- return mapToDashboardViewModel(dashboard).panelGroups[0].panels[0].metrics[0];
- };
-
- it('creates a metric', () => {
- const dashboard = dashboardWithMetric({ label: 'Panel Label' });
-
- expect(getMappedMetric(dashboard)).toEqual({
- label: expect.any(String),
- metricId: expect.any(String),
- loading: false,
- result: null,
- state: null,
- });
- });
-
- it('creates a metric with a correct id', () => {
- const dashboard = dashboardWithMetric({
- id: 'http_responses',
- metric_id: 1,
- });
-
- expect(getMappedMetric(dashboard).metricId).toEqual('1_http_responses');
- });
-
- it('creates a metric without a default label', () => {
- const dashboard = dashboardWithMetric({});
-
- expect(getMappedMetric(dashboard)).toMatchObject({
- label: undefined,
- });
- });
-
- it('creates a metric with an endpoint and query', () => {
- const dashboard = dashboardWithMetric({
- prometheus_endpoint_path: 'http://test',
- query_range: 'http_responses',
- });
-
- expect(getMappedMetric(dashboard)).toMatchObject({
- prometheusEndpointPath: 'http://test',
- queryRange: 'http_responses',
- });
- });
-
- it('creates a metric with an ad-hoc property', () => {
- // This behavior is deprecated and should be removed
- // https://gitlab.com/gitlab-org/gitlab/issues/207198
-
- const dashboard = dashboardWithMetric({
- x_label: 'Another label',
- unkown_option: 'unkown_data',
- });
-
- expect(getMappedMetric(dashboard)).toMatchObject({
- x_label: 'Another label',
- unkown_option: 'unkown_data',
- });
- });
- });
-
- describe('templating variables mapping', () => {
- beforeEach(() => {
- jest.spyOn(urlUtils, 'queryToObject');
- });
-
- afterEach(() => {
- urlUtils.queryToObject.mockRestore();
- });
-
- it('sets variables as-is from yml file if URL has no variables', () => {
- const response = {
- dashboard: 'Dashboard Name',
- links: [],
- templating: {
- variables: {
- pod: 'kubernetes',
- pod_2: 'kubernetes-2',
- },
- },
- };
-
- urlUtils.queryToObject.mockReturnValueOnce();
-
- expect(mapToDashboardViewModel(response).variables).toEqual([
- {
- name: 'pod',
- label: 'pod',
- type: 'text',
- value: 'kubernetes',
- },
- {
- name: 'pod_2',
- label: 'pod_2',
- type: 'text',
- value: 'kubernetes-2',
- },
- ]);
- });
-
- it('sets variables as-is from yml file if URL has no matching variables', () => {
- const response = {
- dashboard: 'Dashboard Name',
- links: [],
- templating: {
- variables: {
- pod: 'kubernetes',
- pod_2: 'kubernetes-2',
- },
- },
- };
-
- urlUtils.queryToObject.mockReturnValueOnce({
- 'var-environment': 'POD',
- });
-
- expect(mapToDashboardViewModel(response).variables).toEqual([
- {
- label: 'pod',
- name: 'pod',
- type: 'text',
- value: 'kubernetes',
- },
- {
- label: 'pod_2',
- name: 'pod_2',
- type: 'text',
- value: 'kubernetes-2',
- },
- ]);
- });
-
- it('merges variables from URL with the ones from yml file', () => {
- const response = {
- dashboard: 'Dashboard Name',
- links: [],
- templating: {
- variables: {
- pod: 'kubernetes',
- pod_2: 'kubernetes-2',
- },
- },
- };
-
- urlUtils.queryToObject.mockReturnValueOnce({
- 'var-environment': 'POD',
- 'var-pod': 'POD1',
- 'var-pod_2': 'POD2',
- });
-
- expect(mapToDashboardViewModel(response).variables).toEqual([
- {
- label: 'pod',
- name: 'pod',
- type: 'text',
- value: 'POD1',
- },
- {
- label: 'pod_2',
- name: 'pod_2',
- type: 'text',
- value: 'POD2',
- },
- ]);
- });
- });
-});
-
-describe('uniqMetricsId', () => {
- [
- { input: { id: 1 }, expected: `${NOT_IN_DB_PREFIX}_1` },
- { input: { metricId: 2 }, expected: '2_undefined' },
- { input: { metricId: 2, id: 21 }, expected: '2_21' },
- { input: { metricId: 22, id: 1 }, expected: '22_1' },
- { input: { metricId: 'aaa', id: '_a' }, expected: 'aaa__a' },
- ].forEach(({ input, expected }) => {
- it(`creates unique metric ID with ${JSON.stringify(input)}`, () => {
- expect(uniqMetricsId(input)).toEqual(expected);
- });
- });
-});
-
-describe('parseEnvironmentsResponse', () => {
- [
- {
- input: null,
- output: [],
- },
- {
- input: undefined,
- output: [],
- },
- {
- input: [],
- output: [],
- },
- {
- input: [
- {
- id: '1',
- name: 'env-1',
- },
- ],
- output: [
- {
- id: 1,
- name: 'env-1',
- metrics_path: `${projectPath}/-/metrics?environment=1`,
- },
- ],
- },
- {
- input: [
- {
- id: 'gid://gitlab/Environment/12',
- name: 'env-12',
- },
- ],
- output: [
- {
- id: 12,
- name: 'env-12',
- metrics_path: `${projectPath}/-/metrics?environment=12`,
- },
- ],
- },
- ].forEach(({ input, output }) => {
- it(`parseEnvironmentsResponse returns ${JSON.stringify(output)} with input ${JSON.stringify(
- input,
- )}`, () => {
- expect(parseEnvironmentsResponse(input, projectPath)).toEqual(output);
- });
- });
-});
-
-describe('parseAnnotationsResponse', () => {
- const parsedAnnotationResponse = [
- {
- description: 'This is a test annotation',
- endingAt: null,
- id: 'gid://gitlab/Metrics::Dashboard::Annotation/1',
- panelId: null,
- startingAt: new Date('2020-04-12T12:51:53.000Z'),
- },
- ];
- it.each`
- case | input | expected
- ${'Returns empty array for null input'} | ${null} | ${[]}
- ${'Returns empty array for undefined input'} | ${undefined} | ${[]}
- ${'Returns empty array for empty input'} | ${[]} | ${[]}
- ${'Returns parsed responses for annotations data'} | ${[annotationsData[0]]} | ${parsedAnnotationResponse}
- `('$case', ({ input, expected }) => {
- expect(parseAnnotationsResponse(input)).toEqual(expected);
- });
-});
-
-describe('removeLeadingSlash', () => {
- [
- { input: null, output: '' },
- { input: '', output: '' },
- { input: 'gitlab-org', output: 'gitlab-org' },
- { input: 'gitlab-org/gitlab', output: 'gitlab-org/gitlab' },
- { input: '/gitlab-org/gitlab', output: 'gitlab-org/gitlab' },
- { input: '////gitlab-org/gitlab', output: 'gitlab-org/gitlab' },
- ].forEach(({ input, output }) => {
- it(`removeLeadingSlash returns ${output} with input ${input}`, () => {
- expect(removeLeadingSlash(input)).toEqual(output);
- });
- });
-});
-
-describe('user-defined links utils', () => {
- const mockRelativeTimeRange = {
- metricsDashboard: {
- duration: {
- seconds: 86400,
- },
- },
- grafana: {
- from: 'now-86400s',
- to: 'now',
- },
- };
- const mockAbsoluteTimeRange = {
- metricsDashboard: {
- start: '2020-06-08T16:13:01.995Z',
- end: '2020-06-08T21:12:32.243Z',
- },
- grafana: {
- from: 1591632781995,
- to: 1591650752243,
- },
- };
- describe('convertToGrafanaTimeRange', () => {
- it('converts relative timezone to grafana timezone', () => {
- expect(convertToGrafanaTimeRange(mockRelativeTimeRange.metricsDashboard)).toEqual(
- mockRelativeTimeRange.grafana,
- );
- });
-
- it('converts absolute timezone to grafana timezone', () => {
- expect(convertToGrafanaTimeRange(mockAbsoluteTimeRange.metricsDashboard)).toEqual(
- mockAbsoluteTimeRange.grafana,
- );
- });
- });
-
- describe('addDashboardMetaDataToLink', () => {
- const link = { title: 'title', url: 'https://gitlab.com' };
- const grafanaLink = { ...link, type: 'grafana' };
-
- it('adds relative time range to link w/o type for metrics dashboards', () => {
- const adder = addDashboardMetaDataToLink({
- timeRange: mockRelativeTimeRange.metricsDashboard,
- });
- expect(adder(link)).toMatchObject({
- title: 'title',
- url: 'https://gitlab.com?duration_seconds=86400',
- });
- });
-
- it('adds relative time range to Grafana type links', () => {
- const adder = addDashboardMetaDataToLink({
- timeRange: mockRelativeTimeRange.metricsDashboard,
- });
- expect(adder(grafanaLink)).toMatchObject({
- title: 'title',
- url: 'https://gitlab.com?from=now-86400s&to=now',
- });
- });
-
- it('adds absolute time range to link w/o type for metrics dashboard', () => {
- const adder = addDashboardMetaDataToLink({
- timeRange: mockAbsoluteTimeRange.metricsDashboard,
- });
- expect(adder(link)).toMatchObject({
- title: 'title',
- url:
- 'https://gitlab.com?start=2020-06-08T16%3A13%3A01.995Z&end=2020-06-08T21%3A12%3A32.243Z',
- });
- });
-
- it('adds absolute time range to Grafana type links', () => {
- const adder = addDashboardMetaDataToLink({
- timeRange: mockAbsoluteTimeRange.metricsDashboard,
- });
- expect(adder(grafanaLink)).toMatchObject({
- title: 'title',
- url: 'https://gitlab.com?from=1591632781995&to=1591650752243',
- });
- });
- });
-});
-
-describe('normalizeQueryResponseData', () => {
- // Data examples from
- // https://prometheus.io/docs/prometheus/latest/querying/api/#expression-queries
-
- it('processes a string result', () => {
- const mockScalar = {
- resultType: 'string',
- result: [1435781451.781, '1'],
- };
-
- expect(normalizeQueryResponseData(mockScalar)).toEqual([
- {
- metric: {},
- value: ['2015-07-01T20:10:51.781Z', '1'],
- values: [['2015-07-01T20:10:51.781Z', '1']],
- },
- ]);
- });
-
- it('processes a scalar result', () => {
- const mockScalar = {
- resultType: 'scalar',
- result: [1435781451.781, '1'],
- };
-
- expect(normalizeQueryResponseData(mockScalar)).toEqual([
- {
- metric: {},
- value: ['2015-07-01T20:10:51.781Z', 1],
- values: [['2015-07-01T20:10:51.781Z', 1]],
- },
- ]);
- });
-
- it('processes a vector result', () => {
- const mockVector = {
- resultType: 'vector',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- value: [1435781451.781, '1'],
- },
- {
- metric: {
- __name__: 'up',
- job: 'node',
- instance: 'localhost:9100',
- },
- value: [1435781451.781, '0'],
- },
- ],
- };
-
- expect(normalizeQueryResponseData(mockVector)).toEqual([
- {
- metric: { __name__: 'up', job: 'prometheus', instance: 'localhost:9090' },
- value: ['2015-07-01T20:10:51.781Z', 1],
- values: [['2015-07-01T20:10:51.781Z', 1]],
- },
- {
- metric: { __name__: 'up', job: 'node', instance: 'localhost:9100' },
- value: ['2015-07-01T20:10:51.781Z', 0],
- values: [['2015-07-01T20:10:51.781Z', 0]],
- },
- ]);
- });
-
- it('processes a matrix result', () => {
- const mockMatrix = {
- resultType: 'matrix',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- values: [
- [1435781430.781, '1'],
- [1435781445.781, '2'],
- [1435781460.781, '3'],
- ],
- },
- {
- metric: {
- __name__: 'up',
- job: 'node',
- instance: 'localhost:9091',
- },
- values: [
- [1435781430.781, '4'],
- [1435781445.781, '5'],
- [1435781460.781, '6'],
- ],
- },
- ],
- };
-
- expect(normalizeQueryResponseData(mockMatrix)).toEqual([
- {
- metric: { __name__: 'up', instance: 'localhost:9090', job: 'prometheus' },
- value: ['2015-07-01T20:11:00.781Z', 3],
- values: [
- ['2015-07-01T20:10:30.781Z', 1],
- ['2015-07-01T20:10:45.781Z', 2],
- ['2015-07-01T20:11:00.781Z', 3],
- ],
- },
- {
- metric: { __name__: 'up', instance: 'localhost:9091', job: 'node' },
- value: ['2015-07-01T20:11:00.781Z', 6],
- values: [
- ['2015-07-01T20:10:30.781Z', 4],
- ['2015-07-01T20:10:45.781Z', 5],
- ['2015-07-01T20:11:00.781Z', 6],
- ],
- },
- ]);
- });
-
- it('processes a scalar result with a NaN result', () => {
- // Queries may return "NaN" string values.
- // e.g. when Prometheus cannot find a metric the query
- // `scalar(does_not_exist)` will return a "NaN" value.
-
- const mockScalar = {
- resultType: 'scalar',
- result: [1435781451.781, 'NaN'],
- };
-
- expect(normalizeQueryResponseData(mockScalar)).toEqual([
- {
- metric: {},
- value: ['2015-07-01T20:10:51.781Z', NaN],
- values: [['2015-07-01T20:10:51.781Z', NaN]],
- },
- ]);
- });
-
- it('processes a matrix result with a "NaN" value', () => {
- // Queries may return "NaN" string values.
- const mockMatrix = {
- resultType: 'matrix',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- values: [
- [1435781430.781, '1'],
- [1435781460.781, 'NaN'],
- ],
- },
- ],
- };
-
- expect(normalizeQueryResponseData(mockMatrix)).toEqual([
- {
- metric: { __name__: 'up', instance: 'localhost:9090', job: 'prometheus' },
- value: ['2015-07-01T20:11:00.781Z', NaN],
- values: [
- ['2015-07-01T20:10:30.781Z', 1],
- ['2015-07-01T20:11:00.781Z', NaN],
- ],
- },
- ]);
- });
-});
-
-describe('normalizeCustomDashboardPath', () => {
- it.each`
- input | expected
- ${[undefined]} | ${''}
- ${[null]} | ${''}
- ${[]} | ${''}
- ${['links.yml']} | ${'links.yml'}
- ${['links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/links.yml'}
- ${['config/prometheus/common_metrics.yml']} | ${'config/prometheus/common_metrics.yml'}
- ${['config/prometheus/common_metrics.yml', '.gitlab/dashboards']} | ${'config/prometheus/common_metrics.yml'}
- ${['dir1/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/links.yml'}
- ${['dir1/dir2/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/dir2/links.yml'}
- ${['.gitlab/dashboards/links.yml']} | ${'.gitlab/dashboards/links.yml'}
- ${['.gitlab/dashboards/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/links.yml'}
- ${['.gitlab/dashboards/dir1/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/links.yml'}
- ${['.gitlab/dashboards/dir1/dir2/links.yml', '.gitlab/dashboards']} | ${'.gitlab/dashboards/dir1/dir2/links.yml'}
- ${['config/prometheus/pod_metrics.yml', '.gitlab/dashboards']} | ${'config/prometheus/pod_metrics.yml'}
- ${['config/prometheus/pod_metrics.yml']} | ${'config/prometheus/pod_metrics.yml'}
- `(`normalizeCustomDashboardPath returns $expected for $input`, ({ input, expected }) => {
- expect(normalizeCustomDashboardPath(...input)).toEqual(expected);
- });
-});
diff --git a/spec/frontend/monitoring/store/variable_mapping_spec.js b/spec/frontend/monitoring/store/variable_mapping_spec.js
deleted file mode 100644
index 58e7175c04c..00000000000
--- a/spec/frontend/monitoring/store/variable_mapping_spec.js
+++ /dev/null
@@ -1,209 +0,0 @@
-import * as urlUtils from '~/lib/utils/url_utility';
-import {
- parseTemplatingVariables,
- mergeURLVariables,
- optionsFromSeriesData,
-} from '~/monitoring/stores/variable_mapping';
-import {
- templatingVariablesExamples,
- storeTextVariables,
- storeCustomVariables,
- storeMetricLabelValuesVariables,
-} from '../mock_data';
-
-describe('Monitoring variable mapping', () => {
- describe('parseTemplatingVariables', () => {
- it.each`
- case | input
- ${'For undefined templating object'} | ${undefined}
- ${'For empty templating object'} | ${{}}
- `('$case, returns an empty array', ({ input }) => {
- expect(parseTemplatingVariables(input)).toEqual([]);
- });
-
- it.each`
- case | input | output
- ${'Returns parsed object for text variables'} | ${templatingVariablesExamples.text} | ${storeTextVariables}
- ${'Returns parsed object for custom variables'} | ${templatingVariablesExamples.custom} | ${storeCustomVariables}
- ${'Returns parsed object for metric label value variables'} | ${templatingVariablesExamples.metricLabelValues} | ${storeMetricLabelValuesVariables}
- `('$case, returns an empty array', ({ input, output }) => {
- expect(parseTemplatingVariables(input)).toEqual(output);
- });
- });
-
- describe('mergeURLVariables', () => {
- beforeEach(() => {
- jest.spyOn(urlUtils, 'queryToObject');
- });
-
- afterEach(() => {
- urlUtils.queryToObject.mockRestore();
- });
-
- it('returns empty object if variables are not defined in yml or URL', () => {
- urlUtils.queryToObject.mockReturnValueOnce({});
-
- expect(mergeURLVariables([])).toEqual([]);
- });
-
- it('returns empty object if variables are defined in URL but not in yml', () => {
- urlUtils.queryToObject.mockReturnValueOnce({
- 'var-env': 'one',
- 'var-instance': 'localhost',
- });
-
- expect(mergeURLVariables([])).toEqual([]);
- });
-
- it('returns yml variables if variables defined in yml but not in the URL', () => {
- urlUtils.queryToObject.mockReturnValueOnce({});
-
- const variables = [
- {
- name: 'env',
- value: 'one',
- },
- {
- name: 'instance',
- value: 'localhost',
- },
- ];
-
- expect(mergeURLVariables(variables)).toEqual(variables);
- });
-
- it('returns yml variables if variables defined in URL do not match with yml variables', () => {
- const urlParams = {
- 'var-env': 'one',
- 'var-instance': 'localhost',
- };
- const variables = [
- {
- name: 'env',
- value: 'one',
- },
- {
- name: 'service',
- value: 'database',
- },
- ];
- urlUtils.queryToObject.mockReturnValueOnce(urlParams);
-
- expect(mergeURLVariables(variables)).toEqual(variables);
- });
-
- it('returns merged yml and URL variables if there is some match', () => {
- const urlParams = {
- 'var-env': 'one',
- 'var-instance': 'localhost:8080',
- };
- const variables = [
- {
- name: 'instance',
- value: 'localhost',
- },
- {
- name: 'service',
- value: 'database',
- },
- ];
-
- urlUtils.queryToObject.mockReturnValueOnce(urlParams);
-
- expect(mergeURLVariables(variables)).toEqual([
- {
- name: 'instance',
- value: 'localhost:8080',
- },
- {
- name: 'service',
- value: 'database',
- },
- ]);
- });
- });
-
- describe('optionsFromSeriesData', () => {
- it('fetches the label values from missing data', () => {
- expect(optionsFromSeriesData({ label: 'job' })).toEqual([]);
- });
-
- it('fetches the label values from a simple series', () => {
- const data = [
- {
- __name__: 'up',
- job: 'job1',
- },
- {
- __name__: 'up',
- job: 'job2',
- },
- ];
-
- expect(optionsFromSeriesData({ label: 'job', data })).toEqual([
- { text: 'job1', value: 'job1' },
- { text: 'job2', value: 'job2' },
- ]);
- });
-
- it('fetches the label values from multiple series', () => {
- const data = [
- {
- __name__: 'up',
- job: 'job1',
- instance: 'host1',
- },
- {
- __name__: 'up',
- job: 'job2',
- instance: 'host1',
- },
- {
- __name__: 'up',
- job: 'job1',
- instance: 'host2',
- },
- {
- __name__: 'up',
- job: 'job2',
- instance: 'host2',
- },
- ];
-
- expect(optionsFromSeriesData({ label: '__name__', data })).toEqual([
- { text: 'up', value: 'up' },
- ]);
-
- expect(optionsFromSeriesData({ label: 'job', data })).toEqual([
- { text: 'job1', value: 'job1' },
- { text: 'job2', value: 'job2' },
- ]);
-
- expect(optionsFromSeriesData({ label: 'instance', data })).toEqual([
- { text: 'host1', value: 'host1' },
- { text: 'host2', value: 'host2' },
- ]);
- });
-
- it('fetches the label values from a series with missing values', () => {
- const data = [
- {
- __name__: 'up',
- job: 'job1',
- },
- {
- __name__: 'up',
- job: 'job2',
- },
- {
- __name__: 'up',
- },
- ];
-
- expect(optionsFromSeriesData({ label: 'job', data })).toEqual([
- { text: 'job1', value: 'job1' },
- { text: 'job2', value: 'job2' },
- ]);
- });
- });
-});
diff --git a/spec/frontend/monitoring/store_utils.js b/spec/frontend/monitoring/store_utils.js
deleted file mode 100644
index 96219661b9b..00000000000
--- a/spec/frontend/monitoring/store_utils.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import * as types from '~/monitoring/stores/mutation_types';
-import { metricsDashboardPayload } from './fixture_data';
-import { metricsResult, environmentData, dashboardGitResponse } from './mock_data';
-
-export const setMetricResult = ({ store, result, group = 0, panel = 0, metric = 0 }) => {
- const { dashboard } = store.state.monitoringDashboard;
- const { metricId } = dashboard.panelGroups[group].panels[panel].metrics[metric];
-
- store.commit(`monitoringDashboard/${types.RECEIVE_METRIC_RESULT_SUCCESS}`, {
- metricId,
- data: {
- resultType: 'matrix',
- result,
- },
- });
-};
-
-const setEnvironmentData = (store) => {
- store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData);
-};
-
-export const setupAllDashboards = (store, path) => {
- store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, dashboardGitResponse);
- if (path) {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: path,
- });
- }
-};
-
-export const setupStoreWithDashboard = (store) => {
- store.commit(
- `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
- metricsDashboardPayload,
- );
-};
-
-export const setupStoreWithLinks = (store) => {
- store.commit(`monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`, {
- ...metricsDashboardPayload,
- links: [
- {
- title: 'GitLab Website',
- url: `https://gitlab.com/website`,
- },
- ],
- });
-};
-
-export const setupStoreWithData = (store) => {
- setupAllDashboards(store);
- setupStoreWithDashboard(store);
-
- setMetricResult({ store, result: [], panel: 0 });
- setMetricResult({ store, result: metricsResult, panel: 1 });
- setMetricResult({ store, result: metricsResult, panel: 2 });
-
- setEnvironmentData(store);
-};
-
-export const setupStoreWithDataForPanelCount = (store, panelCount) => {
- const payloadPanelGroup = metricsDashboardPayload.panel_groups[0];
-
- const panelGroupCustom = {
- ...payloadPanelGroup,
- panels: payloadPanelGroup.panels.slice(0, panelCount),
- };
-
- const metricsDashboardPayloadCustom = {
- ...metricsDashboardPayload,
- panel_groups: [panelGroupCustom],
- };
-
- store.commit(
- `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
- metricsDashboardPayloadCustom,
- );
-
- setMetricResult({ store, result: metricsResult, panel: 0 });
-};
diff --git a/spec/frontend/monitoring/stubs/modal_stub.js b/spec/frontend/monitoring/stubs/modal_stub.js
deleted file mode 100644
index 4cd0362096e..00000000000
--- a/spec/frontend/monitoring/stubs/modal_stub.js
+++ /dev/null
@@ -1,11 +0,0 @@
-const ModalStub = {
- name: 'glmodal-stub',
- template: `
- <div>
- <slot></slot>
- <slot name="modal-ok"></slot>
- </div>
- `,
-};
-
-export default ModalStub;
diff --git a/spec/frontend/monitoring/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
deleted file mode 100644
index 348825c334a..00000000000
--- a/spec/frontend/monitoring/utils_spec.js
+++ /dev/null
@@ -1,464 +0,0 @@
-import { TEST_HOST } from 'helpers/test_constants';
-import * as urlUtils from '~/lib/utils/url_utility';
-import * as monitoringUtils from '~/monitoring/utils';
-import { metricsDashboardViewModel, graphData } from './fixture_data';
-import { singleStatGraphData, anomalyGraphData } from './graph_data';
-import { mockProjectDir, barMockData } from './mock_data';
-
-const mockPath = `${TEST_HOST}${mockProjectDir}/-/environments/29/metrics`;
-
-const generatedLink = 'http://chart.link.com';
-
-const chartTitle = 'Some metric chart';
-
-const range = {
- start: '2019-01-01T00:00:00.000Z',
- end: '2019-01-10T00:00:00.000Z',
-};
-
-const rollingRange = {
- duration: { seconds: 120 },
-};
-
-describe('monitoring/utils', () => {
- describe('trackGenerateLinkToChartEventOptions', () => {
- it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
- document.body.dataset.page = 'groups:clusters:show';
-
- expect(monitoringUtils.generateLinkToChartOptions(generatedLink)).toEqual({
- category: 'Cluster Monitoring',
- action: 'generate_link_to_cluster_metric_chart',
- label: 'Chart link',
- property: generatedLink,
- });
- });
-
- it('should return Incident Management event options if located on Metrics Dashboard', () => {
- document.body.dataset.page = 'metrics:show';
-
- expect(monitoringUtils.generateLinkToChartOptions(generatedLink)).toEqual({
- category: 'Incident Management::Embedded metrics',
- action: 'generate_link_to_metrics_chart',
- label: 'Chart link',
- property: generatedLink,
- });
- });
- });
-
- describe('trackDownloadCSVEvent', () => {
- it('should return Cluster Monitoring options if located on Cluster Health Dashboard', () => {
- document.body.dataset.page = 'groups:clusters:show';
-
- expect(monitoringUtils.downloadCSVOptions(chartTitle)).toEqual({
- category: 'Cluster Monitoring',
- action: 'download_csv_of_cluster_metric_chart',
- label: 'Chart title',
- property: chartTitle,
- });
- });
-
- it('should return Incident Management event options if located on Metrics Dashboard', () => {
- document.body.dataset.page = 'metriss:show';
-
- expect(monitoringUtils.downloadCSVOptions(chartTitle)).toEqual({
- category: 'Incident Management::Embedded metrics',
- action: 'download_csv_of_metrics_dashboard_chart',
- label: 'Chart title',
- property: chartTitle,
- });
- });
- });
-
- describe('graphDataValidatorForValues', () => {
- /*
- * When dealing with a metric using the query format, e.g.
- * query: 'max(go_memstats_alloc_bytes{job="prometheus"}) by (job) /1024/1024'
- * the validator will look for the `value` key instead of `values`
- */
- it('validates data with the query format', () => {
- const validGraphData = monitoringUtils.graphDataValidatorForValues(
- true,
- singleStatGraphData(),
- );
-
- expect(validGraphData).toBe(true);
- });
-
- /*
- * When dealing with a metric using the query?range format, e.g.
- * query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) /1024/1024/1024',
- * the validator will look for the `values` key instead of `value`
- */
- it('validates data with the query_range format', () => {
- const validGraphData = monitoringUtils.graphDataValidatorForValues(false, graphData);
-
- expect(validGraphData).toBe(true);
- });
- });
-
- describe('graphDataValidatorForAnomalyValues', () => {
- let oneMetric;
- let threeMetrics;
- let fourMetrics;
- beforeEach(() => {
- oneMetric = singleStatGraphData();
- threeMetrics = anomalyGraphData();
-
- const metrics = [...threeMetrics.metrics];
- metrics.push(threeMetrics.metrics[0]);
- fourMetrics = {
- ...anomalyGraphData(),
- metrics,
- };
- });
- /*
- * Anomaly charts can accept results for exactly 3 metrics,
- */
- it('validates passes with the right query format', () => {
- expect(monitoringUtils.graphDataValidatorForAnomalyValues(threeMetrics)).toBe(true);
- });
-
- it('validation fails for wrong format, 1 metric', () => {
- expect(monitoringUtils.graphDataValidatorForAnomalyValues(oneMetric)).toBe(false);
- });
-
- it('validation fails for wrong format, more than 3 metrics', () => {
- expect(monitoringUtils.graphDataValidatorForAnomalyValues(fourMetrics)).toBe(false);
- });
- });
-
- describe('timeRangeFromUrl', () => {
- beforeEach(() => {
- jest.spyOn(urlUtils, 'queryToObject');
- });
-
- afterEach(() => {
- urlUtils.queryToObject.mockRestore();
- });
-
- const { timeRangeFromUrl } = monitoringUtils;
-
- it('returns a fixed range when query contains `start` and `end` parameters are given', () => {
- urlUtils.queryToObject.mockReturnValueOnce(range);
- expect(timeRangeFromUrl()).toEqual(range);
- });
-
- it('returns a rolling range when query contains `duration_seconds` parameters are given', () => {
- const { seconds } = rollingRange.duration;
-
- urlUtils.queryToObject.mockReturnValueOnce({
- dashboard: '.gitlab/dashboard/my_dashboard.yml',
- duration_seconds: `${seconds}`,
- });
-
- expect(timeRangeFromUrl()).toEqual(rollingRange);
- });
-
- it('returns null when no time range parameters are given', () => {
- urlUtils.queryToObject.mockReturnValueOnce({
- dashboard: '.gitlab/dashboards/custom_dashboard.yml',
- param1: 'value1',
- param2: 'value2',
- });
-
- expect(timeRangeFromUrl()).toBe(null);
- });
- });
-
- describe('templatingVariablesFromUrl', () => {
- const { templatingVariablesFromUrl } = monitoringUtils;
-
- beforeEach(() => {
- jest.spyOn(urlUtils, 'queryToObject');
- });
-
- afterEach(() => {
- urlUtils.queryToObject.mockRestore();
- });
-
- it('returns an object with only the custom variables', () => {
- urlUtils.queryToObject.mockReturnValueOnce({
- dashboard: '.gitlab/dashboards/custom_dashboard.yml',
- y_label: 'memory usage',
- group: 'kubernetes',
- title: 'Kubernetes memory total',
- start: '2020-05-06',
- end: '2020-05-07',
- duration_seconds: '86400',
- direction: 'left',
- anchor: 'top',
- pod: 'POD',
- 'var-pod': 'POD',
- });
-
- expect(templatingVariablesFromUrl()).toEqual(expect.objectContaining({ pod: 'POD' }));
- });
-
- it('returns an empty object when no custom variables are present', () => {
- urlUtils.queryToObject.mockReturnValueOnce({
- dashboard: '.gitlab/dashboards/custom_dashboard.yml',
- });
-
- expect(templatingVariablesFromUrl()).toStrictEqual({});
- });
- });
-
- describe('removeTimeRangeParams', () => {
- const { removeTimeRangeParams } = monitoringUtils;
-
- it('returns when query contains `start` and `end` parameters are given', () => {
- expect(removeTimeRangeParams(`${mockPath}?start=${range.start}&end=${range.end}`)).toEqual(
- mockPath,
- );
- });
- });
-
- describe('timeRangeToUrl', () => {
- const { timeRangeToUrl } = monitoringUtils;
-
- beforeEach(() => {
- jest.spyOn(urlUtils, 'mergeUrlParams');
- jest.spyOn(urlUtils, 'removeParams');
- });
-
- afterEach(() => {
- urlUtils.mergeUrlParams.mockRestore();
- urlUtils.removeParams.mockRestore();
- });
-
- it('returns a fixed range when query contains `start` and `end` parameters are given', () => {
- const toUrl = `${mockPath}?start=${range.start}&end=${range.end}`;
- const fromUrl = mockPath;
-
- urlUtils.removeParams.mockReturnValueOnce(fromUrl);
- urlUtils.mergeUrlParams.mockReturnValueOnce(toUrl);
-
- expect(timeRangeToUrl(range)).toEqual(toUrl);
- expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(range, fromUrl);
- });
-
- it('returns a rolling range when query contains `duration_seconds` parameters are given', () => {
- const { seconds } = rollingRange.duration;
-
- const toUrl = `${mockPath}?duration_seconds=${seconds}`;
- const fromUrl = mockPath;
-
- urlUtils.removeParams.mockReturnValueOnce(fromUrl);
- urlUtils.mergeUrlParams.mockReturnValueOnce(toUrl);
-
- expect(timeRangeToUrl(rollingRange)).toEqual(toUrl);
- expect(urlUtils.mergeUrlParams).toHaveBeenCalledWith(
- { duration_seconds: `${seconds}` },
- fromUrl,
- );
- });
- });
-
- describe('expandedPanelPayloadFromUrl', () => {
- const { expandedPanelPayloadFromUrl } = monitoringUtils;
- const [panelGroup] = metricsDashboardViewModel.panelGroups;
- const [panel] = panelGroup.panels;
-
- const { group } = panelGroup;
- const { title, y_label: yLabel } = panel;
-
- it('returns payload for a panel when query parameters are given', () => {
- const search = `?group=${group}&title=${title}&y_label=${yLabel}`;
-
- expect(expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toEqual({
- group: panelGroup.group,
- panel,
- });
- });
-
- it('returns null when no parameters are given', () => {
- expect(expandedPanelPayloadFromUrl(metricsDashboardViewModel, '')).toBe(null);
- });
-
- it('throws an error when no group is provided', () => {
- const search = `?title=${panel.title}&y_label=${yLabel}`;
- expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
- });
-
- it('throws an error when no title is provided', () => {
- const search = `?title=${title}&y_label=${yLabel}`;
- expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
- });
-
- it('throws an error when no y_label group is provided', () => {
- const search = `?group=${group}&title=${title}`;
- expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
- });
-
- it.each`
- group | title | yLabel | missingField
- ${'NOT_A_GROUP'} | ${title} | ${yLabel} | ${'group'}
- ${group} | ${'NOT_A_TITLE'} | ${yLabel} | ${'title'}
- ${group} | ${title} | ${'NOT_A_Y_LABEL'} | ${'y_label'}
- `('throws an error when $missingField is incorrect', (params) => {
- const search = `?group=${params.group}&title=${params.title}&y_label=${params.yLabel}`;
- expect(() => expandedPanelPayloadFromUrl(metricsDashboardViewModel, search)).toThrow();
- });
- });
-
- describe('panelToUrl', () => {
- const { panelToUrl } = monitoringUtils;
-
- const dashboard = 'metrics.yml';
- const [panelGroup] = metricsDashboardViewModel.panelGroups;
- const [panel] = panelGroup.panels;
-
- const getUrlParams = (url) => urlUtils.queryToObject(url.split('?')[1]);
-
- it('returns URL for a panel when query parameters are given', () => {
- const params = getUrlParams(panelToUrl(dashboard, {}, panelGroup.group, panel));
-
- expect(params).toEqual(
- expect.objectContaining({
- dashboard,
- group: panelGroup.group,
- title: panel.title,
- y_label: panel.y_label,
- }),
- );
- });
-
- it('returns a dashboard only URL if group is missing', () => {
- const params = getUrlParams(panelToUrl(dashboard, {}, null, panel));
- expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml' }));
- });
-
- it('returns a dashboard only URL if panel is missing', () => {
- const params = getUrlParams(panelToUrl(dashboard, {}, panelGroup.group, null));
- expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml' }));
- });
-
- it('returns URL for a panel when query paramters are given including custom variables', () => {
- const params = getUrlParams(panelToUrl(dashboard, { pod: 'pod' }, panelGroup.group, null));
- expect(params).toEqual(expect.objectContaining({ dashboard: 'metrics.yml', pod: 'pod' }));
- });
- });
-
- describe('barChartsDataParser', () => {
- const singleMetricExpected = {
- SLA: [
- ['0.9935198135198128', 'api'],
- ['0.9975296513504401', 'git'],
- ['0.9994716394716395', 'registry'],
- ['0.9948251748251747', 'sidekiq'],
- ['0.9535664335664336', 'web'],
- ['0.9335664335664336', 'postgresql_database'],
- ],
- };
-
- const multipleMetricExpected = {
- ...singleMetricExpected,
- SLA_2: Object.values(singleMetricExpected)[0],
- };
-
- const barMockDataWithMultipleMetrics = {
- ...barMockData,
- metrics: [
- barMockData.metrics[0],
- {
- ...barMockData.metrics[0],
- label: 'SLA_2',
- },
- ],
- };
-
- it.each([
- {
- input: { metrics: undefined },
- output: {},
- testCase: 'barChartsDataParser returns {} with undefined',
- },
- {
- input: { metrics: null },
- output: {},
- testCase: 'barChartsDataParser returns {} with null',
- },
- {
- input: { metrics: [] },
- output: {},
- testCase: 'barChartsDataParser returns {} with []',
- },
- {
- input: barMockData,
- output: singleMetricExpected,
- testCase: 'barChartsDataParser returns single series object with single metrics',
- },
- {
- input: barMockDataWithMultipleMetrics,
- output: multipleMetricExpected,
- testCase: 'barChartsDataParser returns multiple series object with multiple metrics',
- },
- ])('$testCase', ({ input, output }) => {
- expect(monitoringUtils.barChartsDataParser(input.metrics)).toEqual(
- expect.objectContaining(output),
- );
- });
- });
-
- describe('removePrefixFromLabel', () => {
- it.each`
- input | expected
- ${undefined} | ${''}
- ${null} | ${''}
- ${''} | ${''}
- ${' '} | ${' '}
- ${'pod-1'} | ${'pod-1'}
- ${'pod-var-1'} | ${'pod-var-1'}
- ${'pod-1-var'} | ${'pod-1-var'}
- ${'podvar--1'} | ${'podvar--1'}
- ${'povar-d-1'} | ${'povar-d-1'}
- ${'var-pod-1'} | ${'pod-1'}
- ${'var-var-pod-1'} | ${'var-pod-1'}
- ${'varvar-pod-1'} | ${'varvar-pod-1'}
- ${'var-pod-1-var-'} | ${'pod-1-var-'}
- `('removePrefixFromLabel returns $expected with input $input', ({ input, expected }) => {
- expect(monitoringUtils.removePrefixFromLabel(input)).toEqual(expected);
- });
- });
-
- describe('convertVariablesForURL', () => {
- it.each`
- input | expected
- ${[]} | ${{}}
- ${[{ name: 'env', value: 'prod' }]} | ${{ 'var-env': 'prod' }}
- ${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${{ 'var-env1': 'prod' }}
- ${[{ name: 'var-env', value: 'prod' }]} | ${{ 'var-var-env': 'prod' }}
- `('convertVariablesForURL returns $expected with input $input', ({ input, expected }) => {
- expect(monitoringUtils.convertVariablesForURL(input)).toEqual(expected);
- });
- });
-
- describe('setCustomVariablesFromUrl', () => {
- beforeEach(() => {
- window.history.pushState = jest.fn();
- jest.spyOn(urlUtils, 'updateHistory');
- });
-
- afterEach(() => {
- urlUtils.updateHistory.mockRestore();
- });
-
- it.each`
- input | urlParams
- ${[]} | ${''}
- ${[{ name: 'env', value: 'prod' }]} | ${'?var-env=prod'}
- ${[{ name: 'env1', value: 'prod' }, { name: 'env2', value: null }]} | ${'?var-env1=prod'}
- `(
- 'setCustomVariablesFromUrl updates history with query "$urlParams" with input $input',
- ({ input, urlParams }) => {
- monitoringUtils.setCustomVariablesFromUrl(input);
-
- expect(urlUtils.updateHistory).toHaveBeenCalledTimes(1);
- expect(urlUtils.updateHistory).toHaveBeenCalledWith({
- url: `${TEST_HOST}/${urlParams}`,
- title: '',
- });
- },
- );
- });
-});
diff --git a/spec/frontend/monitoring/validators_spec.js b/spec/frontend/monitoring/validators_spec.js
deleted file mode 100644
index 0c3d77a7d98..00000000000
--- a/spec/frontend/monitoring/validators_spec.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import { alertsValidator, queriesValidator } from '~/monitoring/validators';
-
-describe('alertsValidator', () => {
- const validAlert = {
- alert_path: 'my/alert.json',
- operator: '<',
- threshold: 5,
- metricId: '8',
- };
- it('requires all alerts to have an alert path', () => {
- const { operator, threshold, metricId } = validAlert;
- const input = {
- [validAlert.alert_path]: {
- operator,
- threshold,
- metricId,
- },
- };
- expect(alertsValidator(input)).toEqual(false);
- });
- it('requires that the object key matches the alert path', () => {
- const input = {
- undefined: validAlert,
- };
- expect(alertsValidator(input)).toEqual(false);
- });
- it('requires all alerts to have a metric id', () => {
- const input = {
- [validAlert.alert_path]: { ...validAlert, metricId: undefined },
- };
- expect(alertsValidator(input)).toEqual(false);
- });
- it('requires the metricId to be a string', () => {
- const input = {
- [validAlert.alert_path]: { ...validAlert, metricId: 8 },
- };
- expect(alertsValidator(input)).toEqual(false);
- });
- it('requires all alerts to have an operator', () => {
- const input = {
- [validAlert.alert_path]: { ...validAlert, operator: '' },
- };
- expect(alertsValidator(input)).toEqual(false);
- });
- it('requires all alerts to have an numeric threshold', () => {
- const input = {
- [validAlert.alert_path]: { ...validAlert, threshold: '60' },
- };
- expect(alertsValidator(input)).toEqual(false);
- });
- it('correctly identifies a valid alerts object', () => {
- const input = {
- [validAlert.alert_path]: validAlert,
- };
- expect(alertsValidator(input)).toEqual(true);
- });
-});
-describe('queriesValidator', () => {
- const validQuery = {
- metricId: '8',
- alert_path: 'alert',
- label: 'alert-label',
- };
- it('requires all alerts to have a metric id', () => {
- const input = [{ ...validQuery, metricId: undefined }];
- expect(queriesValidator(input)).toEqual(false);
- });
- it('requires the metricId to be a string', () => {
- const input = [{ ...validQuery, metricId: 8 }];
- expect(queriesValidator(input)).toEqual(false);
- });
- it('requires all queries to have a label', () => {
- const input = [{ ...validQuery, label: undefined }];
- expect(queriesValidator(input)).toEqual(false);
- });
- it('correctly identifies a valid queries array', () => {
- const input = [validQuery];
- expect(queriesValidator(input)).toEqual(true);
- });
-});
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 6c774a1ecd0..a6d88bdd310 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -20,6 +20,7 @@ import eventHub from '~/notes/event_hub';
import { COMMENT_FORM } from '~/notes/i18n';
import notesModule from '~/notes/stores/modules';
import { sprintf } from '~/locale';
+import { mockTracking } from 'helpers/tracking_helper';
import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
jest.mock('autosize');
@@ -31,6 +32,7 @@ Vue.use(Vuex);
describe('issue_comment_form component', () => {
useLocalStorageSpy();
+ let trackingSpy;
let store;
let wrapper;
let axiosMock;
@@ -121,6 +123,15 @@ describe('issue_comment_form component', () => {
provide: {
glFeatures: features,
},
+ mocks: {
+ $apollo: {
+ queries: {
+ currentUser: {
+ loading: false,
+ },
+ },
+ },
+ },
}),
);
};
@@ -128,6 +139,7 @@ describe('issue_comment_form component', () => {
beforeEach(() => {
axiosMock = new MockAdapter(axios);
store = createStore();
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
});
afterEach(() => {
@@ -150,6 +162,21 @@ describe('issue_comment_form component', () => {
expect(wrapper.vm.stopPolling).toHaveBeenCalled();
});
+ it('tracks event', () => {
+ mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
+
+ jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
+ jest.spyOn(wrapper.vm, 'stopPolling');
+
+ findCloseReopenButton().trigger('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context: 'Issue_comment',
+ editorType: 'editor_type_plain_text_editor',
+ label: 'editor_tracking',
+ });
+ });
+
it('does not report errors in the UI when the save succeeds', async () => {
mountComponent({ mountFunction: mount, initialData: { note: '/label ~sdfghj' } });
@@ -294,13 +321,13 @@ describe('issue_comment_form component', () => {
it('hides content editor switcher if feature flag content_editor_on_issues is off', () => {
mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: false } });
- expect(wrapper.text()).not.toContain('Switch to rich text');
+ expect(wrapper.text()).not.toContain('Switch to rich text editing');
});
it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: true } });
- expect(wrapper.text()).toContain('Switch to rich text');
+ expect(wrapper.text()).toContain('Switch to rich text editing');
});
describe('textarea', () => {
@@ -327,9 +354,8 @@ describe('issue_comment_form component', () => {
jest.spyOn(wrapper.vm, 'stopPolling');
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ note: 'hello world' });
+ findMarkdownEditor().vm.$emit('input', 'hello world');
+ await nextTick();
await findCommentButton().trigger('click');
@@ -347,15 +373,7 @@ describe('issue_comment_form component', () => {
const { markdownDocsPath } = notesDataMock;
- expect(wrapper.find(`a[href="${markdownDocsPath}"]`).text()).toBe('Markdown');
- });
-
- it('should link to quick actions docs', () => {
- mountComponent({ mountFunction: mount });
-
- const { quickActionsDocsPath } = notesDataMock;
-
- expect(wrapper.find(`a[href="${quickActionsDocsPath}"]`).text()).toBe('quick actions');
+ expect(wrapper.find(`[href="${markdownDocsPath}"]`).exists()).toBe(true);
});
it('should resize textarea after note discarded', async () => {
@@ -459,9 +477,8 @@ describe('issue_comment_form component', () => {
it('should enable comment button if it has note', async () => {
mountComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- await wrapper.setData({ note: 'Foo' });
+ findMarkdownEditor().vm.$emit('input', 'Foo');
+ await nextTick();
expect(findCommentTypeDropdown().props('disabled')).toBe(false);
});
diff --git a/spec/frontend/notes/components/comment_type_dropdown_spec.js b/spec/frontend/notes/components/comment_type_dropdown_spec.js
index b891c1f553d..053542a421c 100644
--- a/spec/frontend/notes/components/comment_type_dropdown_spec.js
+++ b/spec/frontend/notes/components/comment_type_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlButton, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue';
@@ -8,9 +8,9 @@ import { COMMENT_FORM } from '~/notes/i18n';
describe('CommentTypeDropdown component', () => {
let wrapper;
- const findCommentGlDropdown = () => wrapper.findComponent(GlDropdown);
- const findCommentDropdownOption = () => wrapper.findAllComponents(GlDropdownItem).at(0);
- const findDiscussionDropdownOption = () => wrapper.findAllComponents(GlDropdownItem).at(1);
+ const findCommentButton = () => wrapper.findComponent(GlButton);
+ const findCommentListboxOption = () => wrapper.findAllComponents(GlListboxItem).at(0);
+ const findDiscussionListboxOption = () => wrapper.findAllComponents(GlListboxItem).at(1);
const mountComponent = ({ props = {} } = {}) => {
wrapper = extendedWrapper(
@@ -20,6 +20,10 @@ describe('CommentTypeDropdown component', () => {
noteType: constants.COMMENT,
...props,
},
+ stubs: {
+ GlCollapsibleListbox,
+ GlListboxItem,
+ },
}),
);
};
@@ -33,15 +37,15 @@ describe('CommentTypeDropdown component', () => {
({ isInternalNote, buttonText }) => {
mountComponent({ props: { noteType: constants.COMMENT, isInternalNote } });
- expect(findCommentGlDropdown().props()).toMatchObject({ text: buttonText });
+ expect(findCommentButton().text()).toBe(buttonText);
},
);
it('Should set correct dropdown item checked when comment is selected', () => {
mountComponent({ props: { noteType: constants.COMMENT } });
- expect(findCommentDropdownOption().props()).toMatchObject({ isChecked: true });
- expect(findDiscussionDropdownOption().props()).toMatchObject({ isChecked: false });
+ expect(findCommentListboxOption().props('isSelected')).toBe(true);
+ expect(findDiscussionListboxOption().props('isSelected')).toBe(false);
});
it.each`
@@ -53,32 +57,22 @@ describe('CommentTypeDropdown component', () => {
({ isInternalNote, buttonText }) => {
mountComponent({ props: { noteType: constants.DISCUSSION, isInternalNote } });
- expect(findCommentGlDropdown().props()).toMatchObject({ text: buttonText });
+ expect(findCommentButton().text()).toBe(buttonText);
},
);
it('Should set correct dropdown item option checked when discussion is selected', () => {
mountComponent({ props: { noteType: constants.DISCUSSION } });
- expect(findCommentDropdownOption().props()).toMatchObject({ isChecked: false });
- expect(findDiscussionDropdownOption().props()).toMatchObject({ isChecked: true });
+ expect(findCommentListboxOption().props('isSelected')).toBe(false);
+ expect(findDiscussionListboxOption().props('isSelected')).toBe(true);
});
it('Should emit `change` event when clicking on an alternate dropdown option', () => {
mountComponent({ props: { noteType: constants.DISCUSSION } });
- const event = {
- type: 'click',
- stopPropagation: jest.fn(),
- preventDefault: jest.fn(),
- };
-
- findCommentDropdownOption().vm.$emit('click', event);
- findDiscussionDropdownOption().vm.$emit('click', event);
-
- // ensure the native events don't trigger anything
- expect(event.stopPropagation).toHaveBeenCalledTimes(2);
- expect(event.preventDefault).toHaveBeenCalledTimes(2);
+ findCommentListboxOption().trigger('click');
+ findDiscussionListboxOption().trigger('click');
expect(wrapper.emitted('change')[0]).toEqual([constants.COMMENT]);
expect(wrapper.emitted('change').length).toEqual(1);
@@ -87,7 +81,7 @@ describe('CommentTypeDropdown component', () => {
it('Should emit `click` event when clicking on the action button', () => {
mountComponent({ props: { noteType: constants.DISCUSSION } });
- findCommentGlDropdown().vm.$emit('click');
+ findCommentButton().vm.$emit('click');
expect(wrapper.emitted('click').length > 0).toBe(true);
});
diff --git a/spec/frontend/notes/components/diff_discussion_header_spec.js b/spec/frontend/notes/components/diff_discussion_header_spec.js
index 66b86ed3ce0..123d53de3f3 100644
--- a/spec/frontend/notes/components/diff_discussion_header_spec.js
+++ b/spec/frontend/notes/components/diff_discussion_header_spec.js
@@ -12,14 +12,21 @@ describe('diff_discussion_header component', () => {
let store;
let wrapper;
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = shallowMount(diffDiscussionHeader, {
+ store,
+ propsData: {
+ discussion: discussionMock,
+ ...propsData,
+ },
+ });
+ };
+
beforeEach(() => {
window.mrTabs = {};
store = createStore();
- wrapper = shallowMount(diffDiscussionHeader, {
- store,
- propsData: { discussion: discussionMock },
- });
+ createComponent({ propsData: { discussion: discussionMock } });
});
describe('Avatar', () => {
@@ -27,19 +34,23 @@ describe('diff_discussion_header component', () => {
const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
const findAvatar = () => wrapper.findComponent(GlAvatar);
- it('should render user avatar and user avatar link', () => {
+ it('should render user avatar and user avatar link with popover support', () => {
expect(findAvatar().exists()).toBe(true);
- expect(findAvatarLink().exists()).toBe(true);
+
+ const avatarLink = findAvatarLink();
+ expect(avatarLink.exists()).toBe(true);
+ expect(avatarLink.classes()).toContain('js-user-link');
+ expect(avatarLink.attributes()).toMatchObject({
+ href: firstNoteAuthor.path,
+ 'data-user-id': `${firstNoteAuthor.id}`,
+ 'data-username': `${firstNoteAuthor.username}`,
+ });
});
it('renders avatar of the first note author', () => {
- const props = findAvatar().props();
-
- expect(props).toMatchObject({
- src: firstNoteAuthor.avatar_url,
- alt: firstNoteAuthor.name,
- size: 32,
- });
+ expect(findAvatar().props('src')).toBe(firstNoteAuthor.avatar_url);
+ expect(findAvatar().props('alt')).toBe(firstNoteAuthor.name);
+ expect(findAvatar().props('size')).toBe(32);
});
});
@@ -53,14 +64,16 @@ describe('diff_discussion_header component', () => {
projectPath: 'something',
};
- wrapper.setProps({
- discussion: {
- ...discussionMock,
- for_commit: true,
- commit_id: commitId,
- diff_discussion: true,
- diff_file: {
- ...mockDiffFile,
+ createComponent({
+ propsData: {
+ discussion: {
+ ...discussionMock,
+ for_commit: true,
+ commit_id: commitId,
+ diff_discussion: true,
+ diff_file: {
+ ...mockDiffFile,
+ },
},
},
});
@@ -71,9 +84,15 @@ describe('diff_discussion_header component', () => {
describe('for diff threads without a commit id', () => {
it('should show started a thread on the diff text', async () => {
- Object.assign(wrapper.vm.discussion, {
- for_commit: false,
- commit_id: null,
+ createComponent({
+ propsData: {
+ discussion: {
+ ...discussionMock,
+ diff_discussion: true,
+ for_commit: false,
+ commit_id: null,
+ },
+ },
});
await nextTick();
@@ -81,10 +100,16 @@ describe('diff_discussion_header component', () => {
});
it('should show thread on older version text', async () => {
- Object.assign(wrapper.vm.discussion, {
- for_commit: false,
- commit_id: null,
- active: false,
+ createComponent({
+ propsData: {
+ discussion: {
+ ...discussionMock,
+ diff_discussion: true,
+ for_commit: false,
+ commit_id: null,
+ active: false,
+ },
+ },
});
await nextTick();
@@ -102,7 +127,16 @@ describe('diff_discussion_header component', () => {
describe('for diff thread with a commit id', () => {
it('should display started thread on commit header', async () => {
- wrapper.vm.discussion.for_commit = false;
+ createComponent({
+ propsData: {
+ discussion: {
+ ...discussionMock,
+ diff_discussion: true,
+ for_commit: false,
+ commit_id: commitId,
+ },
+ },
+ });
await nextTick();
expect(wrapper.text()).toContain(`started a thread on commit ${truncatedCommitId}`);
@@ -111,8 +145,17 @@ describe('diff_discussion_header component', () => {
});
it('should display outdated change on commit header', async () => {
- wrapper.vm.discussion.for_commit = false;
- wrapper.vm.discussion.active = false;
+ createComponent({
+ propsData: {
+ discussion: {
+ ...discussionMock,
+ diff_discussion: true,
+ for_commit: false,
+ commit_id: commitId,
+ active: false,
+ },
+ },
+ });
await nextTick();
expect(wrapper.text()).toContain(
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index ac677841ee1..e52dd87f784 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -1,11 +1,11 @@
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } 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';
import notesModule from '~/notes/stores/modules';
import * as types from '~/notes/stores/mutation_types';
-import { noteableDataMock, discussionMock, notesDataMock, userDataMock } from '../mock_data';
+import { discussionMock, noteableDataMock, notesDataMock, userDataMock } from '../mock_data';
describe('DiscussionCounter component', () => {
let store;
@@ -101,9 +101,24 @@ describe('DiscussionCounter component', () => {
`('renders correctly if $title', async ({ resolved, groupLength }) => {
updateStore({ resolvable: true, resolved });
wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
- await wrapper.find('.dropdown-toggle').trigger('click');
+ await wrapper.findComponent(GlDisclosureDropdown).trigger('click');
- expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(groupLength);
+ expect(wrapper.findAllComponents(GlDisclosureDropdownItem)).toHaveLength(groupLength);
+ });
+
+ describe('resolve all with new issue link', () => {
+ it('has correct href prop', async () => {
+ updateStore({ resolvable: true });
+ wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
+
+ const resolveDiscussionsPath =
+ store.getters.getNoteableData.create_issue_to_resolve_discussions_path;
+
+ await wrapper.findComponent(GlDisclosureDropdown).trigger('click');
+ const resolveAllLink = wrapper.find('[data-testid="resolve-all-with-issue-link"]');
+
+ expect(resolveAllLink.attributes('href')).toBe(resolveDiscussionsPath);
+ });
});
});
@@ -114,7 +129,7 @@ describe('DiscussionCounter component', () => {
store.commit(types.ADD_OR_UPDATE_DISCUSSIONS, [discussion]);
store.dispatch('updateResolvableDiscussionsCounts');
wrapper = mount(DiscussionCounter, { store, propsData: { blocksMerge: true } });
- await wrapper.find('.dropdown-toggle').trigger('click');
+ await wrapper.findComponent(GlDisclosureDropdown).trigger('click');
toggleAllButton = wrapper.find('[data-testid="toggle-all-discussions-btn"]');
};
diff --git a/spec/frontend/notes/components/mr_discussion_filter_spec.js b/spec/frontend/notes/components/mr_discussion_filter_spec.js
index beb25c30af6..2bb47fd3c9e 100644
--- a/spec/frontend/notes/components/mr_discussion_filter_spec.js
+++ b/spec/frontend/notes/components/mr_discussion_filter_spec.js
@@ -67,7 +67,7 @@ describe('Merge request discussion filter component', () => {
it('lists current filters', () => {
createComponent();
- expect(wrapper.findAllComponents(GlListboxItem).length).toBe(MR_FILTER_OPTIONS.length);
+ expect(wrapper.findAllComponents(GlListboxItem)).toHaveLength(MR_FILTER_OPTIONS.length);
});
it('updates store when selecting filter', async () => {
@@ -107,4 +107,30 @@ describe('Merge request discussion filter component', () => {
expect(wrapper.findComponent(GlButton).text()).toBe(expectedText);
});
+
+ it('when clicking de-select it de-selects all options', async () => {
+ createComponent();
+
+ wrapper.find('[data-testid="listbox-reset-button"]').vm.$emit('click');
+
+ await nextTick();
+
+ expect(wrapper.findAll('[aria-selected="true"]')).toHaveLength(0);
+ });
+
+ it('when clicking select all it selects all options', async () => {
+ createComponent();
+
+ wrapper.find('[data-testid="listbox-item-approval"]').vm.$emit('select', false);
+
+ await nextTick();
+
+ expect(wrapper.findAll('[aria-selected="true"]')).toHaveLength(9);
+
+ wrapper.find('[data-testid="listbox-select-all-button"]').vm.$emit('click');
+
+ await nextTick();
+
+ expect(wrapper.findAll('[aria-selected="true"]')).toHaveLength(10);
+ });
});
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index b5b33607282..645aef21e38 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -7,6 +7,7 @@ import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import { AT_WHO_ACTIVE_CLASS } from '~/gfm_auto_complete';
import eventHub from '~/environments/event_hub';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking } from 'helpers/tracking_helper';
import { noteableDataMock, notesDataMock, discussionMock, note } from '../mock_data';
jest.mock('~/lib/utils/autosave');
@@ -15,6 +16,7 @@ describe('issue_note_form component', () => {
let store;
let wrapper;
let props;
+ let trackingSpy;
const createComponentWrapper = (propsData = {}, provide = {}) => {
wrapper = mountExtended(NoteForm, {
@@ -26,6 +28,15 @@ describe('issue_note_form component', () => {
provide: {
glFeatures: provide,
},
+ mocks: {
+ $apollo: {
+ queries: {
+ currentUser: {
+ loading: false,
+ },
+ },
+ },
+ },
});
};
@@ -43,6 +54,7 @@ describe('issue_note_form component', () => {
noteBody: 'Magni suscipit eius consectetur enim et ex et commodi.',
noteId: '545',
};
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
});
describe('noteHash', () => {
@@ -66,13 +78,13 @@ describe('issue_note_form component', () => {
it('hides content editor switcher if feature flag content_editor_on_issues is off', () => {
createComponentWrapper({}, { contentEditorOnIssues: false });
- expect(wrapper.text()).not.toContain('Switch to rich text');
+ expect(wrapper.text()).not.toContain('Switch to rich text editing');
});
it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
createComponentWrapper({}, { contentEditorOnIssues: true });
- expect(wrapper.text()).toContain('Switch to rich text');
+ expect(wrapper.text()).toContain('Switch to rich text editing');
});
describe('conflicts editing', () => {
@@ -213,6 +225,21 @@ describe('issue_note_form component', () => {
expect(wrapper.emitted('handleFormUpdate')).toHaveLength(1);
});
+
+ it('tracks event when save button is clicked', () => {
+ createComponentWrapper();
+
+ const textarea = wrapper.find('textarea');
+ textarea.setValue('Foo');
+ const saveButton = wrapper.find('.js-vue-issue-save');
+ saveButton.vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context: 'Issue_note',
+ editorType: 'editor_type_plain_text_editor',
+ label: 'editor_tracking',
+ });
+ });
});
});
@@ -271,7 +298,9 @@ describe('issue_note_form component', () => {
await nextTick();
- expect(wrapper.emitted('handleFormUpdateAddToReview')).toEqual([['Foo', false]]);
+ expect(wrapper.emitted('handleFormUpdateAddToReview')).toStrictEqual([
+ ['Foo', false, wrapper.vm.$refs.editNoteForm, expect.any(Function)],
+ ]);
});
});
});
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index d50fb130a69..059972df56b 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -1,6 +1,6 @@
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { GlAvatar } from '@gitlab/ui';
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
import { clone } from 'lodash';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -218,6 +218,18 @@ describe('issue_note', () => {
});
});
+ it('should render user avatar link with popover support', () => {
+ const { author } = note;
+ const avatarLink = wrapper.findComponent(GlAvatarLink);
+
+ expect(avatarLink.classes()).toContain('js-user-link');
+ expect(avatarLink.attributes()).toMatchObject({
+ href: author.path,
+ 'data-user-id': `${author.id}`,
+ 'data-username': `${author.username}`,
+ });
+ });
+
it('should render user avatar', () => {
const { author } = note;
const avatar = wrapper.findComponent(GlAvatar);
@@ -373,10 +385,24 @@ describe('issue_note', () => {
afterEach(() => updateNote.mockReset());
- it('responds to handleFormUpdate', () => {
+ it('emits handleUpdateNote', () => {
+ const updatedNote = { ...note, note_html: `<p dir="auto">${params.noteText}</p>\n` };
+
findNoteBody().vm.$emit('handleFormUpdate', params);
expect(wrapper.emitted('handleUpdateNote')).toHaveLength(1);
+
+ expect(wrapper.emitted('handleUpdateNote')[0]).toEqual([
+ {
+ note: updatedNote,
+ noteText: params.noteText,
+ resolveDiscussion: params.resolveDiscussion,
+ position: {},
+ flashContainer: wrapper.vm.$el,
+ callback: expect.any(Function),
+ errorCallback: expect.any(Function),
+ },
+ ]);
});
it('updates note content', async () => {
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index 0f70b264326..caf47febedd 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -122,7 +122,7 @@ describe('note_app', () => {
);
});
- // https://gitlab.com/gitlab-org/gitlab/-/issues/410409
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/410409
// eslint-disable-next-line jest/no-disabled-tests
it.skip('should render form comment button as disabled', () => {
expect(findCommentButton().props('disabled')).toEqual(true);
@@ -250,15 +250,7 @@ describe('note_app', () => {
it('should render markdown docs url', () => {
const { markdownDocsPath } = mockData.notesDataMock;
- expect(wrapper.find(`a[href="${markdownDocsPath}"]`).text().trim()).toEqual('Markdown');
- });
-
- it('should render quick action docs url', () => {
- const { quickActionsDocsPath } = mockData.notesDataMock;
-
- expect(wrapper.find(`a[href="${quickActionsDocsPath}"]`).text().trim()).toEqual(
- 'quick actions',
- );
+ expect(wrapper.find(`a[href="${markdownDocsPath}"]`).exists()).toBe(true);
});
});
@@ -274,19 +266,7 @@ describe('note_app', () => {
const { markdownDocsPath } = mockData.notesDataMock;
await nextTick();
- expect(wrapper.find(`.edit-note a[href="${markdownDocsPath}"]`).text().trim()).toEqual(
- 'Markdown',
- );
- });
-
- it('should render quick actions docs url', async () => {
- wrapper.find('.js-note-edit').trigger('click');
- const { quickActionsDocsPath } = mockData.notesDataMock;
-
- await nextTick();
- expect(wrapper.find(`.edit-note a[href="${quickActionsDocsPath}"]`).text().trim()).toEqual(
- 'quick actions',
- );
+ expect(wrapper.find(`.edit-note a[href="${markdownDocsPath}"]`).exists()).toBe(true);
});
});
diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index 355ecb78187..0e0af3f0480 100644
--- a/spec/frontend/notes/deprecated_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -32,8 +32,7 @@ function wrappedDiscussionNote(note) {
return `<table><tbody>${note}</tbody></table>`;
}
-// the following test is unreliable and failing in main 2-3 times a day
-// see https://gitlab.com/gitlab-org/gitlab/issues/206906#note_290602581
+// quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/208441
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('Old Notes (~/deprecated_notes.js)', () => {
beforeEach(() => {
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index b6a2b318ec3..bef8ed8e659 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -74,7 +74,6 @@ describe('Discussion navigation mixin', () => {
});
afterEach(() => {
- jest.clearAllMocks();
resetHTMLFixture();
});
diff --git a/spec/frontend/notes/mock_data.js b/spec/frontend/notes/mock_data.js
index d5b7ad73177..94549c4a73b 100644
--- a/spec/frontend/notes/mock_data.js
+++ b/spec/frontend/notes/mock_data.js
@@ -60,7 +60,7 @@ export const noteableDataMock = {
updated_at: '2017-08-04T09:53:01.226Z',
updated_by_id: 1,
web_url: '/gitlab-org/gitlab-foss/issues/26',
- noteableType: 'issue',
+ noteableType: 'Issue',
blocked_by_issues: [],
};
diff --git a/spec/frontend/notes/utils_spec.js b/spec/frontend/notes/utils_spec.js
index 0882e0a5759..3607c3c546c 100644
--- a/spec/frontend/notes/utils_spec.js
+++ b/spec/frontend/notes/utils_spec.js
@@ -1,12 +1,12 @@
import { sprintf } from '~/locale';
-import { getErrorMessages } from '~/notes/utils';
+import { createNoteErrorMessages, updateNoteErrorMessage } from '~/notes/utils';
import { HTTP_STATUS_UNPROCESSABLE_ENTITY, HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status';
-import { COMMENT_FORM } from '~/notes/i18n';
+import { COMMENT_FORM, UPDATE_COMMENT_FORM } from '~/notes/i18n';
-describe('getErrorMessages', () => {
+describe('createNoteErrorMessages', () => {
describe('when http status is not HTTP_STATUS_UNPROCESSABLE_ENTITY', () => {
it('returns generic error', () => {
- const errorMessages = getErrorMessages(
+ const errorMessages = createNoteErrorMessages(
{ errors: ['unknown error'] },
HTTP_STATUS_BAD_REQUEST,
);
@@ -17,7 +17,7 @@ describe('getErrorMessages', () => {
describe('when http status is HTTP_STATUS_UNPROCESSABLE_ENTITY', () => {
it('returns all errors', () => {
- const errorMessages = getErrorMessages(
+ const errorMessages = createNoteErrorMessages(
{ errors: 'error 1 and error 2' },
HTTP_STATUS_UNPROCESSABLE_ENTITY,
);
@@ -29,7 +29,7 @@ describe('getErrorMessages', () => {
describe('when response contains commands_only errors', () => {
it('only returns commands_only errors', () => {
- const errorMessages = getErrorMessages(
+ const errorMessages = createNoteErrorMessages(
{
errors: {
commands_only: ['commands_only error 1', 'commands_only error 2'],
@@ -44,3 +44,22 @@ describe('getErrorMessages', () => {
});
});
});
+
+describe('updateNoteErrorMessage', () => {
+ describe('with server error', () => {
+ it('returns error message with server error', () => {
+ const error = 'error 1 and error 2';
+ const errorMessage = updateNoteErrorMessage({ response: { data: { errors: error } } });
+
+ expect(errorMessage).toEqual(sprintf(UPDATE_COMMENT_FORM.error, { reason: error }));
+ });
+ });
+
+ describe('without server error', () => {
+ it('returns generic error message', () => {
+ const errorMessage = updateNoteErrorMessage(null);
+
+ expect(errorMessage).toEqual(UPDATE_COMMENT_FORM.defaultError);
+ });
+ });
+});
diff --git a/spec/frontend/notifications/components/notification_email_listbox_input_spec.js b/spec/frontend/notifications/components/notification_email_listbox_input_spec.js
index c490c737cf1..a3a847b9523 100644
--- a/spec/frontend/notifications/components/notification_email_listbox_input_spec.js
+++ b/spec/frontend/notifications/components/notification_email_listbox_input_spec.js
@@ -13,6 +13,7 @@ describe('NotificationEmailListboxInput', () => {
const emptyValueText = 'emptyValueText';
const value = 'value';
const disabled = false;
+ const placement = 'right';
// Finders
const findListboxInput = () => wrapper.findComponent(ListboxInput);
@@ -26,6 +27,7 @@ describe('NotificationEmailListboxInput', () => {
emptyValueText,
value,
disabled,
+ placement,
},
attachTo,
});
diff --git a/spec/frontend/observability/client_spec.js b/spec/frontend/observability/client_spec.js
new file mode 100644
index 00000000000..239d7adf986
--- /dev/null
+++ b/spec/frontend/observability/client_spec.js
@@ -0,0 +1,66 @@
+import MockAdapter from 'axios-mock-adapter';
+import { buildClient } from '~/observability/client';
+import axios from '~/lib/utils/axios_utils';
+
+jest.mock('~/lib/utils/axios_utils');
+
+describe('buildClient', () => {
+ let client;
+ let axiosMock;
+
+ const tracingUrl = 'https://example.com/tracing';
+
+ beforeEach(() => {
+ axiosMock = new MockAdapter(axios);
+ jest.spyOn(axios, 'get');
+
+ client = buildClient({
+ tracingUrl,
+ provisioningUrl: 'https://example.com/provisioning',
+ });
+ });
+
+ afterEach(() => {
+ axiosMock.restore();
+ });
+
+ describe('fetchTraces', () => {
+ it('should fetch traces from the tracing URL', async () => {
+ const mockTraces = [
+ { id: 1, spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }] },
+ { id: 2, spans: [{ duration_nano: 2000 }] },
+ ];
+
+ axiosMock.onGet(tracingUrl).reply(200, {
+ traces: mockTraces,
+ });
+
+ const result = await client.fetchTraces();
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ expect(axios.get).toHaveBeenCalledWith(tracingUrl, {
+ withCredentials: true,
+ });
+ expect(result).toEqual([
+ { id: 1, spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }], duration: 3 },
+ { id: 2, spans: [{ duration_nano: 2000 }], duration: 2 },
+ ]);
+ });
+
+ it('rejects if traces are missing', () => {
+ axiosMock.onGet(tracingUrl).reply(200, {});
+
+ return expect(client.fetchTraces()).rejects.toThrow(
+ 'traces are missing/invalid in the response',
+ );
+ });
+
+ it('rejects if traces are invalid', () => {
+ axiosMock.onGet(tracingUrl).reply(200, { traces: 'invalid' });
+
+ return expect(client.fetchTraces()).rejects.toThrow(
+ 'traces are missing/invalid in the response',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/observability/observability_app_spec.js b/spec/frontend/observability/observability_app_spec.js
index 4a9be71b880..392992a5962 100644
--- a/spec/frontend/observability/observability_app_spec.js
+++ b/spec/frontend/observability/observability_app_spec.js
@@ -1,4 +1,5 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import ObservabilityApp from '~/observability/components/observability_app.vue';
import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
import {
@@ -21,7 +22,7 @@ describe('ObservabilityApp', () => {
query: { otherQuery: 100 },
};
- const mockHandleSkeleton = jest.fn();
+ const mockSkeletonOnContentLoaded = jest.fn();
const findIframe = () => wrapper.findByTestId('observability-ui-iframe');
@@ -36,7 +37,9 @@ describe('ObservabilityApp', () => {
...props,
},
stubs: {
- 'observability-skeleton': ObservabilitySkeleton,
+ ObservabilitySkeleton: stubComponent(ObservabilitySkeleton, {
+ methods: { onContentLoaded: mockSkeletonOnContentLoaded },
+ }),
},
mocks: {
$route,
@@ -155,14 +158,14 @@ describe('ObservabilityApp', () => {
describe('on GOUI_LOADED', () => {
beforeEach(() => {
mountComponent();
- wrapper.vm.$refs.observabilitySkeleton.onContentLoaded = mockHandleSkeleton;
});
+
it('should call onContentLoaded method', () => {
dispatchMessageEvent({
data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
origin: 'https://observe.gitlab.com',
});
- expect(mockHandleSkeleton).toHaveBeenCalled();
+ expect(mockSkeletonOnContentLoaded).toHaveBeenCalled();
});
it('should not call onContentLoaded method if origin is different', () => {
@@ -170,7 +173,7 @@ describe('ObservabilityApp', () => {
data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
origin: 'https://example.com',
});
- expect(mockHandleSkeleton).not.toHaveBeenCalled();
+ expect(mockSkeletonOnContentLoaded).not.toHaveBeenCalled();
});
it('should not call onContentLoaded method if event type is different', () => {
@@ -178,7 +181,7 @@ describe('ObservabilityApp', () => {
data: { type: 'UNKNOWN_EVENT' },
origin: 'https://observe.gitlab.com',
});
- expect(mockHandleSkeleton).not.toHaveBeenCalled();
+ expect(mockSkeletonOnContentLoaded).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/observability/observability_container_spec.js b/spec/frontend/observability/observability_container_spec.js
new file mode 100644
index 00000000000..1152df072d4
--- /dev/null
+++ b/spec/frontend/observability/observability_container_spec.js
@@ -0,0 +1,134 @@
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import ObservabilityContainer from '~/observability/components/observability_container.vue';
+import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
+import { buildClient } from '~/observability/client';
+
+jest.mock('~/observability/client');
+
+describe('ObservabilityContainer', () => {
+ let wrapper;
+
+ const mockSkeletonOnContentLoaded = jest.fn();
+ const mockSkeletonOnError = jest.fn();
+
+ const OAUTH_URL = 'https://example.com/oauth';
+ const TRACING_URL = 'https://example.com/tracing';
+ const PROVISIONING_URL = 'https://example.com/provisioning';
+
+ beforeEach(() => {
+ jest.spyOn(console, 'error').mockImplementation();
+
+ buildClient.mockReturnValue({});
+
+ wrapper = shallowMountExtended(ObservabilityContainer, {
+ propsData: {
+ oauthUrl: OAUTH_URL,
+ tracingUrl: TRACING_URL,
+ provisioningUrl: PROVISIONING_URL,
+ },
+ stubs: {
+ ObservabilitySkeleton: stubComponent(ObservabilitySkeleton, {
+ methods: { onContentLoaded: mockSkeletonOnContentLoaded, onError: mockSkeletonOnError },
+ }),
+ },
+ slots: {
+ default: {
+ render(h) {
+ h(`<div>mockedComponent</div>`);
+ },
+ name: 'MockComponent',
+ props: {
+ observabilityClient: {
+ type: Object,
+ required: true,
+ },
+ },
+ },
+ },
+ });
+ });
+
+ const dispatchMessageEvent = (status, origin) =>
+ window.dispatchEvent(
+ new MessageEvent('message', {
+ data: {
+ type: 'AUTH_COMPLETION',
+ status,
+ },
+ origin: origin ?? new URL(OAUTH_URL).origin,
+ }),
+ );
+
+ const findIframe = () => wrapper.findByTestId('observability-oauth-iframe');
+ const findSlotComponent = () => wrapper.findComponent({ name: 'MockComponent' });
+
+ it('should render the oauth iframe', () => {
+ const iframe = findIframe();
+ expect(iframe.exists()).toBe(true);
+ expect(iframe.attributes('hidden')).toBe('hidden');
+ expect(iframe.attributes('src')).toBe(OAUTH_URL);
+ expect(iframe.attributes('sandbox')).toBe('allow-same-origin allow-forms allow-scripts');
+ });
+
+ it('should render the ObservabilitySkeleton', () => {
+ const skeleton = wrapper.findComponent(ObservabilitySkeleton);
+ expect(skeleton.exists()).toBe(true);
+ });
+
+ it('should not render the default slot', () => {
+ expect(findSlotComponent().exists()).toBe(false);
+ });
+
+ it('renders the slot content and removes the iframe on oauth success message', async () => {
+ dispatchMessageEvent('success');
+
+ await nextTick();
+
+ expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(1);
+
+ const slotComponent = findSlotComponent();
+ expect(slotComponent.exists()).toBe(true);
+ expect(buildClient).toHaveBeenCalledWith({
+ provisioningUrl: PROVISIONING_URL,
+ tracingUrl: TRACING_URL,
+ });
+ expect(findIframe().exists()).toBe(false);
+ });
+
+ it('does not render the slot content and removes the iframe on oauth error message', async () => {
+ dispatchMessageEvent('error');
+
+ await nextTick();
+
+ expect(mockSkeletonOnError).toHaveBeenCalledTimes(1);
+
+ expect(findSlotComponent().exists()).toBe(false);
+ expect(findIframe().exists()).toBe(false);
+ expect(buildClient).not.toHaveBeenCalled();
+ });
+
+ it('handles oauth message only once', () => {
+ dispatchMessageEvent('success');
+ dispatchMessageEvent('success');
+
+ expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(1);
+ });
+
+ it('only handles messages from the oauth url', () => {
+ dispatchMessageEvent('success', 'www.fake-url.com');
+
+ expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(0);
+ expect(findSlotComponent().exists()).toBe(false);
+ expect(findIframe().exists()).toBe(true);
+ });
+
+ it('does not handle messages if the component has been destroyed', () => {
+ wrapper.destroy();
+
+ dispatchMessageEvent('success');
+
+ expect(mockSkeletonOnContentLoaded).toHaveBeenCalledTimes(0);
+ });
+});
diff --git a/spec/frontend/observability/skeleton_spec.js b/spec/frontend/observability/skeleton_spec.js
index 65dbb003743..979070cfb12 100644
--- a/spec/frontend/observability/skeleton_spec.js
+++ b/spec/frontend/observability/skeleton_spec.js
@@ -1,5 +1,5 @@
import { nextTick } from 'vue';
-import { GlSkeletonLoader, GlAlert } from '@gitlab/ui';
+import { GlSkeletonLoader, GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Skeleton from '~/observability/components/skeleton/index.vue';
@@ -17,9 +17,9 @@ import {
describe('Skeleton component', () => {
let wrapper;
- const SKELETON_VARIANTS = Object.values(SKELETON_VARIANTS_BY_ROUTE);
+ const SKELETON_VARIANTS = [...Object.values(SKELETON_VARIANTS_BY_ROUTE), 'spinner'];
- const findContentWrapper = () => wrapper.findByTestId('observability-wrapper');
+ const findContentWrapper = () => wrapper.findByTestId('content-wrapper');
const findExploreSkeleton = () => wrapper.findComponent(ExploreSkeleton);
@@ -42,8 +42,8 @@ describe('Skeleton component', () => {
mountComponent({ variant: 'explore' });
});
- describe('loading timers', () => {
- it('show Skeleton if content is not loaded within CONTENT_WAIT_MS', async () => {
+ describe('showing content', () => {
+ it('shows the skeleton if content is not loaded within CONTENT_WAIT_MS', async () => {
expect(findExploreSkeleton().exists()).toBe(false);
expect(findContentWrapper().isVisible()).toBe(false);
@@ -55,7 +55,7 @@ describe('Skeleton component', () => {
expect(findContentWrapper().isVisible()).toBe(false);
});
- it('does not show the skeleton if content has loaded within CONTENT_WAIT_MS', async () => {
+ it('does not show the skeleton if content loads within CONTENT_WAIT_MS', async () => {
expect(findExploreSkeleton().exists()).toBe(false);
expect(findContentWrapper().isVisible()).toBe(false);
@@ -73,9 +73,25 @@ describe('Skeleton component', () => {
expect(findContentWrapper().isVisible()).toBe(true);
expect(findExploreSkeleton().exists()).toBe(false);
});
+
+ it('hides the skeleton after content loads', async () => {
+ jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
+
+ await nextTick();
+
+ expect(findExploreSkeleton().exists()).toBe(true);
+ expect(findContentWrapper().isVisible()).toBe(false);
+
+ wrapper.vm.onContentLoaded();
+
+ await nextTick();
+
+ expect(findContentWrapper().isVisible()).toBe(true);
+ expect(findExploreSkeleton().exists()).toBe(false);
+ });
});
- describe('error timeout', () => {
+ describe('error handling', () => {
it('shows the error dialog if content has not loaded within TIMEOUT_MS', async () => {
expect(findAlert().exists()).toBe(false);
jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
@@ -86,6 +102,17 @@ describe('Skeleton component', () => {
expect(findContentWrapper().isVisible()).toBe(false);
});
+ it('shows the error dialog if content fails to load', async () => {
+ expect(findAlert().exists()).toBe(false);
+
+ wrapper.vm.onError();
+
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findContentWrapper().isVisible()).toBe(false);
+ });
+
it('does not show the error dialog if content has loaded within TIMEOUT_MS', async () => {
wrapper.vm.onContentLoaded();
jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
@@ -105,6 +132,7 @@ describe('Skeleton component', () => {
${'explore'} | ${'variant is explore'} | ${SKELETON_VARIANTS[1]}
${'manage'} | ${'variant is manage'} | ${SKELETON_VARIANTS[2]}
${'embed'} | ${'variant is embed'} | ${SKELETON_VARIANT_EMBED}
+ ${'spinner'} | ${'variant is spinner'} | ${'spinner'}
${'default'} | ${'variant is not manage, dashboards or explore'} | ${'unknown'}
`('should render $skeletonType skeleton if $condition', async ({ skeletonType, variant }) => {
mountComponent({ variant });
@@ -120,6 +148,8 @@ describe('Skeleton component', () => {
expect(findEmbedSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANT_EMBED);
expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(showsDefaultSkeleton);
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(variant === 'spinner');
});
});
diff --git a/spec/frontend/organizations/groups_and_projects/components/app_spec.js b/spec/frontend/organizations/groups_and_projects/components/app_spec.js
new file mode 100644
index 00000000000..24e1a26336c
--- /dev/null
+++ b/spec/frontend/organizations/groups_and_projects/components/app_spec.js
@@ -0,0 +1,99 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { GlLoadingIcon } from '@gitlab/ui';
+import App from '~/organizations/groups_and_projects/components/app.vue';
+import resolvers from '~/organizations/groups_and_projects/graphql/resolvers';
+import ProjectsList from '~/vue_shared/components/projects_list/projects_list.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { createAlert } from '~/alert';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { organizationProjects } from './mock_data';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+jest.useFakeTimers();
+
+describe('GroupsAndProjectsApp', () => {
+ let wrapper;
+ let mockApollo;
+
+ const createComponent = ({ mockResolvers = resolvers } = {}) => {
+ mockApollo = createMockApollo([], mockResolvers);
+
+ wrapper = shallowMountExtended(App, { apolloProvider: mockApollo });
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ describe('when API call is loading', () => {
+ beforeEach(() => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ });
+
+ it('renders loading icon', () => {
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+ });
+
+ describe('when API call is successful', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `ProjectsList` component and passes correct props', async () => {
+ jest.runAllTimers();
+ await waitForPromises();
+
+ expect(wrapper.findComponent(ProjectsList).props()).toEqual({
+ projects: organizationProjects.projects.nodes.map(
+ ({ id, nameWithNamespace, accessLevel, ...project }) => ({
+ ...project,
+ id: getIdFromGraphQLId(id),
+ name: nameWithNamespace,
+ permissions: {
+ projectAccess: {
+ accessLevel: accessLevel.integerValue,
+ },
+ },
+ }),
+ ),
+ showProjectIcon: true,
+ });
+ });
+ });
+
+ describe('when API call is not successful', () => {
+ const error = new Error();
+
+ beforeEach(() => {
+ const mockResolvers = {
+ Query: {
+ organization: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ });
+
+ it('displays error alert', async () => {
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: App.i18n.errorMessage,
+ error,
+ captureError: true,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/groups_and_projects/components/mock_data.js b/spec/frontend/organizations/groups_and_projects/components/mock_data.js
new file mode 100644
index 00000000000..c3276450745
--- /dev/null
+++ b/spec/frontend/organizations/groups_and_projects/components/mock_data.js
@@ -0,0 +1,98 @@
+export const organizationProjects = {
+ id: 'gid://gitlab/Organization/1',
+ __typename: 'Organization',
+ projects: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Project/8',
+ nameWithNamespace: 'Twitter / Typeahead.Js',
+ webUrl: 'http://127.0.0.1:3000/twitter/Typeahead.Js',
+ topics: ['JavaScript', 'Vue.js'],
+ forksCount: 4,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'public',
+ openIssuesCount: 48,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:59" dir="auto">Optio et reprehenderit enim doloremque deserunt et commodi.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/7',
+ nameWithNamespace: 'Flightjs / Flight',
+ webUrl: 'http://127.0.0.1:3000/flightjs/Flight',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'private',
+ openIssuesCount: 37,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:49" dir="auto">Dolor dicta rerum et ut eius voluptate earum qui.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ accessLevel: {
+ integerValue: 20,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/6',
+ nameWithNamespace: 'Jashkenas / Underscore',
+ webUrl: 'http://127.0.0.1:3000/jashkenas/Underscore',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'private',
+ openIssuesCount: 34,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:52" dir="auto">Incidunt est aliquam autem nihil eveniet quis autem.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ accessLevel: {
+ integerValue: 40,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/5',
+ nameWithNamespace: 'Commit451 / Lab Coat',
+ webUrl: 'http://127.0.0.1:3000/Commit451/lab-coat',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'internal',
+ openIssuesCount: 49,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:34" dir="auto">Sint eos dolorem impedit rerum et.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ accessLevel: {
+ integerValue: 10,
+ },
+ },
+ {
+ id: 'gid://gitlab/Project/1',
+ nameWithNamespace: 'Toolbox / Gitlab Smoke Tests',
+ webUrl: 'http://127.0.0.1:3000/toolbox/gitlab-smoke-tests',
+ topics: [],
+ forksCount: 0,
+ avatarUrl: null,
+ starCount: 0,
+ visibility: 'internal',
+ openIssuesCount: 34,
+ descriptionHtml:
+ '<p data-sourcepos="1:1-1:40" dir="auto">Veritatis error laboriosam libero autem.</p>',
+ issuesAccessLevel: 'enabled',
+ forkingAccessLevel: 'enabled',
+ accessLevel: {
+ integerValue: 30,
+ },
+ },
+ ],
+ },
+};
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 f74dfcb029d..d4b69d3e8e8 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
@@ -1,5 +1,11 @@
-import { GlFormCheckbox, GlSprintf, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import {
+ GlFormCheckbox,
+ GlSprintf,
+ GlIcon,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+} from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -37,16 +43,17 @@ describe('tags list row', () => {
const findManifestDetail = () => wrapper.find('[data-testid="manifest-detail"]');
const findConfigurationDetail = () => wrapper.find('[data-testid="configuration-detail"]');
const findWarningIcon = () => wrapper.findComponent(GlIcon);
- const findAdditionalActionsMenu = () => wrapper.findComponent(GlDropdown);
- const findDeleteButton = () => wrapper.findComponent(GlDropdownItem);
+ const findAdditionalActionsMenu = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDeleteButton = () => wrapper.findComponent(GlDisclosureDropdownItem);
- const mountComponent = (propsData = defaultProps) => {
- wrapper = shallowMount(component, {
+ const mountComponent = (propsData = defaultProps, mountFn = shallowMount) => {
+ wrapper = mountFn(component, {
stubs: {
GlSprintf,
ListItem,
DetailsRow,
- GlDropdown,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
},
propsData,
directives: {
@@ -274,10 +281,10 @@ describe('tags list row', () => {
expect(findAdditionalActionsMenu().props()).toMatchObject({
icon: 'ellipsis_v',
- text: 'More actions',
+ toggleText: 'More actions',
textSrOnly: true,
category: 'tertiary',
- right: true,
+ placement: 'right',
disabled: false,
});
});
@@ -308,16 +315,19 @@ describe('tags list row', () => {
mountComponent();
expect(findDeleteButton().exists()).toBe(true);
- expect(findDeleteButton().attributes()).toMatchObject({
- variant: 'danger',
+ expect(findDeleteButton().props('item').extraAttrs).toMatchObject({
+ class: 'gl-text-red-500!',
+ 'data-testid': 'single-delete-button',
+ 'data-qa-selector': 'tag_delete_button',
});
+
expect(findDeleteButton().text()).toBe(REMOVE_TAG_BUTTON_TITLE);
});
it('delete event emits delete', () => {
- mountComponent();
+ mountComponent(undefined, mount);
- findDeleteButton().vm.$emit('click');
+ wrapper.find('[data-testid="single-delete-button"]').trigger('click');
expect(wrapper.emitted('delete')).toEqual([[]]);
});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
index 1928dbf72b6..f590cff0312 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -6,7 +6,7 @@ import {
GlFormGroup,
GlModal,
GlSprintf,
- GlEmptyState,
+ GlSkeletonLoader,
} from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -78,7 +78,7 @@ describe('DependencyProxyApp', () => {
const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup);
const findProxyCountText = () => wrapper.findByTestId('proxy-count');
const findManifestList = () => wrapper.findComponent(ManifestsList);
- const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findClearCacheDropdownList = () => wrapper.findComponent(GlDropdown);
const findClearCacheModal = () => wrapper.findComponent(GlModal);
const findClearCacheAlert = () => wrapper.findComponent(GlAlert);
@@ -102,9 +102,16 @@ describe('DependencyProxyApp', () => {
describe('when the dependency proxy is available', () => {
describe('when is loading', () => {
- it('does not render a form group with label', () => {
+ beforeEach(() => {
createComponent();
+ });
+
+ it('renders loading component & sets loading prop', () => {
+ expect(findLoader().exists()).toBe(true);
+ expect(findManifestList().props('loading')).toBe(true);
+ });
+ it('does not render a form group with label', () => {
expect(findFormGroup().exists()).toBe(false);
});
});
@@ -120,11 +127,15 @@ describe('DependencyProxyApp', () => {
expect(findFormGroup().attributes('label')).toBe(
DependencyProxyApp.i18n.proxyImagePrefix,
);
+ expect(findFormGroup().attributes('labelfor')).toBe('proxy-url');
});
it('renders a form input group', () => {
expect(findFormInputGroup().exists()).toBe(true);
+ expect(findFormInputGroup().attributes('id')).toBe('proxy-url');
expect(findFormInputGroup().props('value')).toBe(proxyData().dependencyProxyImagePrefix);
+ expect(findFormInputGroup().attributes('readonly')).toBeDefined();
+ expect(findFormInputGroup().props('selectOnClick')).toBe(true);
});
it('form input group has a clipboard button', () => {
@@ -175,23 +186,12 @@ describe('DependencyProxyApp', () => {
return waitForPromises();
});
- it('shows the empty state message', () => {
- expect(findEmptyState().props()).toMatchObject({
- svgPath: provideDefaults.noManifestsIllustration,
- title: DependencyProxyApp.i18n.noManifestTitle,
- });
- });
-
- it('hides the list', () => {
- expect(findManifestList().exists()).toBe(false);
+ it('renders the list', () => {
+ expect(findManifestList().exists()).toBe(true);
});
});
describe('when there are manifests', () => {
- it('hides the empty state message', () => {
- expect(findEmptyState().exists()).toBe(false);
- });
-
it('shows list', () => {
expect(findManifestList().props()).toMatchObject({
dependencyProxyImagePrefix: proxyData().dependencyProxyImagePrefix,
@@ -200,26 +200,58 @@ describe('DependencyProxyApp', () => {
});
});
- it('prev-page event on list fetches the previous page', async () => {
- findManifestList().vm.$emit('prev-page');
- await waitForPromises();
+ describe('prev-page event on list', () => {
+ beforeEach(() => {
+ findManifestList().vm.$emit('prev-page');
+ });
+
+ describe('while loading', () => {
+ it('does not render loading component & sets loading prop', () => {
+ expect(findLoader().exists()).toBe(false);
+ expect(findManifestList().props('loading')).toBe(true);
+ });
- expect(resolver).toHaveBeenCalledWith({
- before: pagination().startCursor,
- first: null,
- fullPath: provideDefaults.groupPath,
- last: GRAPHQL_PAGE_SIZE,
+ it('renders form group with label', () => {
+ expect(findFormGroup().exists()).toBe(true);
+ });
+ });
+
+ it('list fetches the previous page', async () => {
+ await waitForPromises();
+
+ expect(resolver).toHaveBeenCalledWith({
+ before: pagination().startCursor,
+ first: null,
+ fullPath: provideDefaults.groupPath,
+ last: GRAPHQL_PAGE_SIZE,
+ });
});
});
- it('next-page event on list fetches the next page', async () => {
- findManifestList().vm.$emit('next-page');
- await waitForPromises();
+ describe('next-page event on list', () => {
+ beforeEach(() => {
+ findManifestList().vm.$emit('next-page');
+ });
- expect(resolver).toHaveBeenCalledWith({
- after: pagination().endCursor,
- first: GRAPHQL_PAGE_SIZE,
- fullPath: provideDefaults.groupPath,
+ describe('while loading', () => {
+ it('does not render loading component & sets loading prop', () => {
+ expect(findLoader().exists()).toBe(false);
+ expect(findManifestList().props('loading')).toBe(true);
+ });
+
+ it('renders form group with label', () => {
+ expect(findFormGroup().exists()).toBe(true);
+ });
+ });
+
+ it('fetches the next page', async () => {
+ await waitForPromises();
+
+ expect(resolver).toHaveBeenCalledWith({
+ after: pagination().endCursor,
+ first: GRAPHQL_PAGE_SIZE,
+ fullPath: provideDefaults.groupPath,
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
index 4149f728cd8..8f445843aa8 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifest_list_spec.js
@@ -1,6 +1,7 @@
import { GlKeysetPagination, GlSkeletonLoader } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ManifestRow from '~/packages_and_registries/dependency_proxy/components/manifest_row.vue';
+import ManifestsEmptyState from '~/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue';
import Component from '~/packages_and_registries/dependency_proxy/components/manifests_list.vue';
import {
proxyData,
@@ -24,6 +25,7 @@ describe('Manifests List', () => {
});
};
+ const findEmptyState = () => wrapper.findComponent(ManifestsEmptyState);
const findRows = () => wrapper.findAllComponents(ManifestRow);
const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findMainArea = () => wrapper.findByTestId('main-area');
@@ -38,7 +40,13 @@ describe('Manifests List', () => {
it('shows a row for every manifest', () => {
createComponent();
- expect(findRows().length).toBe(defaultProps.manifests.length);
+ expect(findRows()).toHaveLength(defaultProps.manifests.length);
+ });
+
+ it('does not show the empty state component', () => {
+ createComponent();
+
+ expect(findEmptyState().exists()).toBe(false);
});
it('binds a manifest to each row', () => {
@@ -68,6 +76,20 @@ describe('Manifests List', () => {
});
});
+ describe('when there are no manifests', () => {
+ beforeEach(() => {
+ createComponent({ ...defaultProps, manifests: [], pagination: {} });
+ });
+
+ it('shows the empty state component', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('hides the list', () => {
+ expect(findRows()).toHaveLength(0);
+ });
+ });
+
describe('pagination', () => {
it('is hidden when there is no next or prev pages', () => {
createComponent({ ...defaultProps, pagination: {} });
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/components/manifests_empty_state_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/components/manifests_empty_state_spec.js
new file mode 100644
index 00000000000..00c1469994b
--- /dev/null
+++ b/spec/frontend/packages_and_registries/dependency_proxy/components/manifests_empty_state_spec.js
@@ -0,0 +1,81 @@
+import { GlEmptyState, GlFormGroup, GlFormInputGroup, GlLink, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ManifestsEmptyState from '~/packages_and_registries/dependency_proxy/components/manifests_empty_state.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+describe('manifests empty state', () => {
+ let wrapper;
+
+ const provideDefaults = {
+ noManifestsIllustration: 'noManifestsIllustration',
+ };
+
+ const createComponent = ({ stubs = {} } = {}) => {
+ wrapper = shallowMountExtended(ManifestsEmptyState, {
+ provide: provideDefaults,
+ stubs: {
+ GlEmptyState,
+ GlFormInputGroup,
+ ...stubs,
+ },
+ });
+ };
+
+ const findDocsLink = () => wrapper.findComponent(GlLink);
+ const findEmptyTextDescription = () => wrapper.findAllComponents(GlSprintf).at(0);
+ const findDocumentationTextDescription = () => wrapper.findAllComponents(GlSprintf).at(1);
+ const findClipBoardButton = () => wrapper.findComponent(ClipboardButton);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findFormInputGroup = () => wrapper.findComponent(GlFormInputGroup);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows the empty state message', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: provideDefaults.noManifestsIllustration,
+ title: ManifestsEmptyState.i18n.noManifestTitle,
+ });
+ });
+
+ it('renders correct description', () => {
+ expect(findEmptyTextDescription().attributes('message')).toBe(
+ ManifestsEmptyState.i18n.emptyText,
+ );
+ expect(findDocumentationTextDescription().attributes('message')).toBe(
+ ManifestsEmptyState.i18n.documentationText,
+ );
+ });
+
+ it('renders a form group with a label', () => {
+ expect(findFormGroup().attributes('label')).toBe(ManifestsEmptyState.i18n.codeExampleLabel);
+ expect(findFormGroup().attributes('label-sr-only')).toBeDefined();
+ expect(findFormGroup().attributes('label-for')).toBe('code-example');
+ });
+
+ it('renders a form input group', () => {
+ expect(findFormInputGroup().exists()).toBe(true);
+ expect(findFormInputGroup().attributes('id')).toBe('code-example');
+ expect(findFormInputGroup().props('value')).toBe(ManifestsEmptyState.codeExample);
+ expect(findFormInputGroup().attributes('readonly')).toBeDefined();
+ expect(findFormInputGroup().props('selectOnClick')).toBe(true);
+ });
+
+ it('form input group has a clipboard button', () => {
+ expect(findClipBoardButton().exists()).toBe(true);
+ expect(findClipBoardButton().props()).toMatchObject({
+ text: ManifestsEmptyState.codeExample,
+ title: ManifestsEmptyState.i18n.copyExample,
+ });
+ });
+
+ it('shows link to docs', () => {
+ createComponent({ stubs: { GlSprintf } });
+
+ expect(findDocsLink().attributes('href')).toBe(
+ ManifestsEmptyState.links.DEPENDENCY_PROXY_HELP_PAGE_PATH,
+ );
+ });
+});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
index 2b60684e60a..2c712feac86 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_files_spec.js
@@ -1,4 +1,12 @@
-import { GlAlert, GlDropdown, GlButton, GlFormCheckbox, GlLoadingIcon, GlModal } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlDisclosureDropdown,
+ GlFormCheckbox,
+ GlLoadingIcon,
+ GlModal,
+ GlKeysetPagination,
+} from '@gitlab/ui';
+import * as Sentry from '@sentry/browser';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { stubComponent } from 'helpers/stub_component';
@@ -13,6 +21,7 @@ import {
packageFilesQuery,
packageDestroyFilesMutation,
packageDestroyFilesMutationError,
+ pagination,
} from 'jest/packages_and_registries/package_registry/mock_data';
import {
DOWNLOAD_PACKAGE_ASSET_TRACKING_ACTION,
@@ -22,16 +31,22 @@ import {
DELETE_PACKAGE_FILE_ERROR_MESSAGE,
DELETE_PACKAGE_FILES_SUCCESS_MESSAGE,
DELETE_PACKAGE_FILES_ERROR_MESSAGE,
+ GRAPHQL_PACKAGE_FILES_PAGE_SIZE,
} from '~/packages_and_registries/package_registry/constants';
+import { NEXT, PREV } from '~/vue_shared/components/pagination/constants';
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-
+import { scrollToElement } from '~/lib/utils/common_utils';
import getPackageFiles from '~/packages_and_registries/package_registry/graphql/queries/get_package_files.query.graphql';
import destroyPackageFilesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package_files.mutation.graphql';
Vue.use(VueApollo);
jest.mock('~/alert');
+jest.mock('~/lib/utils/common_utils', () => ({
+ ...jest.requireActual('~/lib/utils/common_utils'),
+ scrollToElement: jest.fn(),
+}));
describe('Package Files', () => {
let wrapper;
@@ -43,13 +58,15 @@ describe('Package Files', () => {
const findFirstRow = () => extendedWrapper(findAllRows().at(0));
const findSecondRow = () => extendedWrapper(findAllRows().at(1));
const findPackageFilesAlert = () => wrapper.findComponent(GlAlert);
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findFirstRowDownloadLink = () => findFirstRow().findByTestId('download-link');
const findFirstRowFileIcon = () => findFirstRow().findComponent(FileIcon);
const findFirstRowCreatedAt = () => findFirstRow().findComponent(TimeAgoTooltip);
- const findFirstActionMenu = () => extendedWrapper(findFirstRow().findComponent(GlDropdown));
+ const findFirstActionMenu = () =>
+ extendedWrapper(findFirstRow().findComponent(GlDisclosureDropdown));
const findActionMenuDelete = () => findFirstActionMenu().findByTestId('delete-file');
- const findFirstToggleDetailsButton = () => findFirstRow().findComponent(GlButton);
+ const findFirstToggleDetailsButton = () => findFirstRow().findByTestId('toggle-details-button');
const findFirstRowShaComponent = (id) => wrapper.findByTestId(id);
const findCheckAllCheckbox = () => wrapper.findByTestId('package-files-checkbox-all');
const findAllRowCheckboxes = () => wrapper.findAllByTestId('package-files-checkbox');
@@ -68,6 +85,7 @@ describe('Package Files', () => {
stubs,
resolver = jest.fn().mockResolvedValue(packageFilesQuery({ files: [file] })),
filesDeleteMutationResolver = jest.fn().mockResolvedValue(packageDestroyFilesMutation()),
+ options = {},
} = {}) => {
const requestHandlers = [
[getPackageFiles, resolver],
@@ -92,9 +110,14 @@ describe('Package Files', () => {
}),
...stubs,
},
+ ...options,
});
};
+ beforeEach(() => {
+ jest.spyOn(Sentry, 'captureException').mockImplementation();
+ });
+
describe('rows', () => {
it('do not get rendered when query is loading', () => {
createComponent();
@@ -123,6 +146,7 @@ describe('Package Files', () => {
await waitForPromises();
expect(findPackageFilesAlert().exists()).toBe(false);
+ expect(Sentry.captureException).not.toHaveBeenCalled();
});
it('renders gl-alert if load fails', async () => {
@@ -133,6 +157,40 @@ describe('Package Files', () => {
expect(findPackageFilesAlert().text()).toBe(
s__('PackageRegistry|Something went wrong while fetching package assets.'),
);
+ expect(Sentry.captureException).toHaveBeenCalled();
+ });
+
+ it('renders pagination', async () => {
+ createComponent({ resolver: jest.fn().mockResolvedValue(packageFilesQuery()) });
+ await waitForPromises();
+
+ const { endCursor, startCursor, hasNextPage, hasPreviousPage } = pagination();
+
+ expect(findPagination().props()).toMatchObject({
+ endCursor,
+ startCursor,
+ hasNextPage,
+ hasPreviousPage,
+ prevText: PREV,
+ nextText: NEXT,
+ disabled: false,
+ });
+ });
+
+ it('hides pagination when only one page', async () => {
+ createComponent({
+ resolver: jest.fn().mockResolvedValue(
+ packageFilesQuery({
+ extendPagination: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ },
+ }),
+ ),
+ });
+ await waitForPromises();
+
+ expect(findPagination().exists()).toBe(false);
});
});
@@ -204,7 +262,7 @@ describe('Package Files', () => {
expect(findFirstActionMenu().exists()).toBe(true);
expect(findFirstActionMenu().props('icon')).toBe('ellipsis_v');
expect(findFirstActionMenu().props('textSrOnly')).toBe(true);
- expect(findFirstActionMenu().props('text')).toMatchInterpolatedText('More actions');
+ expect(findFirstActionMenu().props('toggleText')).toMatchInterpolatedText('More actions');
});
describe('menu items', () => {
@@ -214,7 +272,7 @@ describe('Package Files', () => {
});
it('shows delete file confirmation modal', async () => {
- await findActionMenuDelete().trigger('click');
+ await findActionMenuDelete().vm.$emit('action');
expect(showMock).toHaveBeenCalledTimes(1);
@@ -354,7 +412,7 @@ describe('Package Files', () => {
resolver: jest.fn().mockResolvedValue(
packageFilesQuery({
files: [file],
- pageInfo: {
+ extendPagination: {
hasNextPage: false,
},
}),
@@ -379,7 +437,7 @@ describe('Package Files', () => {
createComponent({
resolver: jest.fn().mockResolvedValue(
packageFilesQuery({
- pageInfo: {
+ extendPagination: {
hasNextPage: false,
},
}),
@@ -421,6 +479,69 @@ describe('Package Files', () => {
});
});
+ describe('when user interacts with pagination', () => {
+ const resolver = jest.fn().mockResolvedValue(packageFilesQuery());
+
+ beforeEach(async () => {
+ createComponent({ resolver, options: { attachTo: document.body } });
+ await waitForPromises();
+ });
+
+ describe('when list emits next event', () => {
+ beforeEach(() => {
+ findPagination().vm.$emit('next');
+ });
+
+ it('fetches the next set of files', () => {
+ expect(resolver).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ after: pagination().endCursor,
+ first: GRAPHQL_PACKAGE_FILES_PAGE_SIZE,
+ }),
+ );
+ });
+
+ it('scrolls to top of package files component', async () => {
+ await waitForPromises();
+
+ expect(scrollToElement).toHaveBeenCalledWith(wrapper.vm.$el);
+ });
+
+ it('first row is the active element', async () => {
+ await waitForPromises();
+
+ expect(findFirstRow().element).toBe(document.activeElement);
+ });
+ });
+
+ describe('when list emits prev event', () => {
+ beforeEach(() => {
+ findPagination().vm.$emit('prev');
+ });
+
+ it('fetches the previous set of files', () => {
+ expect(resolver).toHaveBeenLastCalledWith(
+ expect.objectContaining({
+ before: pagination().startCursor,
+ last: GRAPHQL_PACKAGE_FILES_PAGE_SIZE,
+ }),
+ );
+ });
+
+ it('scrolls to top of package files component', async () => {
+ await waitForPromises();
+
+ expect(scrollToElement).toHaveBeenCalledWith(wrapper.vm.$el);
+ });
+
+ it('first row is the active element', async () => {
+ await waitForPromises();
+
+ expect(findFirstRow().element).toBe(document.activeElement);
+ });
+ });
+ });
+
describe('deleting a file', () => {
const doDeleteFile = async () => {
const first = findAllRowCheckboxes().at(0);
@@ -442,6 +563,7 @@ describe('Package Files', () => {
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
+ expect(findPagination().props('disabled')).toBe(true);
});
it('confirming on the modal deletes the file and shows a success message', async () => {
@@ -474,7 +596,7 @@ describe('Package Files', () => {
expect(resolver).toHaveBeenCalledTimes(2);
expect(resolver).toHaveBeenCalledWith({
id: '1',
- first: 100,
+ first: GRAPHQL_PACKAGE_FILES_PAGE_SIZE,
});
});
@@ -534,6 +656,7 @@ describe('Package Files', () => {
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
+ expect(findPagination().props('disabled')).toBe(true);
});
it('confirming on the modal deletes the file and shows a success message', async () => {
@@ -566,7 +689,7 @@ describe('Package Files', () => {
expect(resolver).toHaveBeenCalledTimes(2);
expect(resolver).toHaveBeenCalledWith({
id: '1',
- first: 100,
+ first: GRAPHQL_PACKAGE_FILES_PAGE_SIZE,
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
index f7c8e909ff6..bc7203f73c9 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
@@ -1,5 +1,13 @@
-import { GlDropdownItem, GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlFormCheckbox,
+ GlIcon,
+ GlLink,
+ GlSprintf,
+ GlTruncate,
+} from '@gitlab/ui';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ListItem from '~/vue_shared/components/registry/list_item.vue';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
@@ -24,10 +32,16 @@ describe('VersionRow', () => {
const findPackageName = () => wrapper.findComponent(GlTruncate);
const findWarningIcon = () => wrapper.findComponent(GlIcon);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
- const findDeleteDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+ const findDeleteDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
- function createComponent({ packageEntity = packageVersion, selected = false } = {}) {
- wrapper = shallowMountExtended(VersionRow, {
+ function createComponent(options = {}) {
+ const {
+ mountFn = shallowMountExtended,
+ packageEntity = packageVersion,
+ selected = false,
+ } = options;
+
+ wrapper = mountFn(VersionRow, {
propsData: {
packageEntity,
selected,
@@ -35,6 +49,7 @@ describe('VersionRow', () => {
stubs: {
GlSprintf,
GlTruncate,
+ GlDisclosureDropdown,
},
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
@@ -100,9 +115,7 @@ describe('VersionRow', () => {
});
it('renders checkbox in selected state if selected', () => {
- createComponent({
- selected: true,
- });
+ createComponent({ selected: true });
expect(findBulkDeleteAction().attributes('checked')).toBe('true');
expect(findListItem().props('selected')).toBe(true);
@@ -116,19 +129,16 @@ describe('VersionRow', () => {
expect(findDeleteDropdownItem().exists()).toBe(false);
});
- it('exists and has the correct props', () => {
+ it('exists', () => {
createComponent();
expect(findDeleteDropdownItem().exists()).toBe(true);
- expect(findDeleteDropdownItem().attributes()).toMatchObject({
- variant: 'danger',
- });
});
- it('emits the delete event when the delete button is clicked', () => {
- createComponent();
+ it('emits the delete event when the delete button is clicked', async () => {
+ createComponent({ mountFn: mountExtended });
- findDeleteDropdownItem().vm.$emit('click');
+ await findDeleteDropdownItem().find('button').trigger('click');
expect(wrapper.emitted('delete')).toHaveLength(1);
});
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 c647230bc5f..0443fb85dc9 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
@@ -64,16 +64,12 @@ exports[`packages_list_row renders 1`] = `
withtooltip="true"
/>
- <!---->
-
<span
class="gl-ml-2"
data-testid="package-type"
>
· npm
</span>
-
- <!---->
</div>
</div>
</div>
@@ -91,15 +87,16 @@ exports[`packages_list_row renders 1`] = `
class="gl-display-flex gl-align-items-center gl-min-h-6"
>
<span
- data-testid="created-date"
+ data-testid="right-secondary"
>
- Created
- <timeago-tooltip-stub
- cssclass=""
- datetimeformat="DATE_WITH_TIME_FORMAT"
- time="2020-08-17T14:23:32Z"
- tooltipplacement="top"
- />
+ Published
+ <time
+ class=""
+ datetime="2020-05-17T14:23:32Z"
+ title="May 17, 2020 2:23pm UTC"
+ >
+ 1 month ago
+ </time>
</span>
</div>
</div>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index 81ad47b1e13..523d5f855fc 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -5,9 +5,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
-import PackagePath from '~/packages_and_registries/shared/components/package_path.vue';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
import PublishMethod from '~/packages_and_registries/package_registry/components/list/publish_method.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -29,14 +27,13 @@ describe('packages_list_row', () => {
const defaultProvide = {
isGroupPage: false,
+ canDeletePackages: true,
};
const packageWithoutTags = { ...packageData(), project: packageProject(), ...linksData };
const packageWithTags = { ...packageWithoutTags, tags: { nodes: packageTags() } };
- const packageCannotDestroy = { ...packageData(), ...linksData, canDestroy: false };
const findPackageTags = () => wrapper.findComponent(PackageTags);
- const findPackagePath = () => wrapper.findComponent(PackagePath);
const findDeleteDropdown = () => wrapper.findByTestId('action-delete');
const findPackageType = () => wrapper.findByTestId('package-type');
const findPackageLink = () => wrapper.findByTestId('details-link');
@@ -44,8 +41,7 @@ describe('packages_list_row', () => {
const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
const findPackageVersion = () => findLeftSecondaryInfos().findComponent(GlTruncate);
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
- const findCreatedDateText = () => wrapper.findByTestId('created-date');
- const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
+ const findRightSecondary = () => wrapper.findByTestId('right-secondary');
const findListItem = () => wrapper.findComponent(ListItem);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
const findPackageName = () => wrapper.findComponent(GlTruncate);
@@ -60,6 +56,7 @@ describe('packages_list_row', () => {
stubs: {
ListItem,
GlSprintf,
+ TimeagoTooltip,
},
propsData: {
packageEntity,
@@ -106,18 +103,11 @@ describe('packages_list_row', () => {
});
});
- describe('when it is group', () => {
- it('has a package path component', () => {
- mountComponent({ provide: { isGroupPage: true } });
-
- expect(findPackagePath().exists()).toBe(true);
- expect(findPackagePath().props()).toMatchObject({ path: 'gitlab-org/gitlab-test' });
- });
- });
-
describe('delete button', () => {
it('does not exist when package cannot be destroyed', () => {
- mountComponent({ packageEntity: packageCannotDestroy });
+ mountComponent({
+ packageEntity: { ...packageWithoutTags, canDestroy: false },
+ });
expect(findDeleteDropdown().exists()).toBe(false);
});
@@ -180,7 +170,10 @@ describe('packages_list_row', () => {
describe('left action template', () => {
it('does not render checkbox if not permitted', () => {
mountComponent({
- packageEntity: { ...packageWithoutTags, canDestroy: false },
+ provide: {
+ ...defaultProvide,
+ canDeletePackages: false,
+ },
});
expect(findBulkDeleteAction().exists()).toBe(false);
@@ -223,14 +216,6 @@ describe('packages_list_row', () => {
});
});
- it('if the pipeline exists show the author message', () => {
- mountComponent({
- packageEntity: { ...packageWithoutTags, pipelines: { nodes: packagePipelines() } },
- });
-
- expect(findLeftSecondaryInfos().text()).toContain('published by Administrator');
- });
-
it('has package type with middot', () => {
mountComponent();
@@ -247,13 +232,50 @@ describe('packages_list_row', () => {
expect(findPublishMethod().props('pipeline')).toEqual(packagePipelines()[0]);
});
- it('has the created date', () => {
- mountComponent();
+ it('if the package is published through CI show the author name', () => {
+ mountComponent({
+ packageEntity: { ...packageWithoutTags, pipelines: { nodes: packagePipelines() } },
+ });
+
+ expect(findRightSecondary().text()).toBe(`Published by Administrator, 1 month ago`);
+ });
- expect(findCreatedDateText().text()).toMatchInterpolatedText(PackagesListRow.i18n.createdAt);
- expect(findTimeAgoTooltip().props()).toMatchObject({
- time: packageData().createdAt,
+ it('if the package is published manually then dont show author name', () => {
+ mountComponent({
+ packageEntity: { ...packageWithoutTags },
});
+
+ expect(findRightSecondary().text()).toBe(`Published 1 month ago`);
+ });
+ });
+
+ describe('right info for a group registry', () => {
+ it('if the package is published through CI show the project and author name', () => {
+ mountComponent({
+ provide: {
+ ...defaultProvide,
+ isGroupPage: true,
+ },
+ packageEntity: { ...packageWithoutTags, pipelines: { nodes: packagePipelines() } },
+ });
+
+ expect(findRightSecondary().text()).toBe(
+ `Published to ${packageWithoutTags.project.name} by Administrator, 1 month ago`,
+ );
+ });
+
+ it('if the package is published manually dont show project and the author name', () => {
+ mountComponent({
+ provide: {
+ ...defaultProvide,
+ isGroupPage: true,
+ },
+ packageEntity: { ...packageWithoutTags },
+ });
+
+ expect(findRightSecondary().text()).toBe(
+ `Published to ${packageWithoutTags.project.name}, 1 month ago`,
+ );
});
});
});
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 483b7a9383d..fad8863e3d9 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
@@ -41,6 +41,10 @@ describe('packages_list', () => {
groupSettings: defaultPackageGroupSettings,
};
+ const defaultProvide = {
+ canDeletePackages: true,
+ };
+
const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
const findPackagesListLoader = () => wrapper.findComponent(PackagesListLoader);
@@ -52,8 +56,9 @@ describe('packages_list', () => {
const showMock = jest.fn();
- const mountComponent = (props) => {
+ const mountComponent = ({ props = {}, provide = defaultProvide } = {}) => {
wrapper = shallowMountExtended(PackagesList, {
+ provide,
propsData: {
...defaultProps,
...props,
@@ -75,7 +80,7 @@ describe('packages_list', () => {
describe('when is loading', () => {
beforeEach(() => {
- mountComponent({ isLoading: true });
+ mountComponent({ props: { isLoading: true } });
});
it('shows skeleton loader', () => {
@@ -109,6 +114,7 @@ describe('packages_list', () => {
title: '2 packages',
items: defaultProps.list,
pagination: defaultProps.pageInfo,
+ hiddenDelete: false,
isLoading: false,
});
});
@@ -137,6 +143,16 @@ describe('packages_list', () => {
});
});
+ describe('when the user does not have permission to destroy packages', () => {
+ beforeEach(() => {
+ mountComponent({ provide: { canDeletePackages: false } });
+ });
+
+ it('sets the hidden delete prop of registry list to true', () => {
+ expect(findRegistryList().props('hiddenDelete')).toBe(true);
+ });
+ });
+
describe.each`
description | finderFunction | deletePayload
${'when the user can destroy the package'} | ${findPackagesListRow} | ${firstPackage}
@@ -262,7 +278,7 @@ describe('packages_list', () => {
describe('when an error package is present', () => {
beforeEach(() => {
- mountComponent({ list: [firstPackage, errorPackage] });
+ mountComponent({ props: { list: [firstPackage, errorPackage] } });
return nextTick();
});
@@ -290,7 +306,7 @@ describe('packages_list', () => {
describe('when the list is empty', () => {
beforeEach(() => {
- mountComponent({ list: [] });
+ mountComponent({ props: { list: [] } });
});
it('show the empty slot', () => {
@@ -301,7 +317,7 @@ describe('packages_list', () => {
describe('pagination', () => {
beforeEach(() => {
- mountComponent({ pageInfo: { hasPreviousPage: true } });
+ mountComponent({ props: { pageInfo: { hasPreviousPage: true } } });
});
it('emits prev-page events when the prev event is fired', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 6995a4cc635..91dc02f8f39 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -9,7 +9,7 @@ export const packageTags = () => [
export const packagePipelines = (extend) => [
{
commitPath: '/namespace14/project14/-/commit/b83d6e391c22777fca1ed3012fce84f633d7fed0',
- createdAt: '2020-08-17T14:23:32Z',
+ createdAt: '2020-05-17T14:23:32Z',
id: 'gid://gitlab/Ci::Pipeline/36',
path: '/namespace14/project14/-/pipelines/36',
name: 'project14',
@@ -38,7 +38,7 @@ export const packageFiles = () => [
fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ad',
fileSha256: 'fileSha256',
size: '409600',
- createdAt: '2020-08-17T14:23:32Z',
+ createdAt: '2020-05-17T14:23:32Z',
downloadPath: 'downloadPath',
__typename: 'PackageFile',
},
@@ -49,7 +49,7 @@ export const packageFiles = () => [
fileSha1: 'be93151dc23ac34a82752444556fe79b32c7a1ss',
fileSha256: null,
size: '409600',
- createdAt: '2020-08-17T14:23:32Z',
+ createdAt: '2020-05-17T14:23:32Z',
downloadPath: 'downloadPath',
__typename: 'PackageFile',
},
@@ -92,6 +92,7 @@ export const dependencyLinks = () => [
export const packageProject = () => ({
id: '1',
+ name: 'gitlab-test',
fullPath: 'gitlab-org/gitlab-test',
webUrl: 'http://gdk.test:3000/gitlab-org/gitlab-test',
__typename: 'Project',
@@ -144,7 +145,7 @@ export const packageData = (extend) => ({
name: '@gitlab-org/package-15',
packageType: 'NPM',
version: '1.0.0',
- createdAt: '2020-08-17T14:23:32Z',
+ createdAt: '2020-05-17T14:23:32Z',
updatedAt: '2020-08-17T14:23:32Z',
lastDownloadedAt: '2021-08-17T14:23:32Z',
status: 'DEFAULT',
@@ -278,15 +279,12 @@ export const packagePipelinesQuery = (pipelines = packagePipelines()) => ({
},
});
-export const packageFilesQuery = ({ files = packageFiles(), pageInfo = {} } = {}) => ({
+export const packageFilesQuery = ({ files = packageFiles(), extendPagination = {} } = {}) => ({
data: {
package: {
id: 'gid://gitlab/Packages::Package/111',
packageFiles: {
- pageInfo: {
- hasNextPage: true,
- ...pageInfo,
- },
+ pageInfo: pagination(extendPagination),
nodes: files,
__typename: 'PackageFileConnection',
},
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
index 2ee24200ed3..0d262036ee7 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
@@ -219,7 +219,11 @@ describe('PackagesListApp', () => {
await waitForPromises();
expect(resolver).toHaveBeenCalledWith(
- expect.objectContaining({ before: pagination().startCursor, last: GRAPHQL_PAGE_SIZE }),
+ expect.objectContaining({
+ first: null,
+ before: pagination().startCursor,
+ last: GRAPHQL_PAGE_SIZE,
+ }),
);
});
});
diff --git a/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js b/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js
index b1d2e443d54..d90393d8ab3 100644
--- a/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js
+++ b/spec/frontend/pages/admin/jobs/components/cancel_jobs_modal_spec.js
@@ -1,10 +1,11 @@
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
import CancelJobsModal from '~/pages/admin/jobs/components/cancel_jobs_modal.vue';
+import { setVueErrorHandler } from '../../../../__helpers__/set_vue_error_handler';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
@@ -45,8 +46,6 @@ describe('Cancel jobs modal', () => {
});
it('displays error if canceling jobs failed', async () => {
- Vue.config.errorHandler = () => {}; // silencing thrown error
-
const dummyError = new Error('canceling jobs failed');
// TODO: We can't use axios-mock-adapter because our current version
// does not support responseURL
@@ -57,6 +56,7 @@ describe('Cancel jobs modal', () => {
return Promise.reject(dummyError);
});
+ setVueErrorHandler({ instance: wrapper.vm, handler: () => {} }); // silencing thrown error
wrapper.findComponent(GlModal).vm.$emit('primary');
await nextTick();
diff --git a/spec/frontend/pages/groups/new/components/app_spec.js b/spec/frontend/pages/groups/new/components/app_spec.js
index 19240f1a044..baf0ca2beca 100644
--- a/spec/frontend/pages/groups/new/components/app_spec.js
+++ b/spec/frontend/pages/groups/new/components/app_spec.js
@@ -1,4 +1,7 @@
import { shallowMount } from '@vue/test-utils';
+import GROUP_IMPORT_SVG_URL from '@gitlab/svgs/dist/illustrations/group-import.svg?url';
+import GROUP_NEW_SVG_URL from '@gitlab/svgs/dist/illustrations/group-new.svg?url';
+
import App from '~/pages/groups/new/components/app.vue';
import NewNamespacePage from '~/vue_shared/new_namespace/new_namespace_page.vue';
@@ -27,6 +30,7 @@ describe('App component', () => {
{ href: '#', text: 'New group' },
]);
expect(findCreateGroupPanel().title).toBe('Create group');
+ expect(findCreateGroupPanel().imageSrc).toBe(GROUP_NEW_SVG_URL);
});
it('creates correct component for subgroup creation', () => {
@@ -45,5 +49,6 @@ describe('App component', () => {
]);
expect(findCreateGroupPanel().title).toBe('Create subgroup');
expect(findCreateGroupPanel().detailProps).toEqual(detailProps);
+ expect(findCreateGroupPanel().imageSrc).toBe(GROUP_IMPORT_SVG_URL);
});
});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index 07d05293a3c..197a76f2c86 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -219,4 +219,17 @@ describe('Interval Pattern Input Component', () => {
expect(findIcon().exists()).toBe(false);
});
});
+
+ describe('cronValue event', () => {
+ it('emits cronValue event with cron value', async () => {
+ createWrapper();
+
+ findCustomInput().element.value = '0 16 * * *';
+ findCustomInput().trigger('input');
+
+ await nextTick();
+
+ expect(wrapper.emitted()).toEqual({ cronValue: [['0 16 * * *']] });
+ });
+ });
});
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 1a3eb86a00e..db889abad88 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -7,16 +7,11 @@ import { mockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import WikiForm from '~/pages/shared/wikis/components/wiki_form.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
-import {
- CONTENT_EDITOR_LOADED_ACTION,
- SAVED_USING_CONTENT_EDITOR_ACTION,
- WIKI_CONTENT_EDITOR_TRACKING_LABEL,
- WIKI_FORMAT_LABEL,
- WIKI_FORMAT_UPDATED_ACTION,
-} from '~/pages/shared/wikis/constants';
+import { WIKI_FORMAT_LABEL, WIKI_FORMAT_UPDATED_ACTION } from '~/pages/shared/wikis/constants';
import { DRAWIO_ORIGIN } from 'spec/test_constants';
jest.mock('~/emoji');
+jest.mock('~/lib/graphql');
describe('WikiForm', () => {
let wrapper;
@@ -94,6 +89,15 @@ describe('WikiForm', () => {
GlFormInput,
GlFormGroup,
},
+ mocks: {
+ $apollo: {
+ queries: {
+ currentUser: {
+ loading: false,
+ },
+ },
+ },
+ },
}),
);
}
@@ -224,7 +228,22 @@ describe('WikiForm', () => {
});
it('triggers wiki format tracking event', () => {
- expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'wiki_format_updated', {
+ extra: {
+ old_format: 'markdown',
+ project_path: '/project/path/-/wikis/home',
+ value: 'markdown',
+ },
+ label: 'wiki_format',
+ });
+ });
+
+ it('tracks editor type used', () => {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'editor_type_used', {
+ context: 'Wiki',
+ editorType: 'editor_type_plain_text_editor',
+ label: 'editor_tracking',
+ });
});
it('does not trim page content', () => {
@@ -306,12 +325,6 @@ describe('WikiForm', () => {
expect(findFormat().element.getAttribute('disabled')).toBeDefined();
});
- it('sends tracking event when editor loads', () => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, CONTENT_EDITOR_LOADED_ACTION, {
- label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
- });
- });
-
describe('when triggering form submit', () => {
const updatedMarkdown = 'hello **world**';
@@ -321,10 +334,6 @@ describe('WikiForm', () => {
});
it('triggers tracking events on form submit', () => {
- expect(trackingSpy).toHaveBeenCalledWith(undefined, SAVED_USING_CONTENT_EDITOR_ACTION, {
- label: WIKI_CONTENT_EDITOR_TRACKING_LABEL,
- });
-
expect(trackingSpy).toHaveBeenCalledWith(undefined, WIKI_FORMAT_UPDATED_ACTION, {
label: WIKI_FORMAT_LABEL,
extra: {
diff --git a/spec/frontend/pipeline_wizard/components/commit_spec.js b/spec/frontend/pipeline_wizard/components/commit_spec.js
index 7095525e948..bb9a4b85e0e 100644
--- a/spec/frontend/pipeline_wizard/components/commit_spec.js
+++ b/spec/frontend/pipeline_wizard/components/commit_spec.js
@@ -141,10 +141,6 @@ describe('Pipeline Wizard - Commit Page', () => {
it('emits a done event', () => {
expect(wrapper.emitted().done.length).toBe(1);
});
-
- afterEach(() => {
- jest.clearAllMocks();
- });
});
describe('failed commit', () => {
@@ -167,10 +163,6 @@ describe('Pipeline Wizard - Commit Page', () => {
it('will not emit a done event', () => {
expect(wrapper.emitted().done?.length).toBeUndefined();
});
-
- afterEach(() => {
- jest.clearAllMocks();
- });
});
});
diff --git a/spec/frontend/pipeline_wizard/components/editor_spec.js b/spec/frontend/pipeline_wizard/components/editor_spec.js
index 6d7d4363189..2284c875f58 100644
--- a/spec/frontend/pipeline_wizard/components/editor_spec.js
+++ b/spec/frontend/pipeline_wizard/components/editor_spec.js
@@ -1,42 +1,58 @@
import { mount } from '@vue/test-utils';
import { Document } from 'yaml';
import YamlEditor from '~/pipeline_wizard/components/editor.vue';
+import SourceEditor from '~/editor/source_editor';
describe('Pages Yaml Editor wrapper', () => {
let wrapper;
+ const defaultDoc = new Document({ foo: 'bar' });
+
const defaultOptions = {
- propsData: { doc: new Document({ foo: 'bar' }), filename: 'foo.yml' },
+ propsData: { doc: defaultDoc, filename: 'foo.yml' },
+ };
+
+ const getLatestValue = () => {
+ const latest = wrapper.emitted('update:yaml').pop();
+ return latest[0];
};
describe('mount hook', () => {
beforeEach(() => {
+ jest.spyOn(SourceEditor.prototype, 'createInstance');
+
wrapper = mount(YamlEditor, defaultOptions);
});
- it('editor is mounted', () => {
- expect(wrapper.vm.editor).not.toBeUndefined();
- expect(wrapper.find('.gl-source-editor').exists()).toBe(true);
+ it('creates a source editor instance', () => {
+ expect(SourceEditor.prototype.createInstance).toHaveBeenCalledWith({
+ el: wrapper.element,
+ blobPath: 'foo.yml',
+ language: 'yaml',
+ });
+ });
+
+ it('editor is mounted in the wrapper', () => {
+ expect(wrapper.find('.gl-source-editor.monaco-editor').exists()).toBe(true);
+ });
+
+ it("causes the editor's value to be set to the stringified document", () => {
+ expect(getLatestValue()).toEqual(defaultDoc.toString());
});
});
describe('watchers', () => {
+ beforeEach(() => {
+ wrapper = mount(YamlEditor, defaultOptions);
+ });
+
describe('doc', () => {
const doc = new Document({ baz: ['bar'] });
- beforeEach(() => {
- wrapper = mount(YamlEditor, defaultOptions);
- });
-
- it("causes the editor's value to be set to the stringified document", async () => {
- await wrapper.setProps({ doc });
- expect(wrapper.vm.editor.getValue()).toEqual(doc.toString());
- });
-
it('emits an update:yaml event with the yaml representation of doc', async () => {
await wrapper.setProps({ doc });
- const changeEvents = wrapper.emitted('update:yaml');
- expect(changeEvents[2]).toEqual([doc.toString()]);
+
+ expect(getLatestValue()).toEqual(doc.toString());
});
it('does not cause the touch event to be emitted', () => {
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js
new file mode 100644
index 00000000000..4ba1b82e971
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_job_details_spec.js
@@ -0,0 +1,252 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlIcon, GlLink } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import FailedJobDetails from '~/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue';
+import RetryMrFailedJobMutation from '~/pipelines/graphql/mutations/retry_mr_failed_job.mutation.graphql';
+import { job } from './mock';
+
+Vue.use(VueApollo);
+jest.mock('~/alert');
+
+const createFakeEvent = () => ({ stopPropagation: jest.fn() });
+
+describe('FailedJobDetails component', () => {
+ let wrapper;
+ let mockRetryResponse;
+
+ const retrySuccessResponse = {
+ data: {
+ jobRetry: {
+ errors: [],
+ },
+ },
+ };
+
+ const defaultProps = {
+ job,
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ const handlers = [[RetryMrFailedJobMutation, mockRetryResponse]];
+
+ wrapper = shallowMountExtended(FailedJobDetails, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ apolloProvider: createMockApollo(handlers),
+ });
+ };
+
+ const findArrowIcon = () => wrapper.findComponent(GlIcon);
+ const findJobId = () => wrapper.findComponent(GlLink);
+ const findHiddenJobLog = () => wrapper.findByTestId('log-is-hidden');
+ const findVisibleJobLog = () => wrapper.findByTestId('log-is-visible');
+ const findJobName = () => wrapper.findByText(defaultProps.job.name);
+ const findRetryButton = () => wrapper.findByLabelText('Retry');
+ const findRow = () => wrapper.findByTestId('widget-row');
+ const findStageName = () => wrapper.findByText(defaultProps.job.stage.name);
+
+ beforeEach(() => {
+ mockRetryResponse = jest.fn();
+ mockRetryResponse.mockResolvedValue(retrySuccessResponse);
+ });
+
+ describe('ui', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the job name', () => {
+ expect(findJobName().exists()).toBe(true);
+ });
+
+ it('renders the stage name', () => {
+ expect(findStageName().exists()).toBe(true);
+ });
+
+ it('renders the job id as a link', () => {
+ const jobId = getIdFromGraphQLId(defaultProps.job.id);
+
+ expect(findJobId().exists()).toBe(true);
+ expect(findJobId().text()).toContain(String(jobId));
+ });
+
+ it('does not renders the job lob', () => {
+ expect(findHiddenJobLog().exists()).toBe(true);
+ expect(findVisibleJobLog().exists()).toBe(false);
+ });
+ });
+
+ describe('Retry action', () => {
+ describe('when the job is not retryable', () => {
+ beforeEach(() => {
+ createComponent({ props: { job: { ...job, retryable: false } } });
+ });
+
+ it('disables the retry button', () => {
+ expect(findRetryButton().props().disabled).toBe(true);
+ });
+ });
+
+ describe('when the job is retryable', () => {
+ describe('and user has permission to update the build', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('enables the retry button', () => {
+ expect(findRetryButton().props().disabled).toBe(false);
+ });
+
+ describe('when clicking on the retry button', () => {
+ it('passes the loading state to the button', async () => {
+ await findRetryButton().vm.$emit('click', createFakeEvent());
+
+ expect(findRetryButton().props().loading).toBe(true);
+ });
+
+ describe('and it succeeds', () => {
+ beforeEach(async () => {
+ findRetryButton().vm.$emit('click', createFakeEvent());
+ await waitForPromises();
+ });
+
+ it('is no longer loading', () => {
+ expect(findRetryButton().props().loading).toBe(false);
+ });
+
+ it('calls the retry mutation', () => {
+ expect(mockRetryResponse).toHaveBeenCalled();
+ expect(mockRetryResponse).toHaveBeenCalledWith({
+ id: job.id,
+ });
+ });
+
+ it('emits the `retried-job` event', () => {
+ expect(wrapper.emitted('job-retried')).toStrictEqual([[job.name]]);
+ });
+ });
+
+ describe('and it fails', () => {
+ const customErrorMsg = 'Custom error message from API';
+
+ beforeEach(async () => {
+ mockRetryResponse.mockResolvedValue({
+ data: { jobRetry: { errors: [customErrorMsg] } },
+ });
+ findRetryButton().vm.$emit('click', createFakeEvent());
+
+ await waitForPromises();
+ });
+
+ it('shows an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: customErrorMsg });
+ });
+
+ it('does not emits the `refetch-jobs` event', () => {
+ expect(wrapper.emitted('refetch-jobs')).toBeUndefined();
+ });
+ });
+ });
+ });
+
+ describe('and user does not have permission to update the build', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { job: { ...job, retryable: true, userPermissions: { updateBuild: false } } },
+ });
+ });
+
+ it('disables the retry button', () => {
+ expect(findRetryButton().props().disabled).toBe(true);
+ });
+ });
+ });
+ });
+
+ describe('Job log', () => {
+ describe('without permissions', () => {
+ beforeEach(async () => {
+ createComponent({ props: { job: { ...job, userPermissions: { readBuild: false } } } });
+ await findRow().trigger('click');
+ });
+
+ it('does not renders the received html of the job log', () => {
+ expect(findVisibleJobLog().html()).not.toContain(defaultProps.job.trace.htmlSummary);
+ });
+
+ it('shows a permission error message', () => {
+ expect(findVisibleJobLog().text()).toBe(
+ "You do not have permission to read this job's log",
+ );
+ });
+ });
+
+ describe('with permissions', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when clicking on the row', () => {
+ beforeEach(async () => {
+ await findRow().trigger('click');
+ });
+
+ describe('while collapsed', () => {
+ it('expands the job log', () => {
+ expect(findHiddenJobLog().exists()).toBe(false);
+ expect(findVisibleJobLog().exists()).toBe(true);
+ });
+
+ it('renders the down arrow', () => {
+ expect(findArrowIcon().props().name).toBe('chevron-down');
+ });
+
+ it('renders the received html of the job log', () => {
+ expect(findVisibleJobLog().html()).toContain(defaultProps.job.trace.htmlSummary);
+ });
+ });
+
+ describe('while expanded', () => {
+ it('collapes the job log', async () => {
+ expect(findHiddenJobLog().exists()).toBe(false);
+ expect(findVisibleJobLog().exists()).toBe(true);
+
+ await findRow().trigger('click');
+
+ expect(findHiddenJobLog().exists()).toBe(true);
+ expect(findVisibleJobLog().exists()).toBe(false);
+ });
+
+ it('renders the right arrow', async () => {
+ expect(findArrowIcon().props().name).toBe('chevron-down');
+
+ await findRow().trigger('click');
+
+ expect(findArrowIcon().props().name).toBe('chevron-right');
+ });
+ });
+ });
+
+ describe('when clicking on a link element within the row', () => {
+ it('does not expands/collapse the job log', async () => {
+ expect(findHiddenJobLog().exists()).toBe(true);
+ expect(findVisibleJobLog().exists()).toBe(false);
+ expect(findArrowIcon().props().name).toBe('chevron-right');
+
+ await findJobId().vm.$emit('click');
+
+ expect(findHiddenJobLog().exists()).toBe(true);
+ expect(findVisibleJobLog().exists()).toBe(false);
+ expect(findArrowIcon().props().name).toBe('chevron-right');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js
new file mode 100644
index 00000000000..fc8263c6c4d
--- /dev/null
+++ b/spec/frontend/pipelines/components/pipelines_list/failure_widget/failed_jobs_list_spec.js
@@ -0,0 +1,236 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+
+import { GlLoadingIcon, GlToast } from '@gitlab/ui';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import FailedJobsList from '~/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue';
+import FailedJobDetails from '~/pipelines/components/pipelines_list/failure_widget/failed_job_details.vue';
+import * as utils from '~/pipelines/components/pipelines_list/failure_widget/utils';
+import getPipelineFailedJobs from '~/pipelines/graphql/queries/get_pipeline_failed_jobs.query.graphql';
+import { failedJobsMock, failedJobsMock2, failedJobsMockEmpty, activeFailedJobsMock } from './mock';
+
+Vue.use(VueApollo);
+Vue.use(GlToast);
+
+jest.mock('~/alert');
+
+describe('FailedJobsList component', () => {
+ let wrapper;
+ let mockFailedJobsResponse;
+ const showToast = jest.fn();
+
+ const defaultProps = {
+ graphqlResourceEtag: 'api/graphql',
+ isPipelineActive: false,
+ pipelineIid: 1,
+ pipelinePath: '/pipelines/1',
+ };
+
+ const defaultProvide = {
+ fullPath: 'namespace/project/',
+ graphqlPath: 'api/graphql',
+ };
+
+ const createComponent = ({ props = {}, provide } = {}) => {
+ const handlers = [[getPipelineFailedJobs, mockFailedJobsResponse]];
+ const mockApollo = createMockApollo(handlers);
+
+ wrapper = shallowMountExtended(FailedJobsList, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ apolloProvider: mockApollo,
+ mocks: {
+ $toast: {
+ show: showToast,
+ },
+ },
+ });
+ };
+
+ const findAllHeaders = () => wrapper.findAllByTestId('header');
+ const findFailedJobRows = () => wrapper.findAllComponents(FailedJobDetails);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findNoFailedJobsText = () => wrapper.findByText('No failed jobs in this pipeline 🎉');
+
+ beforeEach(() => {
+ mockFailedJobsResponse = jest.fn();
+ });
+
+ describe('when loading failed jobs', () => {
+ beforeEach(() => {
+ mockFailedJobsResponse.mockResolvedValue(failedJobsMock);
+ createComponent();
+ });
+
+ it('shows a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('when failed jobs have loaded', () => {
+ beforeEach(async () => {
+ mockFailedJobsResponse.mockResolvedValue(failedJobsMock);
+ jest.spyOn(utils, 'sortJobsByStatus');
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('does not renders a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('renders table column', () => {
+ expect(findAllHeaders()).toHaveLength(4);
+ });
+
+ it('shows the list of failed jobs', () => {
+ expect(findFailedJobRows()).toHaveLength(
+ failedJobsMock.data.project.pipeline.jobs.nodes.length,
+ );
+ });
+
+ it('does not renders the empty state', () => {
+ expect(findNoFailedJobsText().exists()).toBe(false);
+ });
+
+ it('calls sortJobsByStatus', () => {
+ expect(utils.sortJobsByStatus).toHaveBeenCalledWith(
+ failedJobsMock.data.project.pipeline.jobs.nodes,
+ );
+ });
+ });
+
+ describe('when there are no failed jobs', () => {
+ beforeEach(async () => {
+ mockFailedJobsResponse.mockResolvedValue(failedJobsMockEmpty);
+ jest.spyOn(utils, 'sortJobsByStatus');
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('renders the empty state', () => {
+ expect(findNoFailedJobsText().exists()).toBe(true);
+ });
+ });
+
+ describe('polling', () => {
+ it.each`
+ isGraphqlActive | text
+ ${true} | ${'polls'}
+ ${false} | ${'does not poll'}
+ `(`$text when isGraphqlActive: $isGraphqlActive`, async ({ isGraphqlActive }) => {
+ const defaultCount = 2;
+ const newCount = 1;
+
+ const expectedCount = isGraphqlActive ? newCount : defaultCount;
+ const expectedCallCount = isGraphqlActive ? 2 : 1;
+ const mockResponse = isGraphqlActive ? activeFailedJobsMock : failedJobsMock;
+
+ // Second result is to simulate polling with a different response
+ mockFailedJobsResponse.mockResolvedValueOnce(mockResponse);
+ mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock2);
+
+ createComponent();
+ await waitForPromises();
+
+ // Initially, we get the first response which is always the default
+ expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1);
+ expect(findFailedJobRows()).toHaveLength(defaultCount);
+
+ jest.advanceTimersByTime(10000);
+ await waitForPromises();
+
+ expect(mockFailedJobsResponse).toHaveBeenCalledTimes(expectedCallCount);
+ expect(findFailedJobRows()).toHaveLength(expectedCount);
+ });
+ });
+
+ describe('when a REST action occurs', () => {
+ beforeEach(() => {
+ // Second result is to simulate polling with a different response
+ mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock);
+ mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock2);
+ });
+
+ it.each([true, false])('triggers a refetch of the jobs count', async (isPipelineActive) => {
+ const defaultCount = 2;
+ const newCount = 1;
+
+ createComponent({ props: { isPipelineActive } });
+ await waitForPromises();
+
+ // Initially, we get the first response which is always the default
+ expect(mockFailedJobsResponse).toHaveBeenCalledTimes(1);
+ expect(findFailedJobRows()).toHaveLength(defaultCount);
+
+ wrapper.setProps({ isPipelineActive: !isPipelineActive });
+ await waitForPromises();
+
+ expect(mockFailedJobsResponse).toHaveBeenCalledTimes(2);
+ expect(findFailedJobRows()).toHaveLength(newCount);
+ });
+ });
+
+ describe('when an error occurs loading jobs', () => {
+ const errorMessage = "We couldn't fetch jobs for you because you are not qualified";
+
+ beforeEach(async () => {
+ mockFailedJobsResponse.mockRejectedValue({ message: errorMessage });
+
+ createComponent();
+
+ await waitForPromises();
+ });
+ it('does not renders a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+
+ it('calls create Alert with the error message and danger variant', () => {
+ expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' });
+ });
+ });
+
+ describe('when `refetch-jobs` job is fired from the widget', () => {
+ beforeEach(async () => {
+ mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock);
+ mockFailedJobsResponse.mockResolvedValueOnce(failedJobsMock2);
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('refetches all failed jobs', async () => {
+ expect(findFailedJobRows()).not.toHaveLength(
+ failedJobsMock2.data.project.pipeline.jobs.nodes.length,
+ );
+
+ await findFailedJobRows().at(0).vm.$emit('job-retried', 'job-name');
+ await waitForPromises();
+
+ expect(findFailedJobRows()).toHaveLength(
+ failedJobsMock2.data.project.pipeline.jobs.nodes.length,
+ );
+ });
+
+ it('shows a toast message', async () => {
+ await findFailedJobRows().at(0).vm.$emit('job-retried', 'job-name');
+ await waitForPromises();
+
+ expect(showToast).toHaveBeenCalledWith('job-name job is being retried');
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js
index a4c90fa3876..b047b57fc34 100644
--- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js
+++ b/spec/frontend/pipelines/components/pipelines_list/failure_widget/mock.js
@@ -13,13 +13,17 @@ export const job = {
},
name: 'job-name',
retried: false,
+ retryable: true,
stage: {
id: '1',
name: 'build',
},
trace: {
- htmlSummary:
- '<span>To install the missing version, run `gem install bundler:2.4.13`<br/>\tfrom /System/Library/Frameworks/Ruby.framework/Versions/2.6/usr/lib/ruby/2.6.0/rubygems.rb:302:in `activate_bin_path\'<br/>\tfrom /usr/bin/bundle:23:in `&lt;main>\'<br/></span><div class="section-start" data-timestamp="1685044123" data-section="upload-artifacts-on-failure" role="button"></div><span class="term-fg-l-cyan term-bold section section-header js-s-upload-artifacts-on-failure">Uploading artifacts for failed job</span><span class="section section-header js-s-upload-artifacts-on-failure"><br/></span><span class="term-fg-l-green term-bold section line js-s-upload-artifacts-on-failure">Uploading artifacts...</span><span class="section line js-s-upload-artifacts-on-failure"><br/>Runtime platform </span><span class="section line js-s-upload-artifacts-on-failure"> arch</span><span class="section line js-s-upload-artifacts-on-failure">=arm64 os</span><span class="section line js-s-upload-artifacts-on-failure">=darwin pid</span><span class="section line js-s-upload-artifacts-on-failure">=16706 revision</span><span class="section line js-s-upload-artifacts-on-failure">=43b2dc3d version</span><span class="section line js-s-upload-artifacts-on-failure">=15.4.0<br/></span><span class="term-fg-yellow section line js-s-upload-artifacts-on-failure">WARNING: rspec.xml: no matching files. Ensure that the artifact path is relative to the working directory</span><span class="section line js-s-upload-artifacts-on-failure"> <br/></span><span class="term-fg-l-red term-bold section line js-s-upload-artifacts-on-failure">ERROR: No files to upload </span><span class="section line js-s-upload-artifacts-on-failure"> <br/></span><div class="section-end" data-section="upload-artifacts-on-failure"></div><span class="term-fg-l-red term-bold">ERROR: Job failed: exit status 1<br/></span><span><br/></span>',
+ htmlSummary: '<h1>Hello</h1>',
+ },
+ userPermissions: {
+ readBuild: true,
+ updateBuild: true,
},
webPath: '/',
};
@@ -30,16 +34,44 @@ export const allowedToFailJob = {
allowFailure: true,
};
-export const failedJobsMock = {
- data: {
- project: {
- id: 'gid://gitlab/Project/20',
- pipeline: {
- id: 'gid://gitlab/Pipeline/20',
- jobs: {
- nodes: [allowedToFailJob, job],
+export const createFailedJobsMockCount = ({ count = 4, active = false } = {}) => {
+ return {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ id: 'gid://gitlab/Pipeline/20',
+ active,
+ jobs: {
+ count,
+ },
},
},
},
- },
+ };
+};
+
+const createFailedJobsMock = (nodes, active = false) => {
+ return {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/20',
+ pipeline: {
+ active,
+ id: 'gid://gitlab/Pipeline/20',
+ jobs: {
+ count: nodes.length,
+ nodes,
+ },
+ },
+ },
+ },
+ };
};
+
+export const failedJobsMock = createFailedJobsMock([allowedToFailJob, job]);
+export const failedJobsMockEmpty = createFailedJobsMock([]);
+
+export const activeFailedJobsMock = createFailedJobsMock([allowedToFailJob, job], true);
+
+export const failedJobsMock2 = createFailedJobsMock([job]);
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js
index df6d114f683..c1a885391e9 100644
--- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget_spec.js
@@ -1,25 +1,16 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-
-import { GlButton, GlIcon, GlLoadingIcon, GlPopover } from '@gitlab/ui';
-import createMockApollo from 'helpers/mock_apollo_helper';
+import { GlButton, GlIcon, GlPopover } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import waitForPromises from 'helpers/wait_for_promises';
import PipelineFailedJobsWidget from '~/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue';
-import { createAlert } from '~/alert';
-import WidgetFailedJobRow from '~/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row.vue';
-import * as utils from '~/pipelines/components/pipelines_list/failure_widget/utils';
-import getPipelineFailedJobs from '~/pipelines/graphql/queries/get_pipeline_failed_jobs.query.graphql';
-import { failedJobsMock } from './mock';
+import FailedJobsList from '~/pipelines/components/pipelines_list/failure_widget/failed_jobs_list.vue';
-Vue.use(VueApollo);
jest.mock('~/alert');
describe('PipelineFailedJobsWidget component', () => {
let wrapper;
- let mockFailedJobsResponse;
const defaultProps = {
+ failedJobsCount: 4,
+ isPipelineActive: false,
pipelineIid: 1,
pipelinePath: '/pipelines/1',
};
@@ -28,10 +19,7 @@ describe('PipelineFailedJobsWidget component', () => {
fullPath: 'namespace/project/',
};
- const createComponent = ({ props = {}, provide } = {}) => {
- const handlers = [[getPipelineFailedJobs, mockFailedJobsResponse]];
- const mockApollo = createMockApollo(handlers);
-
+ const createComponent = ({ props = {}, provide = {} } = {}) => {
wrapper = shallowMountExtended(PipelineFailedJobsWidget, {
propsData: {
...defaultProps,
@@ -41,29 +29,35 @@ describe('PipelineFailedJobsWidget component', () => {
...defaultProvide,
...provide,
},
- apolloProvider: mockApollo,
});
};
- const findAllHeaders = () => wrapper.findAllByTestId('header');
const findFailedJobsButton = () => wrapper.findComponent(GlButton);
- const findFailedJobRows = () => wrapper.findAllComponents(WidgetFailedJobRow);
+ const findFailedJobsList = () => wrapper.findAllComponents(FailedJobsList);
const findInfoIcon = () => wrapper.findComponent(GlIcon);
const findInfoPopover = () => wrapper.findComponent(GlPopover);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- beforeEach(() => {
- mockFailedJobsResponse = jest.fn();
+ describe('when there are no failed jobs', () => {
+ beforeEach(() => {
+ createComponent({ props: { failedJobsCount: 0 } });
+ });
+
+ it('renders the show failed jobs button with a count of 0', () => {
+ expect(findFailedJobsButton().exists()).toBe(true);
+ expect(findFailedJobsButton().text()).toBe('Show failed jobs (0)');
+ });
});
- describe('ui', () => {
+ describe('when there are failed jobs', () => {
beforeEach(() => {
createComponent();
});
- it('renders the show failed jobs button', () => {
+ it('renders the show failed jobs button with correct count', () => {
expect(findFailedJobsButton().exists()).toBe(true);
- expect(findFailedJobsButton().text()).toBe('Show failed jobs');
+ expect(findFailedJobsButton().text()).toBe(
+ `Show failed jobs (${defaultProps.failedJobsCount})`,
+ );
});
it('renders the info icon', () => {
@@ -74,71 +68,53 @@ describe('PipelineFailedJobsWidget component', () => {
expect(findInfoPopover().exists()).toBe(true);
});
- it('does not show the list of failed jobs', () => {
- expect(findFailedJobRows()).toHaveLength(0);
+ it('does not render the failed jobs widget', () => {
+ expect(findFailedJobsList().exists()).toBe(false);
});
});
- describe('when loading failed jobs', () => {
+ describe('when the job button is clicked', () => {
beforeEach(async () => {
- mockFailedJobsResponse.mockResolvedValue(failedJobsMock);
createComponent();
await findFailedJobsButton().vm.$emit('click');
});
- it('shows a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(true);
+ it('renders the failed jobs widget', () => {
+ expect(findFailedJobsList().exists()).toBe(true);
});
});
- describe('when failed jobs have loaded', () => {
- beforeEach(async () => {
- mockFailedJobsResponse.mockResolvedValue(failedJobsMock);
- jest.spyOn(utils, 'sortJobsByStatus');
-
+ describe('when the job count changes', () => {
+ beforeEach(() => {
createComponent();
-
- await findFailedJobsButton().vm.$emit('click');
- await waitForPromises();
- });
- it('does not renders a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(false);
});
- it('renders table column', () => {
- expect(findAllHeaders()).toHaveLength(3);
- });
+ describe('from the prop', () => {
+ it('updates the job count', async () => {
+ const newJobCount = 12;
- it('shows the list of failed jobs', () => {
- expect(findFailedJobRows()).toHaveLength(
- failedJobsMock.data.project.pipeline.jobs.nodes.length,
- );
- });
+ expect(findFailedJobsButton().text()).toContain(String(defaultProps.failedJobsCount));
- it('calls sortJobsByStatus', () => {
- expect(utils.sortJobsByStatus).toHaveBeenCalledWith(
- failedJobsMock.data.project.pipeline.jobs.nodes,
- );
+ await wrapper.setProps({ failedJobsCount: newJobCount });
+
+ expect(findFailedJobsButton().text()).toContain(String(newJobCount));
+ });
});
- });
- describe('when an error occurs loading jobs', () => {
- const errorMessage = "We couldn't fetch jobs for you because you are not qualified";
+ describe('from the event', () => {
+ beforeEach(async () => {
+ await findFailedJobsButton().vm.$emit('click');
+ });
- beforeEach(async () => {
- mockFailedJobsResponse.mockRejectedValue({ message: errorMessage });
+ it('updates the job count', async () => {
+ const newJobCount = 12;
- createComponent();
+ expect(findFailedJobsButton().text()).toContain(String(defaultProps.failedJobsCount));
- await findFailedJobsButton().vm.$emit('click');
- await waitForPromises();
- });
- it('does not renders a loading icon', () => {
- expect(findLoadingIcon().exists()).toBe(false);
- });
+ await findFailedJobsList().at(0).vm.$emit('failed-jobs-count', newJobCount);
- it('calls create Alert with the error message and danger variant', () => {
- expect(createAlert).toHaveBeenCalledWith({ message: errorMessage, variant: 'danger' });
+ expect(findFailedJobsButton().text()).toContain(String(newJobCount));
+ });
});
});
});
diff --git a/spec/frontend/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row_spec.js b/spec/frontend/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row_spec.js
deleted file mode 100644
index dfc2806840f..00000000000
--- a/spec/frontend/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row_spec.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import { GlIcon, GlLink } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import WidgetFailedJobRow from '~/pipelines/components/pipelines_list/failure_widget/widget_failed_job_row.vue';
-
-describe('WidgetFailedJobRow component', () => {
- let wrapper;
-
- const defaultProps = {
- job: {
- id: 'gid://gitlab/Ci::Build/5240',
- detailedStatus: {
- group: 'running',
- icon: 'icon_status_running',
- },
- name: 'my-job',
- stage: {
- name: 'build',
- },
- trace: {
- htmlSummary: '<h1>job log</h1>',
- },
- webpath: '/',
- },
- };
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = shallowMountExtended(WidgetFailedJobRow, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- });
- };
-
- const findArrowIcon = () => wrapper.findComponent(GlIcon);
- const findJobCiStatus = () => wrapper.findComponent(CiIcon);
- const findJobId = () => wrapper.findComponent(GlLink);
- const findHiddenJobLog = () => wrapper.findByTestId('log-is-hidden');
- const findVisibleJobLog = () => wrapper.findByTestId('log-is-visible');
- const findJobName = () => wrapper.findByText(defaultProps.job.name);
- const findRow = () => wrapper.findByTestId('widget-row');
- const findStageName = () => wrapper.findByText(defaultProps.job.stage.name);
-
- describe('ui', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders the job name', () => {
- expect(findJobName().exists()).toBe(true);
- });
-
- it('renders the stage name', () => {
- expect(findStageName().exists()).toBe(true);
- });
-
- it('renders the job id as a link', () => {
- const jobId = getIdFromGraphQLId(defaultProps.job.id);
-
- expect(findJobId().exists()).toBe(true);
- expect(findJobId().text()).toContain(String(jobId));
- });
-
- it('renders the ci status badge', () => {
- expect(findJobCiStatus().exists()).toBe(true);
- });
-
- it('renders the right arrow', () => {
- expect(findArrowIcon().props().name).toBe('chevron-right');
- });
-
- it('does not renders the job lob', () => {
- expect(findHiddenJobLog().exists()).toBe(true);
- expect(findVisibleJobLog().exists()).toBe(false);
- });
- });
-
- describe('Job log', () => {
- beforeEach(() => {
- createComponent();
- });
-
- describe('when clicking on the row', () => {
- beforeEach(async () => {
- await findRow().trigger('click');
- });
-
- describe('while collapsed', () => {
- it('expands the job log', () => {
- expect(findHiddenJobLog().exists()).toBe(false);
- expect(findVisibleJobLog().exists()).toBe(true);
- });
-
- it('renders the down arrow', () => {
- expect(findArrowIcon().props().name).toBe('chevron-down');
- });
-
- it('renders the received html', () => {
- expect(findVisibleJobLog().html()).toContain(defaultProps.job.trace.htmlSummary);
- });
- });
-
- describe('while expanded', () => {
- it('collapes the job log', async () => {
- expect(findHiddenJobLog().exists()).toBe(false);
- expect(findVisibleJobLog().exists()).toBe(true);
-
- await findRow().trigger('click');
-
- expect(findHiddenJobLog().exists()).toBe(true);
- expect(findVisibleJobLog().exists()).toBe(false);
- });
-
- it('renders the right arrow', async () => {
- expect(findArrowIcon().props().name).toBe('chevron-down');
-
- await findRow().trigger('click');
-
- expect(findArrowIcon().props().name).toBe('chevron-right');
- });
- });
- });
-
- describe('when clicking on a link element within the row', () => {
- it('does not expands/collapse the job log', async () => {
- expect(findHiddenJobLog().exists()).toBe(true);
- expect(findVisibleJobLog().exists()).toBe(false);
- expect(findArrowIcon().props().name).toBe('chevron-right');
-
- await findJobId().vm.$emit('click');
-
- expect(findHiddenJobLog().exists()).toBe(true);
- expect(findVisibleJobLog().exists()).toBe(false);
- expect(findArrowIcon().props().name).toBe('chevron-right');
- });
- });
- });
-});
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 9599b5e6b7b..7b59d82ae6f 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -34,7 +34,11 @@ import getPipelineHeaderData from '~/pipelines/graphql/queries/get_pipeline_head
import * as sentryUtils from '~/pipelines/utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import { mockRunningPipelineHeaderData } from '../mock_data';
-import { mapCallouts, mockCalloutsResponse } from './mock_data';
+import {
+ mapCallouts,
+ mockCalloutsResponse,
+ mockPipelineResponseWithTooManyJobs,
+} from './mock_data';
const defaultProvide = {
graphqlResourceEtag: 'frog/amphibirama/etag/',
@@ -49,7 +53,10 @@ describe('Pipeline graph wrapper', () => {
let wrapper;
let requestHandlers;
- const findAlert = () => wrapper.findComponent(GlAlert);
+ let pipelineDetailsHandler;
+
+ const findAlert = () => wrapper.findByTestId('error-alert');
+ const findJobCountWarning = () => wrapper.findByTestId('job-count-warning');
const findDependenciesToggle = () => wrapper.findByTestId('show-links-toggle');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
@@ -83,7 +90,6 @@ describe('Pipeline graph wrapper', () => {
const createComponentWithApollo = ({
calloutsList = [],
data = {},
- getPipelineDetailsHandler = jest.fn().mockResolvedValue(mockPipelineResponse),
mountFn = shallowMountExtended,
provide = {},
} = {}) => {
@@ -92,7 +98,7 @@ describe('Pipeline graph wrapper', () => {
requestHandlers = {
getUserCalloutsHandler: jest.fn().mockResolvedValue(mockCalloutsResponse(callouts)),
getPipelineHeaderDataHandler: jest.fn().mockResolvedValue(mockRunningPipelineHeaderData),
- getPipelineDetailsHandler,
+ getPipelineDetailsHandler: pipelineDetailsHandler,
};
const handlers = [
@@ -105,24 +111,29 @@ describe('Pipeline graph wrapper', () => {
createComponent({ apolloProvider, data, provide, mountFn });
};
+ beforeEach(() => {
+ pipelineDetailsHandler = jest.fn();
+ pipelineDetailsHandler.mockResolvedValue(mockPipelineResponse);
+ });
+
describe('when data is loading', () => {
- it('displays the loading icon', () => {
+ beforeEach(() => {
createComponentWithApollo();
+ });
+
+ it('displays the loading icon', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
it('does not display the alert', () => {
- createComponentWithApollo();
expect(findAlert().exists()).toBe(false);
});
it('does not display the graph', () => {
- createComponentWithApollo();
expect(findGraph().exists()).toBe(false);
});
it('skips querying headerPipeline', () => {
- createComponentWithApollo();
expect(wrapper.vm.$apollo.queries.headerPipeline.skip).toBe(true);
});
});
@@ -153,11 +164,25 @@ describe('Pipeline graph wrapper', () => {
});
});
+ describe('when a stage has 100 jobs or more', () => {
+ beforeEach(async () => {
+ pipelineDetailsHandler.mockResolvedValue(mockPipelineResponseWithTooManyJobs);
+ createComponentWithApollo();
+ await waitForPromises();
+ });
+
+ it('show a warning alert', () => {
+ expect(findJobCountWarning().exists()).toBe(true);
+ expect(findJobCountWarning().props().title).toBe(
+ 'Only the first 100 jobs per stage are displayed',
+ );
+ });
+ });
+
describe('when there is an error', () => {
beforeEach(async () => {
- createComponentWithApollo({
- getPipelineDetailsHandler: jest.fn().mockRejectedValue(new Error('GraphQL error')),
- });
+ pipelineDetailsHandler.mockRejectedValue(new Error('GraphQL error'));
+ createComponentWithApollo();
await waitForPromises();
});
@@ -270,13 +295,12 @@ describe('Pipeline graph wrapper', () => {
errors: [{ message: 'timeout' }],
};
- const failSucceedFail = jest
- .fn()
+ pipelineDetailsHandler
.mockResolvedValueOnce(errorData)
.mockResolvedValueOnce(mockPipelineResponse)
.mockResolvedValueOnce(errorData);
- createComponentWithApollo({ getPipelineDetailsHandler: failSucceedFail });
+ createComponentWithApollo();
await waitForPromises();
});
@@ -438,9 +462,9 @@ describe('Pipeline graph wrapper', () => {
localStorage.setItem(VIEW_TYPE_KEY, LAYER_VIEW);
+ pipelineDetailsHandler.mockResolvedValue(nonNeedsResponse);
createComponentWithApollo({
mountFn: mountExtended,
- getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse),
});
await waitForPromises();
@@ -460,9 +484,9 @@ describe('Pipeline graph wrapper', () => {
const nonNeedsResponse = { ...mockPipelineResponse };
nonNeedsResponse.data.project.pipeline.usesNeeds = false;
+ pipelineDetailsHandler.mockResolvedValue(nonNeedsResponse);
createComponentWithApollo({
mountFn: mountExtended,
- getPipelineDetailsHandler: jest.fn().mockResolvedValue(nonNeedsResponse),
});
jest.runOnlyPendingTimers();
diff --git a/spec/frontend/pipelines/graph/job_name_component_spec.js b/spec/frontend/pipelines/graph/job_name_component_spec.js
index ec432e98fdf..fca4c43d9fa 100644
--- a/spec/frontend/pipelines/graph/job_name_component_spec.js
+++ b/spec/frontend/pipelines/graph/job_name_component_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import jobNameComponent from '~/pipelines/components/jobs_shared/job_name_component.vue';
-import ciIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
describe('job name component', () => {
let wrapper;
@@ -24,7 +24,7 @@ describe('job name component', () => {
});
it('should render an icon with the provided status', () => {
- expect(wrapper.findComponent(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 bf92cd585d9..8dae2aac664 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -10,7 +10,7 @@ import { ACTION_FAILURE, UPSTREAM, DOWNSTREAM } from '~/pipelines/components/gra
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
-import CiStatus from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
import mockPipeline from './linked_pipelines_mock_data';
describe('Linked pipeline', () => {
@@ -87,7 +87,7 @@ describe('Linked pipeline', () => {
});
it('should render an svg within the status container', () => {
- const pipelineStatusElement = wrapper.findComponent(CiStatus);
+ const pipelineStatusElement = wrapper.findComponent(CiIcon);
expect(pipelineStatusElement.find('svg').exists()).toBe(true);
});
@@ -97,7 +97,7 @@ describe('Linked pipeline', () => {
});
it('should have a ci-status child component', () => {
- expect(wrapper.findComponent(CiStatus).exists()).toBe(true);
+ expect(wrapper.findComponent(CiIcon).exists()).toBe(true);
});
it('should render the pipeline id', () => {
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index b012e7f66e1..8d06d6931ed 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -1,3 +1,4 @@
+import mockPipelineResponse from 'test_fixtures/pipelines/pipeline_details.json';
import { unwrapPipelineData } from '~/pipelines/components/graph/utils';
import {
BUILD_KIND,
@@ -5,6 +6,14 @@ import {
RETRY_ACTION_TITLE,
} from '~/pipelines/components/graph/constants';
+// We mock this instead of using fixtures for performance reason.
+const mockPipelineResponseCopy = JSON.parse(JSON.stringify(mockPipelineResponse));
+const groups = new Array(100).fill({
+ ...mockPipelineResponse.data.project.pipeline.stages.nodes[0].groups.nodes[0],
+});
+mockPipelineResponseCopy.data.project.pipeline.stages.nodes[0].groups.nodes = groups;
+export const mockPipelineResponseWithTooManyJobs = mockPipelineResponseCopy;
+
export const downstream = {
nodes: [
{
diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
index 50f754393fe..b4ffd2658fe 100644
--- a/spec/frontend/pipelines/graph_shared/links_inner_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
@@ -80,7 +80,6 @@ describe('Links Inner component', () => {
};
afterEach(() => {
- jest.restoreAllMocks();
resetHTMLFixture();
});
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
deleted file mode 100644
index 18def4ab62c..00000000000
--- a/spec/frontend/pipelines/header_component_spec.js
+++ /dev/null
@@ -1,246 +0,0 @@
-import { GlAlert, GlModal, GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import waitForPromises from 'helpers/wait_for_promises';
-import HeaderComponent from '~/pipelines/components/header_component.vue';
-import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
-import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
-import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
-import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/pipelines/constants';
-import {
- mockCancelledPipelineHeader,
- mockFailedPipelineHeader,
- mockFailedPipelineNoPermissions,
- mockRunningPipelineHeader,
- mockRunningPipelineNoPermissions,
- mockSuccessfulPipelineHeader,
-} from './mock_data';
-
-describe('Pipeline details header', () => {
- let wrapper;
- let glModalDirective;
- let mutate = jest.fn();
-
- 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.findComponent(GlLoadingIcon);
-
- const defaultProvideOptions = {
- pipelineId: '14',
- pipelineIid: 1,
- paths: {
- pipelinesPath: '/namespace/my-project/-/pipelines',
- fullProject: '/namespace/my-project',
- },
- };
-
- const createComponent = (pipelineMock = mockRunningPipelineHeader, { isLoading } = false) => {
- glModalDirective = jest.fn();
-
- const $apollo = {
- queries: {
- pipeline: {
- loading: isLoading,
- stopPolling: jest.fn(),
- startPolling: jest.fn(),
- },
- },
- mutate,
- };
-
- return shallowMount(HeaderComponent, {
- data() {
- return {
- pipeline: pipelineMock,
- };
- },
- provide: {
- ...defaultProvideOptions,
- },
- directives: {
- glModal: {
- bind(_, { value }) {
- glModalDirective(value);
- },
- },
- },
- mocks: { $apollo },
- });
- };
-
- describe('initial loading', () => {
- beforeEach(() => {
- wrapper = createComponent(null, { isLoading: true });
- });
-
- it('shows a loading state while graphQL is fetching initial data', () => {
- expect(findLoadingIcon().exists()).toBe(true);
- });
- });
-
- describe('visible state', () => {
- it.each`
- state | pipelineData | retryValue | cancelValue
- ${'cancelled'} | ${mockCancelledPipelineHeader} | ${true} | ${false}
- ${'failed'} | ${mockFailedPipelineHeader} | ${true} | ${false}
- ${'running'} | ${mockRunningPipelineHeader} | ${false} | ${true}
- ${'successful'} | ${mockSuccessfulPipelineHeader} | ${false} | ${false}
- `(
- 'with a $state pipeline, it will show actions: retry $retryValue and cancel $cancelValue',
- ({ pipelineData, retryValue, cancelValue }) => {
- wrapper = createComponent(pipelineData);
-
- expect(findRetryButton().exists()).toBe(retryValue);
- expect(findCancelButton().exists()).toBe(cancelValue);
- },
- );
- });
-
- describe('actions', () => {
- describe('Retry action', () => {
- beforeEach(() => {
- wrapper = createComponent(mockCancelledPipelineHeader);
- });
-
- it('should call retryPipeline Mutation with pipeline id', () => {
- findRetryButton().vm.$emit('click');
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: retryPipelineMutation,
- variables: { id: mockCancelledPipelineHeader.id },
- });
- });
-
- it('should render retry action tooltip', () => {
- expect(findRetryButton().attributes('title')).toBe(BUTTON_TOOLTIP_RETRY);
- });
-
- it('should display error message on failure', async () => {
- const failureMessage = 'failure message';
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
- data: {
- pipelineRetry: {
- errors: [failureMessage],
- },
- },
- });
-
- findRetryButton().vm.$emit('click');
- await waitForPromises();
-
- expect(findAlert().text()).toBe(failureMessage);
- });
- });
-
- describe('Retry action failed', () => {
- beforeEach(() => {
- mutate = jest.fn().mockRejectedValue('error');
-
- wrapper = createComponent(mockCancelledPipelineHeader);
- });
-
- it('retry button loading state should reset on error', async () => {
- findRetryButton().vm.$emit('click');
-
- await nextTick();
-
- expect(findRetryButton().props('loading')).toBe(true);
-
- await waitForPromises();
-
- expect(findRetryButton().props('loading')).toBe(false);
- });
- });
-
- describe('Cancel action', () => {
- beforeEach(() => {
- wrapper = createComponent(mockRunningPipelineHeader);
- });
-
- it('should call cancelPipeline Mutation with pipeline id', () => {
- findCancelButton().vm.$emit('click');
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: cancelPipelineMutation,
- variables: { id: mockRunningPipelineHeader.id },
- });
- });
-
- it('should render cancel action tooltip', () => {
- expect(findCancelButton().attributes('title')).toBe(BUTTON_TOOLTIP_CANCEL);
- });
-
- it('should display error message on failure', async () => {
- const failureMessage = 'failure message';
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
- data: {
- pipelineCancel: {
- errors: [failureMessage],
- },
- },
- });
-
- findCancelButton().vm.$emit('click');
- await waitForPromises();
-
- expect(findAlert().text()).toBe(failureMessage);
- });
- });
-
- describe('Delete action', () => {
- beforeEach(() => {
- wrapper = createComponent(mockFailedPipelineHeader);
- });
-
- it('displays delete modal when clicking on delete and does not call the delete action', () => {
- findDeleteButton().vm.$emit('click');
-
- expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID);
- expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID);
- expect(wrapper.vm.$apollo.mutate).not.toHaveBeenCalled();
- });
-
- it('should call deletePipeline Mutation with pipeline id when modal is submitted', () => {
- findDeleteModal().vm.$emit('primary');
-
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: deletePipelineMutation,
- variables: { id: mockFailedPipelineHeader.id },
- });
- });
-
- it('should display error message on failure', async () => {
- const failureMessage = 'failure message';
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
- data: {
- pipelineDestroy: {
- errors: [failureMessage],
- },
- },
- });
-
- findDeleteModal().vm.$emit('primary');
- await waitForPromises();
-
- expect(findAlert().text()).toBe(failureMessage);
- });
- });
-
- describe('Permissions', () => {
- it('should not display the cancel action if user does not have permission', () => {
- wrapper = createComponent(mockRunningPipelineNoPermissions);
-
- expect(findCancelButton().exists()).toBe(false);
- });
-
- it('should not display the retry action if user does not have permission', () => {
- wrapper = createComponent(mockFailedPipelineNoPermissions);
-
- expect(findRetryButton().exists()).toBe(false);
- });
- });
- });
-});
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index 62c0d6e2d91..673db3b5178 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -26,19 +26,19 @@ export const pipelineRetryMutationResponseFailed = {
};
export const pipelineCancelMutationResponseSuccess = {
- data: { pipelineRetry: { errors: [] } },
+ data: { pipelineCancel: { errors: [] } },
};
export const pipelineCancelMutationResponseFailed = {
- data: { pipelineRetry: { errors: ['error'] } },
+ data: { pipelineCancel: { errors: ['error'] } },
};
export const pipelineDeleteMutationResponseSuccess = {
- data: { pipelineRetry: { errors: [] } },
+ data: { pipelineDestroy: { errors: [] } },
};
export const pipelineDeleteMutationResponseFailed = {
- data: { pipelineRetry: { errors: ['error'] } },
+ data: { pipelineDestroy: { errors: ['error'] } },
};
export const mockPipelineHeader = {
diff --git a/spec/frontend/pipelines/pipeline_details_header_spec.js b/spec/frontend/pipelines/pipeline_details_header_spec.js
index deaf5c6f72f..5c75020afad 100644
--- a/spec/frontend/pipelines/pipeline_details_header_spec.js
+++ b/spec/frontend/pipelines/pipeline_details_header_spec.js
@@ -7,7 +7,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import PipelineDetailsHeader from '~/pipelines/components/pipeline_details_header.vue';
import { BUTTON_TOOLTIP_RETRY, BUTTON_TOOLTIP_CANCEL } from '~/pipelines/constants';
-import TimeAgo from '~/pipelines/components/pipelines_list/time_ago.vue';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
@@ -59,19 +58,20 @@ describe('Pipeline details header', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findStatus = () => wrapper.findComponent(CiBadgeLink);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findTimeAgo = () => wrapper.findComponent(TimeAgo);
const findAllBadges = () => wrapper.findAllComponents(GlBadge);
+ const findDeleteModal = () => wrapper.findComponent(GlModal);
+ const findCreatedTimeAgo = () => wrapper.findByTestId('pipeline-created-time-ago');
+ const findFinishedTimeAgo = () => wrapper.findByTestId('pipeline-finished-time-ago');
const findPipelineName = () => wrapper.findByTestId('pipeline-name');
const findCommitTitle = () => wrapper.findByTestId('pipeline-commit-title');
const findTotalJobs = () => wrapper.findByTestId('total-jobs');
- const findComputeCredits = () => wrapper.findByTestId('compute-credits');
+ const findComputeMinutes = () => wrapper.findByTestId('compute-minutes');
const findCommitLink = () => wrapper.findByTestId('commit-link');
const findPipelineRunningText = () => wrapper.findByTestId('pipeline-running-text').text();
const findPipelineRefText = () => wrapper.findByTestId('pipeline-ref-text').text();
const findRetryButton = () => wrapper.findByTestId('retry-pipeline');
const findCancelButton = () => wrapper.findByTestId('cancel-pipeline');
const findDeleteButton = () => wrapper.findByTestId('delete-pipeline');
- const findDeleteModal = () => wrapper.findComponent(GlModal);
const findPipelineUserLink = () => wrapper.findByTestId('pipeline-user-link');
const findPipelineDuration = () => wrapper.findByTestId('pipeline-duration-text');
@@ -89,7 +89,7 @@ describe('Pipeline details header', () => {
const defaultProps = {
name: 'Ruby 3.0 master branch pipeline',
totalJobs: '50',
- computeCredits: '0.65',
+ computeMinutes: '0.65',
yamlErrors: 'errors',
failureReason: 'pipeline failed',
badges: {
@@ -216,28 +216,36 @@ describe('Pipeline details header', () => {
});
describe('finished pipeline', () => {
- it('displays compute credits when not zero', async () => {
+ it('displays compute minutes when not zero', async () => {
createComponent();
await waitForPromises();
- expect(findComputeCredits().text()).toBe('0.65');
+ expect(findComputeMinutes().text()).toBe('0.65');
+ });
+
+ it('does not display compute minutes when zero', async () => {
+ createComponent(defaultHandlers, { ...defaultProps, computeMinutes: '0.0' });
+
+ await waitForPromises();
+
+ expect(findComputeMinutes().exists()).toBe(false);
});
- it('does not display compute credits when zero', async () => {
- createComponent(defaultHandlers, { ...defaultProps, computeCredits: '0.0' });
+ it('does not display created time ago', async () => {
+ createComponent();
await waitForPromises();
- expect(findComputeCredits().exists()).toBe(false);
+ expect(findCreatedTimeAgo().exists()).toBe(false);
});
- it('displays time ago', async () => {
+ it('displays finished time ago', async () => {
createComponent();
await waitForPromises();
- expect(findTimeAgo().exists()).toBe(true);
+ expect(findFinishedTimeAgo().exists()).toBe(true);
});
it('displays pipeline duartion text', async () => {
@@ -258,12 +266,12 @@ describe('Pipeline details header', () => {
await waitForPromises();
});
- it('does not display compute credits', () => {
- expect(findComputeCredits().exists()).toBe(false);
+ it('does not display compute minutes', () => {
+ expect(findComputeMinutes().exists()).toBe(false);
});
- it('does not display time ago', () => {
- expect(findTimeAgo().exists()).toBe(false);
+ it('does not display finished time ago', () => {
+ expect(findFinishedTimeAgo().exists()).toBe(false);
});
it('does not display pipeline duration text', () => {
@@ -273,6 +281,10 @@ describe('Pipeline details header', () => {
it('displays pipeline running text', () => {
expect(findPipelineRunningText()).toBe('In progress, queued for 3,600 seconds');
});
+
+ it('displays created time ago', () => {
+ expect(findCreatedTimeAgo().exists()).toBe(true);
+ });
});
describe('running pipeline with duration', () => {
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js
index 9fedbaf9b56..1abc2887682 100644
--- a/spec/frontend/pipelines/pipelines_artifacts_spec.js
+++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js
@@ -1,4 +1,9 @@
-import { GlDropdown, GlDropdownItem, GlSprintf } from '@gitlab/ui';
+import {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdownGroup,
+ GlSprintf,
+} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
@@ -25,25 +30,27 @@ describe('Pipelines Artifacts dropdown', () => {
},
stubs: {
GlSprintf,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ GlDisclosureDropdownGroup,
},
});
};
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findFirstGlDropdownItem = () => wrapper.findComponent(GlDropdownItem);
- const findAllGlDropdownItems = () =>
- wrapper.findComponent(GlDropdown).findAllComponents(GlDropdownItem);
+ const findGlDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findFirstGlDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
it('should render a dropdown with all the provided artifacts', () => {
createComponent();
- expect(findAllGlDropdownItems()).toHaveLength(artifacts.length);
+ const [{ items }] = findGlDropdown().props('items');
+ expect(items).toHaveLength(artifacts.length);
});
it('should render a link with the provided path', () => {
createComponent();
- expect(findFirstGlDropdownItem().attributes('href')).toBe(artifacts[0].path);
+ expect(findFirstGlDropdownItem().props('item').href).toBe(artifacts[0].path);
expect(findFirstGlDropdownItem().text()).toBe(artifacts[0].name);
});
@@ -51,7 +58,7 @@ describe('Pipelines Artifacts dropdown', () => {
it('should not render the dropdown', () => {
createComponent({ mockArtifacts: [] });
- expect(findDropdown().exists()).toBe(false);
+ expect(findGlDropdown().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 10752cee841..251d823cc37 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -10,7 +10,6 @@ import PipelineTriggerer from '~/pipelines/components/pipelines_list/pipeline_tr
import PipelineUrl from '~/pipelines/components/pipelines_list/pipeline_url.vue';
import PipelinesTable from '~/pipelines/components/pipelines_list/pipelines_table.vue';
import PipelinesTimeago from '~/pipelines/components/pipelines_list/time_ago.vue';
-import PipelineFailedJobsWidget from '~/pipelines/components/pipelines_list/failure_widget/pipeline_failed_jobs_widget.vue';
import {
PipelineKeyOptions,
BUTTON_TOOLTIP_RETRY,
@@ -74,7 +73,6 @@ describe('Pipelines Table', () => {
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
const findTimeAgo = () => wrapper.findComponent(PipelinesTimeago);
const findActions = () => wrapper.findComponent(PipelineOperations);
- const findPipelineFailedJobsWidget = () => wrapper.findComponent(PipelineFailedJobsWidget);
const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row');
const findStatusTh = () => wrapper.findByTestId('status-th');
@@ -218,30 +216,6 @@ describe('Pipelines Table', () => {
});
});
});
-
- describe('widget', () => {
- describe('when there are no failed jobs', () => {
- beforeEach(() => {
- createComponent(
- { pipelines: [{ ...pipeline, failed_builds: [] }] },
- provideWithDetails,
- );
- });
-
- it('does not renders', () => {
- expect(findPipelineFailedJobsWidget().exists()).toBe(false);
- });
- });
-
- describe('when there are failed jobs', () => {
- beforeEach(() => {
- createComponent({ pipelines: [pipeline] }, provideWithDetails);
- });
- it('renders', () => {
- expect(findPipelineFailedJobsWidget().exists()).toBe(true);
- });
- });
- });
});
describe('tracking', () => {
diff --git a/spec/frontend/pipelines/time_ago_spec.js b/spec/frontend/pipelines/time_ago_spec.js
index 5afe91c4784..d2aa340a980 100644
--- a/spec/frontend/pipelines/time_ago_spec.js
+++ b/spec/frontend/pipelines/time_ago_spec.js
@@ -65,22 +65,11 @@ describe('Timeago component', () => {
expect(time.exists()).toBe(true);
});
- it('should display calendar icon by default', () => {
+ it('should display calendar icon', () => {
createComponent({ duration: 0, finished_at: '2017-04-26T12:40:23.277Z' });
expect(findCalendarIcon().exists()).toBe(true);
});
-
- it('should hide calendar icon if correct prop is passed', () => {
- createComponent(
- { duration: 0, finished_at: '2017-04-26T12:40:23.277Z' },
- {
- displayCalendarIcon: false,
- },
- );
-
- expect(findCalendarIcon().exists()).toBe(false);
- });
});
describe('without finishedTime', () => {
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index fa107600d64..a7052e53062 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -1,6 +1,6 @@
import { GlModal } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -8,6 +8,7 @@ import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import UpdateUsername from '~/profile/account/components/update_username.vue';
+import { setVueErrorHandler, resetVueErrorHandler } from 'helpers/set_vue_error_handler';
jest.mock('~/alert');
@@ -43,7 +44,7 @@ describe('UpdateUsername component', () => {
afterEach(() => {
axiosMock.restore();
- Vue.config.errorHandler = null;
+ resetVueErrorHandler();
});
const findElements = () => {
@@ -60,7 +61,7 @@ describe('UpdateUsername component', () => {
};
const clickModalWithErrorResponse = () => {
- Vue.config.errorHandler = jest.fn(); // silence thrown error
+ setVueErrorHandler({ instance: wrapper.vm, handler: jest.fn() }); // silence thrown error
const { modal } = findElements();
modal.vm.$emit('primary');
return waitForPromises();
diff --git a/spec/frontend/profile/components/follow_spec.js b/spec/frontend/profile/components/follow_spec.js
index 2555e41257f..a2e8d065a46 100644
--- a/spec/frontend/profile/components/follow_spec.js
+++ b/spec/frontend/profile/components/follow_spec.js
@@ -1,11 +1,19 @@
-import { GlAvatarLabeled, GlAvatarLink, GlLoadingIcon, GlPagination } from '@gitlab/ui';
+import {
+ GlAvatarLabeled,
+ GlAvatarLink,
+ GlEmptyState,
+ GlLoadingIcon,
+ GlPagination,
+} from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import users from 'test_fixtures/api/users/followers/get.json';
import Follow from '~/profile/components/follow.vue';
import { DEFAULT_PER_PAGE } from '~/api';
+import { isCurrentUser } from '~/lib/utils/common_utils';
jest.mock('~/rest_api');
+jest.mock('~/lib/utils/common_utils');
describe('FollowersTab', () => {
let wrapper;
@@ -15,6 +23,13 @@ describe('FollowersTab', () => {
loading: false,
page: 1,
totalItems: 50,
+ currentUserEmptyStateTitle: 'UserProfile|You do not have any followers.',
+ visitorEmptyStateTitle: "UserProfile|This user doesn't have any followers.",
+ };
+
+ const defaultProvide = {
+ followEmptyState: '/illustrations/empty-state/empty-friends-md.svg',
+ userId: '1',
};
const createComponent = ({ propsData = {} } = {}) => {
@@ -23,11 +38,13 @@ describe('FollowersTab', () => {
...defaultPropsData,
...propsData,
},
+ provide: defaultProvide,
});
};
const findPagination = () => wrapper.findComponent(GlPagination);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
describe('when `loading` prop is `true`', () => {
it('renders loading icon', () => {
@@ -95,5 +112,35 @@ describe('FollowersTab', () => {
expect(wrapper.emitted('pagination-input')).toEqual([[nextPage]]);
});
});
+
+ describe('when the users prop is empty', () => {
+ describe('when user is the current user', () => {
+ beforeEach(() => {
+ isCurrentUser.mockImplementation(() => true);
+ createComponent({ propsData: { users: [] } });
+ });
+
+ it('displays empty state with correct message', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: defaultProvide.followEmptyState,
+ title: defaultPropsData.currentUserEmptyStateTitle,
+ });
+ });
+ });
+
+ describe('when user is a visitor', () => {
+ beforeEach(() => {
+ isCurrentUser.mockImplementation(() => false);
+ createComponent({ propsData: { users: [] } });
+ });
+
+ it('displays empty state with correct message', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ svgPath: defaultProvide.followEmptyState,
+ title: defaultPropsData.visitorEmptyStateTitle,
+ });
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/profile/components/followers_tab_spec.js b/spec/frontend/profile/components/followers_tab_spec.js
index 0370005d0a4..75586a2c9ea 100644
--- a/spec/frontend/profile/components/followers_tab_spec.js
+++ b/spec/frontend/profile/components/followers_tab_spec.js
@@ -75,6 +75,8 @@ describe('FollowersTab', () => {
loading: false,
page: 1,
totalItems: 6,
+ currentUserEmptyStateTitle: FollowersTab.i18n.currentUserEmptyStateTitle,
+ visitorEmptyStateTitle: FollowersTab.i18n.visitorEmptyStateTitle,
});
});
diff --git a/spec/frontend/profile/components/following_tab_spec.js b/spec/frontend/profile/components/following_tab_spec.js
index c0583cf4877..48d84187739 100644
--- a/spec/frontend/profile/components/following_tab_spec.js
+++ b/spec/frontend/profile/components/following_tab_spec.js
@@ -1,32 +1,114 @@
import { GlBadge, GlTab } from '@gitlab/ui';
-
+import { shallowMount } from '@vue/test-utils';
+import following from 'test_fixtures/api/users/following/get.json';
import { s__ } from '~/locale';
import FollowingTab from '~/profile/components/following_tab.vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Follow from '~/profile/components/follow.vue';
+import { getUserFollowing } from '~/rest_api';
+import { createAlert } from '~/alert';
+import waitForPromises from 'helpers/wait_for_promises';
+
+const MOCK_FOLLOWEES_COUNT = 2;
+const MOCK_TOTAL_FOLLOWING = 6;
+const MOCK_PAGE = 1;
+
+jest.mock('~/rest_api');
+jest.mock('~/alert');
describe('FollowingTab', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMountExtended(FollowingTab, {
+ wrapper = shallowMount(FollowingTab, {
provide: {
- followeesCount: 3,
+ followeesCount: MOCK_FOLLOWEES_COUNT,
+ userId: 1,
+ },
+ stubs: {
+ GlTab,
},
});
};
- it('renders `GlTab` and sets title', () => {
- createComponent();
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
+ const findFollow = () => wrapper.findComponent(Follow);
+
+ describe('when API request is loading', () => {
+ beforeEach(() => {
+ getUserFollowing.mockReturnValueOnce(new Promise(() => {}));
+ createComponent();
+ });
+
+ it('renders `Follow` component and sets `loading` prop to `true`', () => {
+ expect(findFollow().props('loading')).toBe(true);
+ });
+ });
+
+ describe('when API request is successful', () => {
+ beforeEach(() => {
+ getUserFollowing.mockResolvedValueOnce({
+ data: following,
+ headers: { 'X-TOTAL': `${MOCK_TOTAL_FOLLOWING}` },
+ });
+ createComponent();
+ });
+
+ it('renders `GlTab` and sets title', () => {
+ expect(wrapper.findComponent(GlTab).text()).toContain(s__('UserProfile|Following'));
+ });
+
+ it('renders `GlBadge`, sets size and content', () => {
+ expect(findGlBadge().props('size')).toBe('sm');
+ expect(findGlBadge().text()).toBe(`${MOCK_FOLLOWEES_COUNT}`);
+ });
+
+ it('renders `Follow` component and passes correct props', () => {
+ expect(findFollow().props()).toMatchObject({
+ users: following,
+ loading: false,
+ page: MOCK_PAGE,
+ totalItems: MOCK_TOTAL_FOLLOWING,
+ currentUserEmptyStateTitle: FollowingTab.i18n.currentUserEmptyStateTitle,
+ visitorEmptyStateTitle: FollowingTab.i18n.visitorEmptyStateTitle,
+ });
+ });
+
+ describe('when `Follow` component emits `pagination-input` event', () => {
+ it('calls API and updates `users` and `page` props', async () => {
+ const NEXT_PAGE = MOCK_PAGE + 1;
+ const NEXT_PAGE_FOLLOWING = [{ id: 999, name: 'page 2 following' }];
- expect(wrapper.findComponent(GlTab).element.textContent).toContain(
- s__('UserProfile|Following'),
- );
+ getUserFollowing.mockResolvedValueOnce({
+ data: NEXT_PAGE_FOLLOWING,
+ headers: { 'X-TOTAL': `${MOCK_TOTAL_FOLLOWING}` },
+ });
+
+ findFollow().vm.$emit('pagination-input', NEXT_PAGE);
+
+ await waitForPromises();
+
+ expect(findFollow().props()).toMatchObject({
+ users: NEXT_PAGE_FOLLOWING,
+ loading: false,
+ page: NEXT_PAGE,
+ totalItems: MOCK_TOTAL_FOLLOWING,
+ });
+ });
+ });
});
- it('renders `GlBadge`, sets size and content', () => {
- createComponent();
+ describe('when API request is not successful', () => {
+ beforeEach(() => {
+ getUserFollowing.mockRejectedValueOnce(new Error());
+ createComponent();
+ });
- expect(wrapper.findComponent(GlBadge).attributes('size')).toBe('sm');
- expect(wrapper.findComponent(GlBadge).element.textContent).toBe('3');
+ it('shows error alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: FollowingTab.i18n.errorMessage,
+ error: new Error(),
+ captureError: true,
+ });
+ });
});
});
diff --git a/spec/frontend/profile/components/profile_tabs_spec.js b/spec/frontend/profile/components/profile_tabs_spec.js
index f3dda2e205f..3474bbf8d0c 100644
--- a/spec/frontend/profile/components/profile_tabs_spec.js
+++ b/spec/frontend/profile/components/profile_tabs_spec.js
@@ -4,6 +4,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createAlert } from '~/alert';
import { getUserProjects } from '~/rest_api';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { VISIBILITY_LEVEL_PUBLIC_STRING } from '~/visibility_level/constants';
import OverviewTab from '~/profile/components/overview_tab.vue';
import ActivityTab from '~/profile/components/activity_tab.vue';
import GroupsTab from '~/profile/components/groups_tab.vue';
@@ -60,18 +61,30 @@ describe('ProfileTabs', () => {
});
describe('when personal projects API request is successful', () => {
- beforeEach(async () => {
+ it('passes correct props to `OverviewTab` component', async () => {
getUserProjects.mockResolvedValueOnce({ data: projects });
createComponent();
await waitForPromises();
- });
- it('passes correct props to `OverviewTab` component', () => {
expect(wrapper.findComponent(OverviewTab).props()).toMatchObject({
personalProjects: convertObjectPropsToCamelCase(projects, { deep: true }),
personalProjectsLoading: false,
});
});
+
+ describe('when projects do not have `visibility` key', () => {
+ it('sets visibility to public', async () => {
+ const [{ visibility, ...projectWithoutVisibility }] = projects;
+
+ getUserProjects.mockResolvedValueOnce({ data: [projectWithoutVisibility] });
+ createComponent();
+ await waitForPromises();
+
+ expect(wrapper.findComponent(OverviewTab).props('personalProjects')[0].visibility).toBe(
+ VISIBILITY_LEVEL_PUBLIC_STRING,
+ );
+ });
+ });
});
describe('when personal projects API request is not successful', () => {
diff --git a/spec/frontend/profile/components/snippets/snippets_tab_spec.js b/spec/frontend/profile/components/snippets/snippets_tab_spec.js
index 47e2fbcf2c0..5992bb03e4d 100644
--- a/spec/frontend/profile/components/snippets/snippets_tab_spec.js
+++ b/spec/frontend/profile/components/snippets/snippets_tab_spec.js
@@ -7,6 +7,7 @@ import { SNIPPET_MAX_LIST_COUNT } from '~/profile/constants';
import SnippetsTab from '~/profile/components/snippets/snippets_tab.vue';
import SnippetRow from '~/profile/components/snippets/snippet_row.vue';
import getUserSnippets from '~/profile/components/graphql/get_user_snippets.query.graphql';
+import { isCurrentUser } from '~/lib/utils/common_utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import {
@@ -15,8 +16,14 @@ import {
MOCK_USER_SNIPPETS_RES,
MOCK_USER_SNIPPETS_PAGINATION_RES,
MOCK_USER_SNIPPETS_EMPTY_RES,
+ MOCK_NEW_SNIPPET_PATH,
} from 'jest/profile/mock_data';
+jest.mock('~/lib/utils/common_utils');
+jest.mock('~/helpers/help_page_helper', () => ({
+ helpPagePath: jest.fn().mockImplementation(() => 'http://127.0.0.1:3000/help/user/snippets'),
+}));
+
Vue.use(VueApollo);
describe('UserProfileSnippetsTab', () => {
@@ -32,6 +39,7 @@ describe('UserProfileSnippetsTab', () => {
provide: {
userId: MOCK_USER.id,
snippetsEmptyState: MOCK_SNIPPETS_EMPTY_STATE,
+ newSnippetPath: MOCK_NEW_SNIPPET_PATH,
},
});
};
@@ -52,9 +60,38 @@ describe('UserProfileSnippetsTab', () => {
expect(findSnippetRows().exists()).toBe(false);
});
- it('does render empty state with correct svg', () => {
- expect(findGlEmptyState().exists()).toBe(true);
- expect(findGlEmptyState().attributes('svgpath')).toBe(MOCK_SNIPPETS_EMPTY_STATE);
+ describe('when user is the current user', () => {
+ beforeEach(() => {
+ isCurrentUser.mockImplementation(() => true);
+ createComponent();
+ });
+
+ it('displays empty state with correct message', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ svgPath: MOCK_SNIPPETS_EMPTY_STATE,
+ title: SnippetsTab.i18n.currentUserEmptyStateTitle,
+ description: SnippetsTab.i18n.emptyStateDescription,
+ primaryButtonLink: MOCK_NEW_SNIPPET_PATH,
+ primaryButtonText: SnippetsTab.i18n.newSnippet,
+ secondaryButtonLink: 'http://127.0.0.1:3000/help/user/snippets',
+ secondaryButtonText: SnippetsTab.i18n.learnMore,
+ });
+ });
+ });
+
+ describe('when user is a visitor', () => {
+ beforeEach(() => {
+ isCurrentUser.mockImplementation(() => false);
+ createComponent();
+ });
+
+ it('displays empty state with correct message', () => {
+ expect(findGlEmptyState().props()).toMatchObject({
+ svgPath: MOCK_SNIPPETS_EMPTY_STATE,
+ title: SnippetsTab.i18n.visitorEmptyStateTitle,
+ description: null,
+ });
+ });
});
});
diff --git a/spec/frontend/profile/mock_data.js b/spec/frontend/profile/mock_data.js
index 856534aebd3..6c4ff0a84f9 100644
--- a/spec/frontend/profile/mock_data.js
+++ b/spec/frontend/profile/mock_data.js
@@ -22,6 +22,7 @@ export const userCalendarResponse = {
};
export const MOCK_SNIPPETS_EMPTY_STATE = 'illustrations/empty-state/empty-snippets-md.svg';
+export const MOCK_NEW_SNIPPET_PATH = '/-/snippets/new';
export const MOCK_USER = {
id: '1',
diff --git a/spec/frontend/profile/preferences/components/profile_preferences_spec.js b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
index 21167dccda9..144d9e76869 100644
--- a/spec/frontend/profile/preferences/components/profile_preferences_spec.js
+++ b/spec/frontend/profile/preferences/components/profile_preferences_spec.js
@@ -47,10 +47,6 @@ describe('ProfilePreferences component', () => {
);
}
- function findIntegrationsDivider() {
- return wrapper.findByTestId('profile-preferences-integrations-rule');
- }
-
function findIntegrationsHeading() {
return wrapper.findByTestId('profile-preferences-integrations-heading');
}
@@ -86,21 +82,17 @@ describe('ProfilePreferences component', () => {
it('should not render Integrations section', () => {
wrapper = createComponent();
const views = wrapper.findAllComponents(IntegrationView);
- const divider = findIntegrationsDivider();
const heading = findIntegrationsHeading();
- expect(divider.exists()).toBe(false);
expect(heading.exists()).toBe(false);
expect(views).toHaveLength(0);
});
it('should render Integration section', () => {
wrapper = createComponent({ provide: { integrationViews } });
- const divider = findIntegrationsDivider();
const heading = findIntegrationsHeading();
const views = wrapper.findAllComponents(IntegrationView);
- expect(divider.exists()).toBe(true);
expect(heading.exists()).toBe(true);
expect(views).toHaveLength(integrationViews.length);
});
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index 630b8feafbc..50e3f2d0f37 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -1,12 +1,17 @@
-import { GlDropdown, GlDropdownSectionHeader, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
import setWindowLocation from 'helpers/set_window_location_helper';
-import * as urlUtility from '~/lib/utils/url_utility';
import AuthorSelect from '~/projects/commits/components/author_select.vue';
import { createStore } from '~/projects/commits/store';
+import { visitUrl } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrl: jest.fn(),
+}));
Vue.use(Vuex);
@@ -44,6 +49,10 @@ describe('Author Select', () => {
propsData: {
projectCommitsEl: document.querySelector('.js-project-commits-show'),
},
+ stubs: {
+ GlCollapsibleListbox,
+ GlListboxItem,
+ },
});
};
@@ -58,11 +67,9 @@ describe('Author Select', () => {
resetHTMLFixture();
});
- 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);
+ const findListboxContainer = () => wrapper.findComponent({ ref: 'listboxContainer' });
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findListboxItems = () => wrapper.findAllComponents(GlListboxItem);
describe('user is searching via "filter by commit message"', () => {
beforeEach(() => {
@@ -70,24 +77,28 @@ describe('Author Select', () => {
createComponent();
});
- it('does not disable dropdown container', () => {
- expect(findDropdownContainer().attributes('disabled')).toBeUndefined();
+ it('does not disable listbox container', () => {
+ expect(findListboxContainer().attributes('disabled')).toBeUndefined();
});
it('has correct tooltip message', () => {
- expect(findDropdownContainer().attributes('title')).toBe(
+ expect(findListboxContainer().attributes('title')).toBe(
'Searching by both author and message is currently not supported.',
);
});
- it('disables dropdown', () => {
- expect(findDropdown().attributes('disabled')).toBeDefined();
+ it('disables listbox', () => {
+ expect(findListbox().attributes('disabled')).toBeDefined();
});
});
- describe('dropdown', () => {
+ describe('listbox', () => {
+ beforeEach(() => {
+ store.state.commitsPath = commitsPath;
+ });
+
it('displays correct default text', () => {
- expect(findDropdown().attributes('text')).toBe('Author');
+ expect(findListbox().props('toggleText')).toBe('Author');
});
it('displays the current selected author', async () => {
@@ -95,81 +106,62 @@ describe('Author Select', () => {
createComponent();
await nextTick();
- expect(findDropdown().attributes('text')).toBe(currentAuthor);
+ expect(findListbox().props('toggleText')).toBe(currentAuthor);
});
it('displays correct header text', () => {
- expect(findDropdownHeader().text()).toBe('Search by author');
+ expect(findListbox().props('headerText')).toBe('Search by author');
});
it('does not have popover text by default', () => {
expect(wrapper.attributes('title')).toBeUndefined();
});
+
+ it('passes selected author to redirectPath', () => {
+ const redirectPath = `${commitsPath}?author=${currentAuthor}`;
+
+ findListbox().vm.$emit('select', currentAuthor);
+
+ expect(visitUrl).toHaveBeenCalledWith(redirectPath);
+ });
+
+ it('does not pass any author to redirectPath', () => {
+ const redirectPath = commitsPath;
+
+ findListbox().vm.$emit('select', '');
+
+ expect(visitUrl).toHaveBeenCalledWith(redirectPath);
+ });
});
- describe('dropdown search box', () => {
+ describe('listbox search box', () => {
it('has correct placeholder', () => {
- expect(findSearchBox().attributes('placeholder')).toBe('Search');
+ expect(findListbox().props('searchPlaceholder')).toBe('Search');
});
it('fetch authors on input change', () => {
const authorName = 'lorem';
- findSearchBox().vm.$emit('input', authorName);
+ findListbox().vm.$emit('search', authorName);
expect(store.actions.fetchAuthors).toHaveBeenCalledWith(expect.anything(), authorName);
});
});
- describe('dropdown list', () => {
+ describe('listbox list', () => {
beforeEach(() => {
store.state.commitsAuthors = authors;
- store.state.commitsPath = commitsPath;
});
it('has a "Any Author" as the first list item', () => {
- expect(findDropdownItems().at(0).text()).toBe('Any Author');
+ expect(findListboxItems().at(0).text()).toBe('Any Author');
});
it('displays the project authors', () => {
- expect(findDropdownItems()).toHaveLength(authors.length + 1);
- });
-
- it('has the correct props', async () => {
- setWindowLocation(`?author=${currentAuthor}`);
- createComponent();
-
- const [{ avatar_url: avatarUrl, username }] = authors;
- const result = {
- avatarUrl,
- secondaryText: username,
- isChecked: true,
- };
-
- await nextTick();
- expect(findDropdownItems().at(1).props()).toEqual(expect.objectContaining(result));
+ expect(findListboxItems()).toHaveLength(authors.length + 1);
});
it("display the author's name", () => {
- expect(findDropdownItems().at(1).text()).toBe(currentAuthor);
- });
-
- it('passes selected author to redirectPath', () => {
- const redirectToUrl = `${commitsPath}?author=${currentAuthor}`;
- const spy = jest.spyOn(urlUtility, 'redirectTo');
- spy.mockImplementation(() => 'mock');
-
- findDropdownItems().at(1).vm.$emit('click');
-
- expect(spy).toHaveBeenCalledWith(redirectToUrl);
- });
-
- it('does not pass any author to redirectPath', () => {
- const redirectToUrl = commitsPath;
- const spy = jest.spyOn(urlUtility, 'redirectTo');
- spy.mockImplementation();
-
- findDropdownItems().at(0).vm.$emit('click');
- expect(spy).toHaveBeenCalledWith(redirectToUrl);
+ expect(findListboxItems().at(1).text()).toContain(currentAuthor);
});
});
});
diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js
index ee96f46ea0c..6cc76d4a573 100644
--- a/spec/frontend/projects/compare/components/app_spec.js
+++ b/spec/frontend/projects/compare/components/app_spec.js
@@ -1,23 +1,37 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlIcon, GlLink, GlSprintf, GlFormGroup, GlFormRadioGroup, GlFormRadio } from '@gitlab/ui';
import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CompareApp from '~/projects/compare/components/app.vue';
+import {
+ COMPARE_REVISIONS_DOCS_URL,
+ I18N,
+ COMPARE_OPTIONS,
+ COMPARE_OPTIONS_INPUT_NAME,
+} from '~/projects/compare/constants';
import RevisionCard from '~/projects/compare/components/revision_card.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { appDefaultProps as defaultProps } from './mock_data';
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
describe('CompareApp component', () => {
let wrapper;
- const findSourceRevisionCard = () => wrapper.find('[data-testid="sourceRevisionCard"]');
- const findTargetRevisionCard = () => wrapper.find('[data-testid="targetRevisionCard"]');
+ const findSourceRevisionCard = () => wrapper.findByTestId('sourceRevisionCard');
+ const findTargetRevisionCard = () => wrapper.findByTestId('targetRevisionCard');
const createComponent = (props = {}) => {
- wrapper = shallowMount(CompareApp, {
+ wrapper = shallowMountExtended(CompareApp, {
propsData: {
...defaultProps,
...props,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ stubs: {
+ GlSprintf,
+ GlFormRadioGroup,
+ },
});
};
@@ -37,6 +51,21 @@ describe('CompareApp component', () => {
);
});
+ it('renders title', () => {
+ const title = wrapper.find('h1');
+ expect(title.text()).toBe(I18N.title);
+ });
+
+ it('renders subtitle', () => {
+ const subtitle = wrapper.find('p');
+ expect(subtitle.text()).toMatchInterpolatedText(I18N.subtitle);
+ });
+
+ it('renders link to docs', () => {
+ const docsLink = wrapper.findComponent(GlLink);
+ expect(docsLink.attributes('href')).toBe(COMPARE_REVISIONS_DOCS_URL);
+ });
+
it('contains the correct form attributes', () => {
expect(wrapper.attributes('action')).toBe(defaultProps.projectCompareIndexPath);
expect(wrapper.attributes('method')).toBe('POST');
@@ -48,20 +77,16 @@ describe('CompareApp component', () => {
);
});
- it('has ellipsis', () => {
- expect(wrapper.find('[data-testid="ellipsis"]').exists()).toBe(true);
- });
-
it('render Source and Target BranchDropdown components', () => {
const revisionCards = wrapper.findAllComponents(RevisionCard);
expect(revisionCards.length).toBe(2);
- expect(revisionCards.at(0).props('revisionText')).toBe('Source');
- expect(revisionCards.at(1).props('revisionText')).toBe('Target');
+ expect(revisionCards.at(0).props('revisionText')).toBe(I18N.source);
+ expect(revisionCards.at(1).props('revisionText')).toBe(I18N.target);
});
describe('compare button', () => {
- const findCompareButton = () => wrapper.findComponent(GlButton);
+ const findCompareButton = () => wrapper.findByTestId('compare-button');
it('renders button', () => {
expect(findCompareButton().exists()).toBe(true);
@@ -109,14 +134,19 @@ describe('CompareApp component', () => {
});
describe('swap revisions button', () => {
- const findSwapRevisionsButton = () => wrapper.find('[data-testid="swapRevisionsButton"]');
+ const findSwapRevisionsButton = () => wrapper.findByTestId('swapRevisionsButton');
it('renders the swap revisions button', () => {
expect(findSwapRevisionsButton().exists()).toBe(true);
});
- it('has the correct text', () => {
- expect(findSwapRevisionsButton().text()).toBe('Swap revisions');
+ it('renders icon', () => {
+ expect(findSwapRevisionsButton().findComponent(GlIcon).props('name')).toBe('substitute');
+ });
+
+ it('has tooltip', () => {
+ const tooltip = getBinding(findSwapRevisionsButton().element, 'gl-tooltip');
+ expect(tooltip.value).toBe(I18N.swapRevisions);
});
it('swaps revisions when clicked', async () => {
@@ -129,43 +159,43 @@ describe('CompareApp component', () => {
});
});
- describe('mode dropdown', () => {
- const findModeDropdownButton = () => wrapper.find('[data-testid="modeDropdown"]');
- const findEnableStraightModeButton = () =>
- wrapper.find('[data-testid="enableStraightModeButton"]');
- const findDisableStraightModeButton = () =>
- wrapper.find('[data-testid="disableStraightModeButton"]');
+ describe('compare options', () => {
+ const findGroup = () => wrapper.findComponent(GlFormGroup);
+ const findOptionsGroup = () => wrapper.findComponent(GlFormRadioGroup);
- it('renders the mode dropdown button', () => {
- expect(findModeDropdownButton().exists()).toBe(true);
- });
+ const findOptions = () => wrapper.findAllComponents(GlFormRadio);
- it('has the correct text', () => {
- expect(findEnableStraightModeButton().text()).toBe('...');
- expect(findDisableStraightModeButton().text()).toBe('..');
+ it('renders label for the compare options', () => {
+ expect(findGroup().attributes('label')).toBe(I18N.optionsLabel);
});
- it('straight mode button when clicked', async () => {
- expect(wrapper.props('straight')).toBe(false);
- expect(wrapper.find('input[name="straight"]').attributes('value')).toBe('false');
+ it('correct input name', () => {
+ expect(findOptionsGroup().attributes('name')).toBe(COMPARE_OPTIONS_INPUT_NAME);
+ });
- findEnableStraightModeButton().vm.$emit('click');
+ it('renders "only incoming changes" option', () => {
+ expect(findOptions().at(0).text()).toBe(COMPARE_OPTIONS[0].text);
+ });
- await nextTick();
+ it('renders "since source was created" option', () => {
+ expect(findOptions().at(1).text()).toBe(COMPARE_OPTIONS[1].text);
+ });
- expect(wrapper.find('input[name="straight"]').attributes('value')).toBe('true');
+ it('straight mode button when clicked', async () => {
+ expect(wrapper.props('straight')).toBe(false);
+ expect(wrapper.vm.isStraight).toBe(false);
- findDisableStraightModeButton().vm.$emit('click');
+ findOptionsGroup().vm.$emit('input', COMPARE_OPTIONS[1].value);
await nextTick();
- expect(wrapper.find('input[name="straight"]').attributes('value')).toBe('false');
+ expect(wrapper.vm.isStraight).toBe(true);
});
});
describe('merge request buttons', () => {
- const findProjectMrButton = () => wrapper.find('[data-testid="projectMrButton"]');
- const findCreateMrButton = () => wrapper.find('[data-testid="createMrButton"]');
+ const findProjectMrButton = () => wrapper.findByTestId('projectMrButton');
+ const findCreateMrButton = () => wrapper.findByTestId('createMrButton');
it('does not have merge request buttons', () => {
createComponent();
diff --git a/spec/frontend/projects/new/components/__snapshots__/app_spec.js.snap b/spec/frontend/projects/new/components/__snapshots__/app_spec.js.snap
new file mode 100644
index 00000000000..0c4d63c3509
--- /dev/null
+++ b/spec/frontend/projects/new/components/__snapshots__/app_spec.js.snap
@@ -0,0 +1,30 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Experimental new project creation app creates correct panels 1`] = `
+Array [
+ Object {
+ "description": "Create a blank project to store your files, plan your work, and collaborate on code, among other things.",
+ "imageSrc": "file-mock",
+ "key": "blank",
+ "name": "blank_project",
+ "selector": "#blank-project-pane",
+ "title": "Create blank project",
+ },
+ Object {
+ "description": "Create a project pre-populated with the necessary files to get you started quickly.",
+ "imageSrc": "file-mock",
+ "key": "template",
+ "name": "create_from_template",
+ "selector": "#create-from-template-pane",
+ "title": "Create from template",
+ },
+ Object {
+ "description": "Migrate your data from an external source like GitHub, Bitbucket, or another instance of GitLab.",
+ "imageSrc": "file-mock",
+ "key": "import",
+ "name": "import_project",
+ "selector": "#import-project-pane",
+ "title": "Import project",
+ },
+]
+`;
diff --git a/spec/frontend/projects/new/components/app_spec.js b/spec/frontend/projects/new/components/app_spec.js
index 60d8385eb91..006114e7254 100644
--- a/spec/frontend/projects/new/components/app_spec.js
+++ b/spec/frontend/projects/new/components/app_spec.js
@@ -23,6 +23,12 @@ describe('Experimental new project creation app', () => {
expect(wrapper.find(guidelineSelector).text()).toBe(DEMO_GUIDELINES);
});
+ it('creates correct panels', () => {
+ createComponent();
+
+ expect(findNewNamespacePage().props('panels')).toMatchSnapshot();
+ });
+
it.each`
isCiCdAvailable | outcome
${false} | ${'do not show CI/CD panel'}
diff --git a/spec/frontend/projects/settings/access_dropdown_spec.js b/spec/frontend/projects/settings/access_dropdown_spec.js
index d51360a7597..a94d7669b2b 100644
--- a/spec/frontend/projects/settings/access_dropdown_spec.js
+++ b/spec/frontend/projects/settings/access_dropdown_spec.js
@@ -158,6 +158,31 @@ describe('AccessDropdown', () => {
expect(template).not.toContain(user.name);
});
+
+ it('show user avatar correctly', () => {
+ const user = {
+ id: 613,
+ avatar_url: 'some_valid_avatar.png',
+ name: 'test',
+ username: 'test',
+ };
+ const template = dropdown.userRowHtml(user);
+
+ expect(template).toContain(user.avatar_url);
+ expect(template).not.toContain('identicon');
+ });
+
+ it('show identicon when user do not have avatar', () => {
+ const user = {
+ id: 613,
+ avatar_url: '',
+ name: 'test',
+ username: 'test',
+ };
+ const template = dropdown.userRowHtml(user);
+
+ expect(template).toContain('identicon');
+ });
});
describe('deployKeyRowHtml', () => {
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
index 077995ab6e4..76d45692a63 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
@@ -91,7 +91,6 @@ describe('View branch rules', () => {
expect(findBranchName().text()).toBe(I18N.allBranches);
expect(findBranchTitle().text()).toBe(I18N.targetBranch);
- jest.restoreAllMocks();
});
it('renders the correct branch title', () => {
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 7f6ecbac748..b84d1c9c0aa 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
@@ -13,8 +13,8 @@ describe('ServiceDeskRoot', () => {
let spy;
const provideData = {
- customEmail: 'custom.email@example.com',
- customEmailEnabled: true,
+ serviceDeskEmail: 'custom.email@example.com',
+ serviceDeskEmailEnabled: true,
endpoint: '/gitlab-org/gitlab-test/service_desk',
initialIncomingEmail: 'servicedeskaddress@example.com',
initialIsEnabled: true,
@@ -52,8 +52,8 @@ describe('ServiceDeskRoot', () => {
wrapper = createComponent();
expect(wrapper.findComponent(ServiceDeskSetting).props()).toEqual({
- customEmail: provideData.customEmail,
- customEmailEnabled: provideData.customEmailEnabled,
+ serviceDeskEmail: provideData.serviceDeskEmail,
+ serviceDeskEmailEnabled: provideData.serviceDeskEmailEnabled,
incomingEmail: provideData.initialIncomingEmail,
initialOutgoingName: provideData.outgoingName,
initialProjectKey: provideData.projectKey,
@@ -80,7 +80,7 @@ describe('ServiceDeskRoot', () => {
const alertBodyLink = alertEl.findComponent(GlLink);
expect(alertBodyLink.exists()).toBe(true);
expect(alertBodyLink.attributes('href')).toBe(
- '/help/user/project/service_desk.html#use-a-custom-email-address',
+ '/help/user/project/service_desk.html#use-an-additional-service-desk-alias-email',
);
expect(alertBodyLink.text()).toBe('How do I create a custom email address?');
});
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 5631927cc2f..260fd200f03 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
@@ -134,26 +134,26 @@ describe('ServiceDeskSetting', () => {
});
});
- describe('with customEmail', () => {
- describe('customEmail is different than incomingEmail', () => {
+ describe('with serviceDeskEmail', () => {
+ describe('serviceDeskEmail is different than incomingEmail', () => {
const incomingEmail = 'foo@bar.com';
- const customEmail = 'custom@bar.com';
+ const serviceDeskEmail = 'servicedesk@bar.com';
beforeEach(() => {
wrapper = createComponent({
- props: { incomingEmail, customEmail },
+ props: { incomingEmail, serviceDeskEmail },
});
});
- it('should see custom email', () => {
- expect(findIncomingEmail().element.value).toEqual(customEmail);
+ it('should see service desk email', () => {
+ expect(findIncomingEmail().element.value).toEqual(serviceDeskEmail);
});
});
describe('project suffix', () => {
it('input is hidden', () => {
wrapper = createComponent({
- props: { customEmailEnabled: false },
+ props: { serviceDeskEmailEnabled: false },
});
const input = wrapper.findByTestId('project-suffix');
@@ -163,7 +163,7 @@ describe('ServiceDeskSetting', () => {
it('input is enabled', () => {
wrapper = createComponent({
- props: { customEmailEnabled: true },
+ props: { serviceDeskEmailEnabled: true },
});
const input = wrapper.findByTestId('project-suffix');
@@ -174,7 +174,7 @@ describe('ServiceDeskSetting', () => {
it('shows error when value contains uppercase or special chars', async () => {
wrapper = createComponent({
- props: { email: 'foo@bar.com', customEmailEnabled: true },
+ props: { email: 'foo@bar.com', serviceDeskEmailEnabled: true },
});
const input = wrapper.findByTestId('project-suffix');
@@ -189,16 +189,16 @@ describe('ServiceDeskSetting', () => {
});
});
- describe('customEmail is the same as incomingEmail', () => {
+ describe('serviceDeskEmail is the same as incomingEmail', () => {
const email = 'foo@bar.com';
beforeEach(() => {
wrapper = createComponent({
- props: { incomingEmail: email, customEmail: email },
+ props: { incomingEmail: email, serviceDeskEmail: email },
});
});
- it('should see custom email', () => {
+ it('should see service desk email', () => {
expect(findIncomingEmail().element.value).toEqual(email);
});
});
diff --git a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
index 1d0faebbcb2..89f4694d1f8 100644
--- a/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
+++ b/spec/frontend/projects/terraform_notification/terraform_notification_spec.js
@@ -60,14 +60,15 @@ describe('TerraformNotificationBanner', () => {
describe('when close button is clicked', () => {
beforeEach(() => {
- wrapper.vm.$refs.calloutDismisser.dismiss = userCalloutDismissSpy;
findBanner().vm.$emit('close');
});
+
it('should send the dismiss event', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, DISMISS_EVENT, {
label: EVENT_LABEL,
});
});
+
it('should call the dismiss callback', () => {
expect(userCalloutDismissSpy).toHaveBeenCalledTimes(1);
});
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 f7333bf6893..30f957a4c45 100644
--- a/spec/frontend/related_issues/components/related_issuable_input_spec.js
+++ b/spec/frontend/related_issues/components/related_issuable_input_spec.js
@@ -1,38 +1,37 @@
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
+import GfmAutoComplete from '~/gfm_auto_complete';
import { TYPE_ISSUE } from '~/issues/constants';
import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
import { PathIdSeparator } from '~/related_issues/constants';
-jest.mock('ee_else_ce/gfm_auto_complete', () => {
- return function gfmAutoComplete() {
- return {
- constructor() {},
- setup() {},
- };
- };
-});
+jest.mock('~/gfm_auto_complete');
describe('RelatedIssuableInput', () => {
- let propsData;
-
- beforeEach(() => {
- propsData = {
- inputValue: '',
- references: [],
- pathIdSeparator: PathIdSeparator.Issue,
- issuableType: TYPE_ISSUE,
- autoCompleteSources: {
- issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
+ let wrapper;
+
+ const autoCompleteSources = {
+ issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
+ };
+
+ const mountComponent = (props = {}) => {
+ wrapper = shallowMount(RelatedIssuableInput, {
+ propsData: {
+ inputValue: '',
+ references: [],
+ pathIdSeparator: PathIdSeparator.Issue,
+ issuableType: TYPE_ISSUE,
+ autoCompleteSources,
+ ...props,
},
- };
- });
+ attachTo: document.body,
+ });
+ };
describe('autocomplete', () => {
describe('with autoCompleteSources', () => {
it('shows placeholder text', () => {
- const wrapper = shallowMount(RelatedIssuableInput, { propsData });
+ mountComponent();
expect(wrapper.findComponent({ ref: 'input' }).element.placeholder).toBe(
'Paste issue link or <#issue id>',
@@ -40,51 +39,32 @@ describe('RelatedIssuableInput', () => {
});
it('has GfmAutoComplete', () => {
- const wrapper = shallowMount(RelatedIssuableInput, { propsData });
+ mountComponent();
- expect(wrapper.vm.gfmAutoComplete).toBeDefined();
+ expect(GfmAutoComplete).toHaveBeenCalledWith(autoCompleteSources);
});
});
describe('with no autoCompleteSources', () => {
it('shows placeholder text', () => {
- const wrapper = shallowMount(RelatedIssuableInput, {
- propsData: {
- ...propsData,
- references: ['!1', '!2'],
- },
- });
+ mountComponent({ references: ['!1', '!2'] });
expect(wrapper.findComponent({ ref: 'input' }).element.value).toBe('');
});
it('does not have GfmAutoComplete', () => {
- const wrapper = shallowMount(RelatedIssuableInput, {
- propsData: {
- ...propsData,
- autoCompleteSources: {},
- },
- });
+ mountComponent({ autoCompleteSources: {} });
- expect(wrapper.vm.gfmAutoComplete).not.toBeDefined();
+ expect(GfmAutoComplete).not.toHaveBeenCalled();
});
});
});
describe('focus', () => {
it('when clicking anywhere on the input wrapper it should focus the input', async () => {
- const wrapper = shallowMount(RelatedIssuableInput, {
- propsData: {
- ...propsData,
- references: ['foo', 'bar'],
- },
- // We need to attach to document, so that `document.activeElement` is properly set in jsdom
- attachTo: document.body,
- });
-
- wrapper.find('li').trigger('click');
+ mountComponent({ references: ['foo', 'bar'] });
- await nextTick();
+ await wrapper.find('li').trigger('click');
expect(document.activeElement).toBe(wrapper.findComponent({ ref: 'input' }).element);
});
@@ -92,11 +72,7 @@ describe('RelatedIssuableInput', () => {
describe('when filling in the input', () => {
it('emits addIssuableFormInput with data', () => {
- const wrapper = shallowMount(RelatedIssuableInput, {
- propsData,
- });
-
- wrapper.vm.$emit = jest.fn();
+ mountComponent();
const newInputValue = 'filling in things';
const untouchedRawReferences = newInputValue.trim().split(/\s/);
@@ -108,12 +84,16 @@ describe('RelatedIssuableInput', () => {
input.element.selectionEnd = newInputValue.length;
input.trigger('input');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('addIssuableFormInput', {
- newValue: newInputValue,
- caretPos: newInputValue.length,
- untouchedRawReferences,
- touchedReference,
- });
+ expect(wrapper.emitted('addIssuableFormInput')).toEqual([
+ [
+ {
+ newValue: newInputValue,
+ caretPos: newInputValue.length,
+ untouchedRawReferences,
+ touchedReference,
+ },
+ ],
+ ]);
});
});
});
diff --git a/spec/frontend/releases/components/releases_pagination_spec.js b/spec/frontend/releases/components/releases_pagination_spec.js
index 923d84ae2b3..b241eb9acd4 100644
--- a/spec/frontend/releases/components/releases_pagination_spec.js
+++ b/spec/frontend/releases/components/releases_pagination_spec.js
@@ -60,9 +60,22 @@ describe('releases_pagination.vue', () => {
const findPrevButton = () => wrapper.findByTestId('prevButton');
const findNextButton = () => wrapper.findByTestId('nextButton');
+ describe('when there is only one page of results', () => {
+ beforeEach(() => {
+ createComponent(singlePageInfo);
+ });
+
+ it('hides the "Prev" button', () => {
+ expect(findPrevButton().exists()).toBe(false);
+ });
+
+ it('hides the "Next" button', () => {
+ expect(findNextButton().exists()).toBe(false);
+ });
+ });
+
describe.each`
description | pageInfo | prevEnabled | nextEnabled
- ${'when there is only one page of results'} | ${singlePageInfo} | ${false} | ${false}
${'when there is a next page, but not a previous page'} | ${onlyNextPageInfo} | ${false} | ${true}
${'when there is a previous page, but not a next page'} | ${onlyPrevPageInfo} | ${true} | ${false}
${'when there is both a previous and next page'} | ${prevAndNextPageInfo} | ${true} | ${true}
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 6825d4afecf..ede04390586 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -12,13 +12,15 @@ exports[`Repository last commit component renders commit widget 1`] = `
imgsize="32"
imgsrc="https://test.com"
linkhref="/test"
+ popoveruserid=""
+ popoverusername=""
tooltipplacement="top"
tooltiptext=""
username=""
/>
<div
- 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"
+ class="commit-detail flex-list gl-display-flex gl-justify-content-space-between gl-align-items-center gl-flex-grow-1 gl-min-w-0"
>
<div
class="commit-content"
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index ecd617ca44b..e2bb7cdb2d7 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -11,7 +11,7 @@ import BlobContent from '~/blob/components/blob_content.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
import BlobButtonGroup from '~/repository/components/blob_button_group.vue';
import BlobContentViewer from '~/repository/components/blob_content_viewer.vue';
-import WebIdeLink from '~/vue_shared/components/web_ide_link.vue';
+import WebIdeLink from 'ee_else_ce/vue_shared/components/web_ide_link.vue';
import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
@@ -52,6 +52,8 @@ let userInfoMockResolver;
let projectInfoMockResolver;
let applicationInfoMockResolver;
+Vue.use(Vuex);
+
const mockAxios = new MockAdapter(axios);
const createMockStore = () =>
@@ -150,6 +152,7 @@ const createComponent = async (mockData = {}, mountFn = shallowMount, mockRoute
...inject,
glFeatures: {
highlightJs,
+ highlightJsWorker: false,
},
},
}),
@@ -403,7 +406,7 @@ describe('Blob content viewer component', () => {
await waitForPromises();
- expect(loadViewer).toHaveBeenCalledWith(viewer, false);
+ expect(loadViewer).toHaveBeenCalledWith(viewer, false, false, 'javascript');
expect(wrapper.findComponent(loadViewerReturnValue).exists()).toBe(true);
});
});
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index f4baa817d32..46a7f2ee1bb 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -1,6 +1,6 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown, GlDisclosureDropdownGroup } from '@gitlab/ui';
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
@@ -11,6 +11,7 @@ import permissionsQuery from 'shared_queries/repository/permissions.query.graphq
import projectPathQuery from '~/repository/queries/project_path.query.graphql';
import createApolloProvider from 'helpers/mock_apollo_helper';
+import { __ } from '~/locale';
const defaultMockRoute = {
name: 'blobPath',
@@ -61,6 +62,7 @@ describe('Repository breadcrumbs component', () => {
},
stubs: {
RouterLink: RouterLinkStub,
+ GlDisclosureDropdown,
},
mocks: {
$route: {
@@ -71,7 +73,8 @@ describe('Repository breadcrumbs component', () => {
});
};
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDropdownGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup);
const findUploadBlobModal = () => wrapper.findComponent(UploadBlobModal);
const findNewDirectoryModal = () => wrapper.findComponent(NewDirectoryModal);
const findRouterLink = () => wrapper.findAllComponents(RouterLinkStub);
@@ -146,7 +149,11 @@ 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 });
+ factory(
+ 'app/assets/javascripts.js',
+ { canCollaborate: true, canEditTree: true },
+ { name: routeName },
+ );
expect(findDropdown().exists()).toBe(isRendered);
},
);
@@ -156,7 +163,7 @@ describe('Repository breadcrumbs component', () => {
createPermissionsQueryResponse({ forkProject: true, createMergeRequestIn: true }),
);
- factory('/', { canCollaborate: true });
+ factory('/', { canCollaborate: true, canEditTree: true });
await nextTick();
expect(findDropdown().exists()).toBe(true);
@@ -193,4 +200,32 @@ describe('Repository breadcrumbs component', () => {
expect(findNewDirectoryModal().props('path')).toBe('root/master/some_dir');
});
});
+
+ describe('"this repository" dropdown group', () => {
+ it('renders when user has pushCode permissions', async () => {
+ permissionsQuerySpy.mockResolvedValue(
+ createPermissionsQueryResponse({
+ pushCode: true,
+ }),
+ );
+
+ factory('/', { canCollaborate: true });
+ await waitForPromises();
+
+ expect(findDropdownGroup().props('group').name).toBe(__('This repository'));
+ });
+
+ it('does not render when user does not have pushCode permissions', async () => {
+ permissionsQuerySpy.mockResolvedValue(
+ createPermissionsQueryResponse({
+ pushCode: false,
+ }),
+ );
+
+ factory('/', { canCollaborate: true });
+ await waitForPromises();
+
+ expect(findDropdownGroup().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/repository/mixins/highlight_mixin_spec.js b/spec/frontend/repository/mixins/highlight_mixin_spec.js
index 5f872749581..fd14f01747a 100644
--- a/spec/frontend/repository/mixins/highlight_mixin_spec.js
+++ b/spec/frontend/repository/mixins/highlight_mixin_spec.js
@@ -31,10 +31,13 @@ describe('HighlightMixin', () => {
const dummyComponent = {
mixins: [highlightMixin],
- inject: { highlightWorker: { default: workerMock } },
+ inject: {
+ highlightWorker: { default: workerMock },
+ glFeatures: { default: { highlightJsWorker: true } },
+ },
template: '<div>{{chunks[0]?.highlightedContent}}</div>',
created() {
- this.initHighlightWorker({ rawTextBlob, simpleViewer, language });
+ this.initHighlightWorker({ rawTextBlob, simpleViewer, language, fileType });
},
methods: { onError: onErrorMock },
};
@@ -84,6 +87,7 @@ describe('HighlightMixin', () => {
expect(workerMock.postMessage.mock.calls[1][0]).toMatchObject({
content: rawTextBlob,
language: languageMock,
+ fileType: TEXT_FILE_TYPE,
});
});
});
diff --git a/spec/frontend/scripts/frontend/po_to_json_spec.js b/spec/frontend/scripts/frontend/po_to_json_spec.js
index 858e3c9d3c7..47d5ccfefd4 100644
--- a/spec/frontend/scripts/frontend/po_to_json_spec.js
+++ b/spec/frontend/scripts/frontend/po_to_json_spec.js
@@ -168,7 +168,7 @@ msgstr ""
});
describe('escaping', () => {
- it('escapes quotes in msgid and translation', () => {
+ it('escapes quotes in translation', () => {
const poContent = `
# Escaped quotes in msgid and msgstr
msgid "Changes the title to \\"%{title_param}\\"."
@@ -183,7 +183,7 @@ msgstr "Ändert den Titel in \\"%{title_param}\\"."
domain: 'app',
lang: LOCALE,
},
- 'Changes the title to \\"%{title_param}\\".': [
+ 'Changes the title to "%{title_param}".': [
'Ändert den Titel in \\"%{title_param}\\".',
],
},
@@ -191,7 +191,7 @@ msgstr "Ändert den Titel in \\"%{title_param}\\"."
});
});
- it('escapes backslashes in msgid and translation', () => {
+ it('escapes backslashes in translation', () => {
const poContent = `
# Escaped backslashes in msgid and msgstr
msgid "Example: ssh\\\\:\\\\/\\\\/"
@@ -206,7 +206,7 @@ msgstr "Beispiel: ssh\\\\:\\\\/\\\\/"
domain: 'app',
lang: LOCALE,
},
- 'Example: ssh\\\\:\\\\/\\\\/': ['Beispiel: ssh\\\\:\\\\/\\\\/'],
+ 'Example: ssh\\:\\/\\/': ['Beispiel: ssh\\\\:\\\\/\\\\/'],
},
},
});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index 7cf8633d749..3f23803bbf6 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -132,6 +132,13 @@ export const MOCK_NAVIGATION = {
active: true,
count: '2,430',
},
+ epics: {
+ label: 'Epics',
+ scope: 'epics',
+ link: '/search?scope=epics&search=et',
+ active: true,
+ count: '0',
+ },
merge_requests: {
label: 'Merge requests',
scope: 'merge_requests',
@@ -496,6 +503,14 @@ export const MOCK_NAVIGATION_ITEMS = [
items: [],
},
{
+ title: 'Epics',
+ icon: 'epic',
+ link: '/search?scope=epics&search=et',
+ is_active: true,
+ pill_count: '0',
+ items: [],
+ },
+ {
title: 'Merge requests',
icon: 'merge-request',
link: '/search?scope=merge_requests&search=et',
@@ -505,7 +520,7 @@ export const MOCK_NAVIGATION_ITEMS = [
},
{
title: 'Wiki',
- icon: 'overview',
+ icon: 'book',
link: '/search?scope=wiki_blobs&search=et',
is_active: false,
pill_count: '0',
@@ -529,7 +544,7 @@ export const MOCK_NAVIGATION_ITEMS = [
},
{
title: 'Milestones',
- icon: 'tag',
+ icon: 'clock',
link: '/search?scope=milestones&search=et',
is_active: false,
pill_count: '0',
@@ -887,3 +902,5 @@ export const MOCK_FILTERED_UNAPPLIED_SELECTED_LABELS = [
parent_full_name: 'Toolbox / Gitlab Smoke Tests',
},
];
+
+export const CURRENT_SCOPE = 'blobs';
diff --git a/spec/frontend/search/sidebar/components/label_filter_spec.js b/spec/frontend/search/sidebar/components/label_filter_spec.js
index c5df374d4ef..2a5b3a96045 100644
--- a/spec/frontend/search/sidebar/components/label_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/label_filter_spec.js
@@ -92,6 +92,7 @@ describe('GlobalSearchSidebarLabelFilter', () => {
const findCheckboxFilter = () => wrapper.findAllComponents(LabelDropdownItems);
const findAlert = () => wrapper.findComponent(GlAlert);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findNoLabelsFoundMessage = () => wrapper.findComponentByTestId('no-labels-found-message');
describe('Renders correctly closed', () => {
beforeEach(async () => {
@@ -228,6 +229,33 @@ describe('GlobalSearchSidebarLabelFilter', () => {
});
});
+ describe('Renders no-labels state correctly', () => {
+ beforeEach(async () => {
+ createComponent();
+ store.commit(REQUEST_AGGREGATIONS);
+ await Vue.nextTick();
+
+ findSearchBox().vm.$emit('focusin');
+ findSearchBox().vm.$emit('input', 'ssssssss');
+ });
+
+ it('renders checkbox filter', () => {
+ expect(findCheckboxFilter().exists()).toBe(false);
+ });
+
+ it("doesn't render alert", () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it("doesn't render items", () => {
+ expect(findAllSelectedLabelsAbove().exists()).toBe(false);
+ });
+
+ it('renders no labels found text', () => {
+ expect(findNoLabelsFoundMessage().exists()).toBe(true);
+ });
+ });
+
describe('Renders error state correctly', () => {
beforeEach(async () => {
createComponent();
@@ -294,6 +322,8 @@ describe('GlobalSearchSidebarLabelFilter', () => {
describe('dropdown checkboxes work', () => {
beforeEach(async () => {
createComponent();
+ store.commit(RECEIVE_AGGREGATIONS_SUCCESS, MOCK_LABEL_AGGREGATIONS.data);
+ await Vue.nextTick();
await findSearchBox().vm.$emit('focusin');
await Vue.nextTick();
diff --git a/spec/frontend/search/sidebar/components/scope_legacy_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_legacy_navigation_spec.js
index 6a94da31a1b..786ad806ea6 100644
--- a/spec/frontend/search/sidebar/components/scope_legacy_navigation_spec.js
+++ b/spec/frontend/search/sidebar/components/scope_legacy_navigation_spec.js
@@ -7,6 +7,8 @@ import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navi
Vue.use(Vuex);
+const MOCK_NAVIGATION_ENTRIES = Object.entries(MOCK_NAVIGATION);
+
describe('ScopeLegacyNavigation', () => {
let wrapper;
@@ -55,12 +57,12 @@ describe('ScopeLegacyNavigation', () => {
});
it('renders all nav item components', () => {
- expect(findGlNavItems()).toHaveLength(9);
+ expect(findGlNavItems()).toHaveLength(MOCK_NAVIGATION_ENTRIES.length);
});
it('has all proper links', () => {
const linkAtPosition = 3;
- const { link } = MOCK_NAVIGATION[Object.keys(MOCK_NAVIGATION)[linkAtPosition]];
+ const { link } = MOCK_NAVIGATION_ENTRIES[linkAtPosition][1];
expect(findGlNavItems().at(linkAtPosition).attributes('href')).toBe(link);
});
diff --git a/spec/frontend/search/sidebar/components/scope_sidebar_navigation_spec.js b/spec/frontend/search/sidebar/components/scope_sidebar_navigation_spec.js
index 4b71ff0bedc..86939bdc5d6 100644
--- a/spec/frontend/search/sidebar/components/scope_sidebar_navigation_spec.js
+++ b/spec/frontend/search/sidebar/components/scope_sidebar_navigation_spec.js
@@ -7,6 +7,8 @@ import { MOCK_QUERY, MOCK_NAVIGATION, MOCK_NAVIGATION_ITEMS } from '../../mock_d
Vue.use(Vuex);
+const MOCK_NAVIGATION_ENTRIES = Object.entries(MOCK_NAVIGATION);
+
describe('ScopeSidebarNavigation', () => {
let wrapper;
@@ -59,7 +61,7 @@ describe('ScopeSidebarNavigation', () => {
});
it('renders all nav item components', () => {
- expect(findNavItems()).toHaveLength(9);
+ expect(findNavItems()).toHaveLength(MOCK_NAVIGATION_ENTRIES.length);
});
it('has all proper links', () => {
diff --git a/spec/frontend/search/topbar/components/app_spec.js b/spec/frontend/search/topbar/components/app_spec.js
index 423ec6ff63b..9dc14b97ce0 100644
--- a/spec/frontend/search/topbar/components/app_spec.js
+++ b/spec/frontend/search/topbar/components/app_spec.js
@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import { MOCK_QUERY } from 'jest/search/mock_data';
+import { stubComponent } from 'helpers/stub_component';
import GlobalSearchTopbar from '~/search/topbar/components/app.vue';
import GroupFilter from '~/search/topbar/components/group_filter.vue';
import ProjectFilter from '~/search/topbar/components/project_filter.vue';
@@ -93,11 +94,20 @@ describe('GlobalSearchTopbar', () => {
});
it('dispatched correct click action', () => {
- const draweToggleSpy = jest.fn();
- wrapper.vm.$refs.markdownDrawer.toggleDrawer = draweToggleSpy;
+ const drawerToggleSpy = jest.fn();
+
+ createComponent(
+ { query: { repository_ref: '' } },
+ { elasticsearchEnabled: true, defaultBranchName: '' },
+ {
+ MarkdownDrawer: stubComponent(MarkdownDrawer, {
+ methods: { toggleDrawer: drawerToggleSpy },
+ }),
+ },
+ );
findSyntaxOptionButton().vm.$emit('click');
- expect(draweToggleSpy).toHaveBeenCalled();
+ expect(drawerToggleSpy).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/search/topbar/components/group_filter_spec.js b/spec/frontend/search/topbar/components/group_filter_spec.js
index 78d9efbd686..94882d181d3 100644
--- a/spec/frontend/search/topbar/components/group_filter_spec.js
+++ b/spec/frontend/search/topbar/components/group_filter_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { MOCK_GROUP, MOCK_QUERY } from 'jest/search/mock_data';
+import { MOCK_GROUP, MOCK_QUERY, CURRENT_SCOPE } from 'jest/search/mock_data';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { GROUPS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
import GroupFilter from '~/search/topbar/components/group_filter.vue';
@@ -37,6 +37,7 @@ describe('GroupFilter', () => {
actions: actionSpies,
getters: {
frequentGroups: () => [],
+ currentScope: () => CURRENT_SCOPE,
},
});
@@ -89,6 +90,7 @@ describe('GroupFilter', () => {
[GROUP_DATA.queryParam]: null,
[PROJECT_DATA.queryParam]: null,
nav_source: null,
+ scope: CURRENT_SCOPE,
});
expect(visitUrl).toHaveBeenCalled();
@@ -109,6 +111,7 @@ describe('GroupFilter', () => {
[GROUP_DATA.queryParam]: MOCK_GROUP.id,
[PROJECT_DATA.queryParam]: null,
nav_source: null,
+ scope: CURRENT_SCOPE,
});
expect(visitUrl).toHaveBeenCalled();
diff --git a/spec/frontend/search/topbar/components/project_filter_spec.js b/spec/frontend/search/topbar/components/project_filter_spec.js
index 9eda34b1633..c25d2b94027 100644
--- a/spec/frontend/search/topbar/components/project_filter_spec.js
+++ b/spec/frontend/search/topbar/components/project_filter_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
-import { MOCK_PROJECT, MOCK_QUERY } from 'jest/search/mock_data';
+import { MOCK_PROJECT, MOCK_QUERY, CURRENT_SCOPE } from 'jest/search/mock_data';
import { visitUrl, setUrlParams } from '~/lib/utils/url_utility';
import { PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
import ProjectFilter from '~/search/topbar/components/project_filter.vue';
@@ -37,6 +37,7 @@ describe('ProjectFilter', () => {
actions: actionSpies,
getters: {
frequentProjects: () => [],
+ currentScope: () => CURRENT_SCOPE,
},
});
@@ -88,6 +89,7 @@ describe('ProjectFilter', () => {
expect(setUrlParams).toHaveBeenCalledWith({
[PROJECT_DATA.queryParam]: null,
nav_source: null,
+ scope: CURRENT_SCOPE,
});
expect(visitUrl).toHaveBeenCalled();
});
@@ -107,6 +109,7 @@ describe('ProjectFilter', () => {
[GROUP_DATA.queryParam]: MOCK_PROJECT.namespace.id,
[PROJECT_DATA.queryParam]: MOCK_PROJECT.id,
nav_source: null,
+ scope: CURRENT_SCOPE,
});
expect(visitUrl).toHaveBeenCalled();
});
diff --git a/spec/frontend/service_desk/components/info_banner_spec.js b/spec/frontend/service_desk/components/info_banner_spec.js
new file mode 100644
index 00000000000..7487d5d8b64
--- /dev/null
+++ b/spec/frontend/service_desk/components/info_banner_spec.js
@@ -0,0 +1,81 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink, GlButton } from '@gitlab/ui';
+import InfoBanner from '~/service_desk/components/info_banner.vue';
+import { infoBannerAdminNote, enableServiceDesk } from '~/service_desk/constants';
+
+describe('InfoBanner', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ serviceDeskCalloutSvgPath: 'callout.svg',
+ serviceDeskEmailAddress: 'sd@gmail.com',
+ canAdminIssues: true,
+ canEditProjectSettings: true,
+ serviceDeskSettingsPath: 'path/to/project/settings',
+ serviceDeskHelpPath: 'path/to/documentation',
+ isServiceDeskEnabled: true,
+ };
+
+ const findEnableSDButton = () => wrapper.findComponent(GlButton);
+
+ const mountComponent = (provide) => {
+ return shallowMount(InfoBanner, {
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ stubs: {
+ GlLink,
+ GlButton,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ wrapper = mountComponent();
+ });
+
+ describe('Service Desk email address', () => {
+ it('renders when user can admin issues and service desk is enabled', () => {
+ expect(wrapper.text()).toContain(infoBannerAdminNote);
+ expect(wrapper.text()).toContain(wrapper.vm.serviceDeskEmailAddress);
+ });
+
+ it('does not render, when user can not admin issues', () => {
+ wrapper = mountComponent({ canAdminIssues: false });
+
+ expect(wrapper.text()).not.toContain(infoBannerAdminNote);
+ expect(wrapper.text()).not.toContain(wrapper.vm.serviceDeskEmailAddress);
+ });
+
+ it('does not render, when service desk is not setup', () => {
+ wrapper = mountComponent({ isServiceDeskEnabled: false });
+
+ expect(wrapper.text()).not.toContain(infoBannerAdminNote);
+ expect(wrapper.text()).not.toContain(wrapper.vm.serviceDeskEmailAddress);
+ });
+ });
+
+ describe('Link to Service Desk settings', () => {
+ it('renders when user can edit settings and service desk is not enabled', () => {
+ wrapper = mountComponent({ isServiceDeskEnabled: false });
+
+ expect(wrapper.text()).toContain(enableServiceDesk);
+ expect(findEnableSDButton().exists()).toBe(true);
+ });
+
+ it('does not render when service desk is enabled', () => {
+ wrapper = mountComponent();
+
+ expect(wrapper.text()).not.toContain(enableServiceDesk);
+ expect(findEnableSDButton().exists()).toBe(false);
+ });
+
+ it('does not render when user cannot edit settings', () => {
+ wrapper = mountComponent({ canEditProjectSettings: false });
+
+ expect(wrapper.text()).not.toContain(enableServiceDesk);
+ expect(findEnableSDButton().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/service_desk/components/service_desk_list_app_spec.js b/spec/frontend/service_desk/components/service_desk_list_app_spec.js
new file mode 100644
index 00000000000..2ac789745aa
--- /dev/null
+++ b/spec/frontend/service_desk/components/service_desk_list_app_spec.js
@@ -0,0 +1,151 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import * as Sentry from '@sentry/browser';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
+import { issuableListTabs } from '~/vue_shared/issuable/list/constants';
+import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
+import getServiceDeskIssuesQuery from '~/service_desk/queries/get_service_desk_issues.query.graphql';
+import getServiceDeskIssuesCountsQuery from '~/service_desk/queries/get_service_desk_issues_counts.query.graphql';
+import ServiceDeskListApp from '~/service_desk/components/service_desk_list_app.vue';
+import InfoBanner from '~/service_desk/components/info_banner.vue';
+import {
+ getServiceDeskIssuesQueryResponse,
+ getServiceDeskIssuesCountsQueryResponse,
+} from '../mock_data';
+
+jest.mock('@sentry/browser');
+
+describe('ServiceDeskListApp', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const defaultProvide = {
+ emptyStateSvgPath: 'empty-state.svg',
+ isProject: true,
+ isSignedIn: true,
+ fullPath: 'path/to/project',
+ isServiceDeskSupported: true,
+ hasAnyIssues: true,
+ };
+
+ const defaultQueryResponse = getServiceDeskIssuesQueryResponse;
+
+ const mockServiceDeskIssuesQueryResponse = jest.fn().mockResolvedValue(defaultQueryResponse);
+ const mockServiceDeskIssuesCountsQueryResponse = jest
+ .fn()
+ .mockResolvedValue(getServiceDeskIssuesCountsQueryResponse);
+
+ const findIssuableList = () => wrapper.findComponent(IssuableList);
+ const findInfoBanner = () => wrapper.findComponent(InfoBanner);
+
+ const mountComponent = ({
+ provide = {},
+ data = {},
+ serviceDeskIssuesQueryResponse = mockServiceDeskIssuesQueryResponse,
+ serviceDeskIssuesCountsQueryResponse = mockServiceDeskIssuesCountsQueryResponse,
+ stubs = {},
+ mountFn = shallowMount,
+ } = {}) => {
+ const requestHandlers = [
+ [getServiceDeskIssuesQuery, serviceDeskIssuesQueryResponse],
+ [getServiceDeskIssuesCountsQuery, serviceDeskIssuesCountsQueryResponse],
+ ];
+
+ return mountFn(ServiceDeskListApp, {
+ apolloProvider: createMockApollo(
+ requestHandlers,
+ {},
+ {
+ typePolicies: {
+ Query: {
+ fields: {
+ project: {
+ merge: true,
+ },
+ },
+ },
+ },
+ },
+ ),
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ data() {
+ return data;
+ },
+ stubs,
+ });
+ };
+
+ beforeEach(() => {
+ wrapper = mountComponent();
+ return waitForPromises();
+ });
+
+ it('fetches service desk issues and renders them in the issuable list', () => {
+ expect(findIssuableList().props()).toMatchObject({
+ namespace: 'service-desk',
+ recentSearchesStorageKey: 'issues',
+ issuables: defaultQueryResponse.data.project.issues.nodes,
+ tabs: issuableListTabs,
+ currentTab: STATUS_OPEN,
+ tabCounts: {
+ opened: 1,
+ closed: 1,
+ all: 1,
+ },
+ });
+ });
+
+ describe('InfoBanner', () => {
+ it('renders when Service Desk is supported and has any number of issues', () => {
+ expect(findInfoBanner().exists()).toBe(true);
+ });
+
+ it('does not render, when there are no issues', async () => {
+ wrapper = mountComponent({ provide: { hasAnyIssues: false } });
+ await waitForPromises();
+
+ expect(findInfoBanner().exists()).toBe(false);
+ });
+ });
+
+ describe('Events', () => {
+ describe('when "click-tab" event is emitted by IssuableList', () => {
+ beforeEach(() => {
+ mountComponent();
+
+ findIssuableList().vm.$emit('click-tab', STATUS_CLOSED);
+ });
+
+ it('updates ui to the new tab', () => {
+ expect(findIssuableList().props('currentTab')).toBe(STATUS_CLOSED);
+ });
+ });
+ });
+
+ describe('Errors', () => {
+ describe.each`
+ error | mountOption | message
+ ${'fetching issues'} | ${'serviceDeskIssuesQueryResponse'} | ${ServiceDeskListApp.i18n.errorFetchingIssues}
+ ${'fetching issue counts'} | ${'serviceDeskIssuesCountsQueryResponse'} | ${ServiceDeskListApp.i18n.errorFetchingCounts}
+ `('when there is an error $error', ({ mountOption, message }) => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ [mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')),
+ });
+ return waitForPromises();
+ });
+
+ it('shows an error message', () => {
+ expect(findIssuableList().props('error')).toBe(message);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
+ });
+ });
+ });
+});
diff --git a/spec/frontend/service_desk/mock_data.js b/spec/frontend/service_desk/mock_data.js
new file mode 100644
index 00000000000..17b400e8670
--- /dev/null
+++ b/spec/frontend/service_desk/mock_data.js
@@ -0,0 +1,118 @@
+export const getServiceDeskIssuesQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ __typename: 'Project',
+ issues: {
+ __persist: true,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'startcursor',
+ endCursor: 'endcursor',
+ },
+ nodes: [
+ {
+ __persist: true,
+ __typename: 'Issue',
+ id: 'gid://gitlab/Issue/123456',
+ iid: '789',
+ confidential: false,
+ createdAt: '2021-05-22T04:08:01Z',
+ downvotes: 2,
+ dueDate: '2021-05-29',
+ hidden: false,
+ humanTimeEstimate: null,
+ mergeRequestsCount: false,
+ moved: false,
+ state: 'opened',
+ title: 'Issue title',
+ updatedAt: '2021-05-22T04:08:01Z',
+ closedAt: null,
+ upvotes: 3,
+ userDiscussionsCount: 4,
+ webPath: 'project/-/issues/789',
+ webUrl: 'project/-/issues/789',
+ type: 'issue',
+ assignees: {
+ nodes: [
+ {
+ __persist: true,
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/234',
+ avatarUrl: 'avatar/url',
+ name: 'Marge Simpson',
+ username: 'msimpson',
+ webUrl: 'url/msimpson',
+ },
+ ],
+ },
+ author: {
+ __persist: true,
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/456',
+ avatarUrl: 'avatar/url',
+ name: 'GitLab Support Bot',
+ username: 'support-bot',
+ webUrl: 'url/hsimpson',
+ },
+ labels: {
+ nodes: [
+ {
+ __persist: true,
+ id: 'gid://gitlab/ProjectLabel/456',
+ color: '#333',
+ title: 'Label title',
+ description: 'Label description',
+ },
+ ],
+ },
+ milestone: null,
+ taskCompletionStatus: {
+ completedCount: 1,
+ count: 2,
+ },
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const getServiceDeskIssuesQueryEmptyResponse = {
+ data: {
+ project: {
+ id: '1',
+ __typename: 'Project',
+ issues: {
+ __persist: true,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'startcursor',
+ endCursor: 'endcursor',
+ },
+ nodes: [],
+ },
+ },
+ },
+};
+
+export const getServiceDeskIssuesCountsQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ openedIssues: {
+ count: 1,
+ },
+ closedIssues: {
+ count: 1,
+ },
+ allIssues: {
+ count: 1,
+ },
+ },
+ },
+};
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 81b65f4f050..52355806487 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -1,15 +1,12 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
-import { TEST_HOST } from 'helpers/test_constants';
import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
import userDataMock from '../../user_data_mock';
-const TOOLTIP_PLACEMENT = 'bottom';
-const { name: USER_NAME } = userDataMock();
-const TEST_ISSUABLE_TYPE = 'merge_request';
+const TEST_ISSUABLE_TYPE = 'issue';
describe('AssigneeAvatarLink component', () => {
let wrapper;
@@ -17,10 +14,6 @@ describe('AssigneeAvatarLink component', () => {
function createComponent(props = {}) {
const propsData = {
user: userDataMock(),
- showLess: true,
- rootPath: TEST_HOST,
- tooltipPlacement: TOOLTIP_PLACEMENT,
- singleUser: false,
issuableType: TEST_ISSUABLE_TYPE,
...props,
};
@@ -30,7 +23,6 @@ describe('AssigneeAvatarLink component', () => {
});
}
- const findTooltipText = () => wrapper.attributes('title');
const findUserLink = () => wrapper.findComponent(GlLink);
it('has the root url present in the assigneeUrl method', () => {
@@ -50,69 +42,6 @@ describe('AssigneeAvatarLink component', () => {
);
});
- describe.each`
- issuableType | tooltipHasName | canMerge | expected
- ${'merge_request'} | ${true} | ${true} | ${USER_NAME}
- ${'merge_request'} | ${true} | ${false} | ${`${USER_NAME} (cannot merge)`}
- ${'merge_request'} | ${false} | ${true} | ${''}
- ${'merge_request'} | ${false} | ${false} | ${'Cannot merge'}
- ${'issue'} | ${true} | ${true} | ${USER_NAME}
- ${'issue'} | ${true} | ${false} | ${USER_NAME}
- ${'issue'} | ${false} | ${true} | ${''}
- ${'issue'} | ${false} | ${false} | ${''}
- `(
- 'with $issuableType and tooltipHasName=$tooltipHasName and canMerge=$canMerge',
- ({ issuableType, tooltipHasName, canMerge, expected }) => {
- beforeEach(() => {
- createComponent({
- issuableType,
- tooltipHasName,
- user: {
- ...userDataMock(),
- can_merge: canMerge,
- },
- });
- });
-
- it('sets tooltip', () => {
- expect(findTooltipText()).toBe(expected);
- });
- },
- );
-
- describe.each`
- tooltipHasName | name | availability | canMerge | expected
- ${true} | ${"Rabbit O'Hare"} | ${''} | ${true} | ${"Rabbit O'Hare"}
- ${true} | ${"Rabbit O'Hare"} | ${'Busy'} | ${false} | ${"Rabbit O'Hare (Busy) (cannot merge)"}
- ${true} | ${'Root'} | ${'Busy'} | ${false} | ${'Root (Busy) (cannot merge)'}
- ${true} | ${'Root'} | ${'Busy'} | ${true} | ${'Root (Busy)'}
- ${true} | ${'Root'} | ${''} | ${false} | ${'Root (cannot merge)'}
- ${true} | ${'Root'} | ${''} | ${true} | ${'Root'}
- ${false} | ${'Root'} | ${'Busy'} | ${false} | ${'Cannot merge'}
- ${false} | ${'Root'} | ${'Busy'} | ${true} | ${''}
- ${false} | ${'Root'} | ${''} | ${false} | ${'Cannot merge'}
- ${false} | ${'Root'} | ${''} | ${true} | ${''}
- `(
- "with name=$name tooltipHasName=$tooltipHasName and availability='$availability' and canMerge=$canMerge",
- ({ name, tooltipHasName, availability, canMerge, expected }) => {
- beforeEach(() => {
- createComponent({
- tooltipHasName,
- user: {
- ...userDataMock(),
- name,
- can_merge: canMerge,
- availability,
- },
- });
- });
-
- it(`sets tooltip to "${expected}"`, () => {
- expect(findTooltipText()).toBe(expected);
- });
- },
- );
-
it('passes the correct user id for REST API', () => {
createComponent({
tooltipHasName: true,
@@ -135,15 +64,24 @@ describe('AssigneeAvatarLink component', () => {
expect(findUserLink().attributes('data-user-id')).toBe(String(userId));
});
- it.each`
- issuableType | userId
- ${'merge_request'} | ${undefined}
- ${'issue'} | ${'1'}
- `('sets data-user-id as $userId for $issuableType', ({ issuableType, userId }) => {
+ it('passes the correct username, cannotMerge, and CSS class for popover support', () => {
+ const moctUserData = userDataMock();
+ const { id, username } = moctUserData;
+
createComponent({
- issuableType,
+ tooltipHasName: true,
+ issuableType: 'merge_request',
+ user: { ...moctUserData, can_merge: false },
});
- expect(findUserLink().attributes('data-user-id')).toBe(userId);
+ const userLink = findUserLink();
+
+ expect(userLink.attributes()).toMatchObject({
+ 'data-user-id': `${id}`,
+ 'data-username': username,
+ 'data-cannot-merge': 'true',
+ 'data-placement': 'left',
+ });
+ expect(userLink.classes()).toContain('js-user-link');
});
});
diff --git a/spec/frontend/sidebar/components/assignees/assignee_title_spec.js b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
index d561c761c99..b2d15e76e80 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_title_spec.js
@@ -37,27 +37,6 @@ describe('AssigneeTitle component', () => {
});
});
- describe('gutter toggle', () => {
- it('does not show toggle by default', () => {
- wrapper = createComponent({
- numberOfAssignees: 2,
- editable: false,
- });
-
- expect(wrapper.vm.$el.querySelector('.gutter-toggle')).toBeNull();
- });
-
- it('shows toggle when showToggle is true', () => {
- wrapper = createComponent({
- numberOfAssignees: 2,
- editable: false,
- showToggle: true,
- });
-
- expect(wrapper.vm.$el.querySelector('.gutter-toggle')).toEqual(expect.any(Object));
- });
- });
-
describe('when changing is false', () => {
it('renders "Edit"', () => {
wrapper = createComponent({ editable: true });
diff --git a/spec/frontend/sidebar/components/assignees/assignees_spec.js b/spec/frontend/sidebar/components/assignees/assignees_spec.js
index 1661e28abd2..65a07382ebc 100644
--- a/spec/frontend/sidebar/components/assignees/assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignees_spec.js
@@ -181,7 +181,10 @@ describe('Assignee component', () => {
const userItems = findAllAvatarLinks();
expect(userItems).toHaveLength(3);
- expect(userItems.at(0).attributes('title')).toBe(users[2].name);
+ expect(userItems.at(0).attributes()).toMatchObject({
+ 'data-user-id': `${users[2].id}`,
+ 'data-username': users[2].username,
+ });
});
it('passes the sorted assignees to the collapsed-assignee-list', () => {
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 40d3d090bb4..52d68d7047e 100644
--- a/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
+++ b/spec/frontend/sidebar/components/assignees/collapsed_assignee_list_spec.js
@@ -194,7 +194,7 @@ describe('CollapsedAssigneeList component', () => {
${[busyUser, canMergeUser]} | ${1} | ${1} | ${`${busyUser.name} (Busy), ${canMergeUser.name} (1/2 can merge)`}
${[busyUser]} | ${1} | ${0} | ${`${busyUser.name} (Busy) (cannot merge)`}
${[canMergeUser]} | ${0} | ${1} | ${`${canMergeUser.name}`}
- ${[]} | ${0} | ${0} | ${'Assignee(s)'}
+ ${[]} | ${0} | ${0} | ${'Assignees'}
`(
'with $users.length users, $busy is busy and $canMerge that can merge',
({ users, expected }) => {
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
index a189d3656a2..a8b2db66723 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_assignees_spec.js
@@ -8,6 +8,7 @@ import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees.v
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarStore from '~/sidebar/stores/sidebar_store';
+import eventHub from '~/sidebar/event_hub';
import Mock from '../../mock_data';
describe('sidebar assignees', () => {
@@ -30,6 +31,9 @@ describe('sidebar assignees', () => {
});
};
+ const findAssigness = () => wrapper.findComponent(Assigness);
+ const findAssigneesRealtime = () => wrapper.findComponent(AssigneesRealtime);
+
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
mediator = new SidebarMediator(Mock.mediator);
@@ -50,18 +54,20 @@ describe('sidebar assignees', () => {
expect(mediator.saveAssignees).not.toHaveBeenCalled();
- wrapper.vm.saveAssignees();
+ eventHub.$emit('sidebar.saveAssignees');
expect(mediator.saveAssignees).toHaveBeenCalled();
});
- it('calls the mediator when "assignSelf" method is called', () => {
+ it('calls the mediator when "assignSelf" method is called', async () => {
createComponent();
+ mediator.store.isFetching.assignees = false;
+ await nextTick();
expect(mediator.assignYourself).not.toHaveBeenCalled();
expect(mediator.store.assignees.length).toBe(0);
- wrapper.vm.assignSelf();
+ await findAssigness().vm.$emit('assign-self');
expect(mediator.assignYourself).toHaveBeenCalled();
expect(mediator.store.assignees.length).toBe(1);
@@ -70,19 +76,19 @@ describe('sidebar assignees', () => {
it('hides assignees until fetched', async () => {
createComponent();
- expect(wrapper.findComponent(Assigness).exists()).toBe(false);
+ expect(findAssigness().exists()).toBe(false);
- wrapper.vm.store.isFetching.assignees = false;
+ mediator.store.isFetching.assignees = false;
await nextTick();
- expect(wrapper.findComponent(Assigness).exists()).toBe(true);
+ expect(findAssigness().exists()).toBe(true);
});
describe('when issuableType is issue', () => {
it('finds AssigneesRealtime component', () => {
createComponent();
- expect(wrapper.findComponent(AssigneesRealtime).exists()).toBe(true);
+ expect(findAssigneesRealtime().exists()).toBe(true);
});
});
@@ -90,7 +96,7 @@ describe('sidebar assignees', () => {
it('does not find AssigneesRealtime component', () => {
createComponent({ issuableType: 'MR' });
- expect(wrapper.findComponent(AssigneesRealtime).exists()).toBe(false);
+ expect(findAssigneesRealtime().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
index 47f68e1fe83..da79daebb93 100644
--- a/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
+++ b/spec/frontend/sidebar/components/lock/issuable_lock_form_spec.js
@@ -1,3 +1,4 @@
+import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
@@ -154,6 +155,13 @@ describe('IssuableLockForm', () => {
expect(tooltip).toBeDefined();
expect(tooltip.value.title).toBe(isLocked ? 'Locked' : 'Unlocked');
});
+
+ it('renders lock icon', () => {
+ const icon = findSidebarCollapseIcon().findComponent(GlIcon).props('name');
+ const expected = isLocked ? 'lock' : 'lock-open';
+
+ expect(icon).toBe(expected);
+ });
});
});
});
diff --git a/spec/frontend/sidebar/components/participants/participants_spec.js b/spec/frontend/sidebar/components/participants/participants_spec.js
index 72d83ebeca4..2b0eac46313 100644
--- a/spec/frontend/sidebar/components/participants/participants_spec.js
+++ b/spec/frontend/sidebar/components/participants/participants_spec.js
@@ -63,6 +63,19 @@ describe('Participants component', () => {
expect(findParticipantsAuthor()).toHaveLength(numberOfLessParticipants);
});
+ it('participants link has data attributes and class present for popover support', () => {
+ const numberOfLessParticipants = 2;
+ wrapper = mountComponent({ participants, numberOfLessParticipants });
+
+ const participantsLink = wrapper.find('.js-user-link');
+
+ expect(participantsLink.attributes()).toMatchObject({
+ href: `${participant.web_url}`,
+ 'data-user-id': `${participant.id}`,
+ 'data-username': `${participant.username}`,
+ });
+ });
+
it('when only showing all participants, each has an avatar', async () => {
wrapper = mountComponent({ participants, numberOfLessParticipants: 2 });
diff --git a/spec/frontend/sidebar/components/reviewers/reviewer_avatar_link_spec.js b/spec/frontend/sidebar/components/reviewers/reviewer_avatar_link_spec.js
new file mode 100644
index 00000000000..79d12fa3992
--- /dev/null
+++ b/spec/frontend/sidebar/components/reviewers/reviewer_avatar_link_spec.js
@@ -0,0 +1,66 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink } from '@gitlab/ui';
+import { TEST_HOST } from 'helpers/test_constants';
+import ReviewerAvatar from '~/sidebar/components/reviewers/reviewer_avatar.vue';
+import ReviewerAvatarLink from '~/sidebar/components/reviewers/reviewer_avatar_link.vue';
+import userDataMock from '../../user_data_mock';
+
+const TEST_ISSUABLE_TYPE = 'merge_request';
+
+describe('ReviewerAvatarLink component', () => {
+ const mockUserData = {
+ ...userDataMock(),
+ webUrl: `${TEST_HOST}/root`,
+ };
+ let wrapper;
+
+ function createComponent(props = {}) {
+ const propsData = {
+ user: mockUserData,
+ rootPath: TEST_HOST,
+ issuableType: TEST_ISSUABLE_TYPE,
+ ...props,
+ };
+
+ wrapper = shallowMount(ReviewerAvatarLink, {
+ propsData,
+ });
+ }
+
+ const findUserLink = () => wrapper.findComponent(GlLink);
+
+ it('has the root url present in the assigneeUrl method', () => {
+ createComponent();
+
+ expect(wrapper.attributes().href).toEqual(mockUserData.web_url);
+ });
+
+ it('renders reviewer avatar', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(ReviewerAvatar).props()).toMatchObject({
+ imgSize: 24,
+ user: mockUserData,
+ });
+ });
+
+ it('passes the correct user id, username, cannotMerge, and CSS class for popover support', () => {
+ const { id, username } = mockUserData;
+
+ createComponent({
+ tooltipHasName: true,
+ issuableType: 'merge_request',
+ user: mockUserData,
+ });
+
+ const userLink = findUserLink();
+
+ expect(userLink.attributes()).toMatchObject({
+ 'data-user-id': `${id}`,
+ 'data-username': username,
+ 'data-cannot-merge': 'true',
+ 'data-placement': 'left',
+ });
+ expect(userLink.classes()).toContain('js-user-link');
+ });
+});
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js
index bee90d2b2b6..05fb75dc0fb 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_widget_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
+import { GlCollapsibleListbox, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -51,8 +51,7 @@ describe('SidebarSeverityWidget', () => {
const findSeverityToken = () => wrapper.findAllComponents(SeverityToken);
const findEditBtn = () => wrapper.findByTestId('edit-button');
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findCriticalSeverityDropdownItem = () => wrapper.findComponent(GlDropdownItem); // First dropdown item is critical severity
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findTooltip = () => wrapper.findComponent(GlTooltip);
const findCollapsedSeverity = () => wrapper.findComponent({ ref: 'severity' });
@@ -87,7 +86,7 @@ describe('SidebarSeverityWidget', () => {
});
createComponent({ mutationMock });
- findCriticalSeverityDropdownItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', severity);
expect(mutationMock).toHaveBeenCalledWith({
iid,
@@ -100,7 +99,7 @@ describe('SidebarSeverityWidget', () => {
const mutationMock = jest.fn().mockRejectedValue('Something went wrong');
createComponent({ mutationMock });
- findCriticalSeverityDropdownItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', severity);
await waitForPromises();
expect(createAlert).toHaveBeenCalled();
@@ -110,7 +109,7 @@ describe('SidebarSeverityWidget', () => {
const mutationMock = jest.fn().mockRejectedValue({});
createComponent({ mutationMock });
- findCriticalSeverityDropdownItem().vm.$emit('click');
+ findDropdown().vm.$emit('select', severity);
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
diff --git a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
index a7c3867c359..a3b32e98506 100644
--- a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
@@ -1,9 +1,10 @@
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlAlert, GlModal } from '@gitlab/ui';
+import { GlAlert, GlModal, GlFormInput, GlDatepicker, GlFormTextarea } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import CreateTimelogForm from '~/sidebar/components/time_tracking/create_timelog_form.vue';
import createTimelogMutation from '~/sidebar/queries/create_timelog.mutation.graphql';
@@ -49,21 +50,19 @@ describe('Create Timelog Form', () => {
const findSaveButton = () => findModal().props('actionPrimary');
const findSaveButtonLoadingState = () => findSaveButton().attributes.loading;
const findSaveButtonDisabledState = () => findSaveButton().attributes.disabled;
+ const findGlFormInput = () => wrapper.findComponent(GlFormInput);
+ const findGlDatepicker = () => wrapper.findComponent(GlDatepicker);
+ const findGlFormTextarea = () => wrapper.findComponent(GlFormTextarea);
const submitForm = () => findForm().trigger('submit');
const mountComponent = (
- { props, data, providedProps } = {},
+ { props, providedProps } = {},
mutationResolverMock = rejectedMutationMock,
) => {
fakeApollo = createMockApollo([[createTimelogMutation, mutationResolverMock]]);
wrapper = shallowMountExtended(CreateTimelogForm, {
- data() {
- return {
- ...data,
- };
- },
provide: {
issuableType: 'issue',
...providedProps,
@@ -73,13 +72,17 @@ describe('Create Timelog Form', () => {
...props,
},
apolloProvider: fakeApollo,
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: { close: modalCloseMock },
+ }),
+ },
});
-
- wrapper.vm.$refs.modal.close = modalCloseMock;
};
afterEach(() => {
fakeApollo = null;
+ modalCloseMock.mockClear();
});
describe('save button', () => {
@@ -90,15 +93,18 @@ describe('Create Timelog Form', () => {
expect(findSaveButtonDisabledState()).toBe(true);
});
- it('is enabled and not loading when time spent is not empty', () => {
- mountComponent({ data: { timeSpent: '2d' } });
+ it('is enabled and not loading when time spent is not empty', async () => {
+ mountComponent();
+
+ await findGlFormInput().vm.$emit('input', '2d');
expect(findSaveButtonLoadingState()).toBe(false);
expect(findSaveButtonDisabledState()).toBe(false);
});
it('is disabled and loading when the the form is submitted', async () => {
- mountComponent({ data: { timeSpent: '2d' } });
+ mountComponent();
+ await findGlFormInput().vm.$emit('input', '2d');
submitForm();
@@ -109,7 +115,8 @@ describe('Create Timelog Form', () => {
});
it('is enabled and not loading the when form is submitted but the mutation has errors', async () => {
- mountComponent({ data: { timeSpent: '2d' } });
+ mountComponent();
+ await findGlFormInput().vm.$emit('input', '2d');
submitForm();
@@ -121,7 +128,8 @@ describe('Create Timelog Form', () => {
});
it('is enabled and not loading the when form is submitted but the mutation returns errors', async () => {
- mountComponent({ data: { timeSpent: '2d' } }, resolvedMutationWithErrorsMock);
+ mountComponent({}, resolvedMutationWithErrorsMock);
+ await findGlFormInput().vm.$emit('input', '2d');
submitForm();
@@ -145,7 +153,8 @@ describe('Create Timelog Form', () => {
});
it('closes the modal after a successful mutation', async () => {
- mountComponent({ data: { timeSpent: '2d' } }, resolvedMutationWithoutErrorsMock);
+ mountComponent({}, resolvedMutationWithoutErrorsMock);
+ await findGlFormInput().vm.$emit('input', '2d');
submitForm();
@@ -166,7 +175,10 @@ describe('Create Timelog Form', () => {
const spentAt = '2022-11-20T21:53:00+0000';
const summary = 'Example';
- mountComponent({ data: { timeSpent, spentAt, summary }, providedProps: { issuableType } });
+ mountComponent({ providedProps: { issuableType } });
+ await findGlFormInput().vm.$emit('input', timeSpent);
+ await findGlDatepicker().vm.$emit('input', spentAt);
+ await findGlFormTextarea().vm.$emit('input', summary);
submitForm();
@@ -187,7 +199,8 @@ describe('Create Timelog Form', () => {
});
it('shows an error if the submission fails with a handled error', async () => {
- mountComponent({ data: { timeSpent: '2d' } }, resolvedMutationWithErrorsMock);
+ mountComponent({}, resolvedMutationWithErrorsMock);
+ await findGlFormInput().vm.$emit('input', '2d');
submitForm();
@@ -198,7 +211,8 @@ describe('Create Timelog Form', () => {
});
it('shows an error if the submission fails with an unhandled error', async () => {
- mountComponent({ data: { timeSpent: '2d' } });
+ mountComponent();
+ await findGlFormInput().vm.$emit('input', '2d');
submitForm();
diff --git a/spec/frontend/sidebar/components/time_tracking/mock_data.js b/spec/frontend/sidebar/components/time_tracking/mock_data.js
index f161ae677d0..08b6c71629a 100644
--- a/spec/frontend/sidebar/components/time_tracking/mock_data.js
+++ b/spec/frontend/sidebar/components/time_tracking/mock_data.js
@@ -148,3 +148,16 @@ export const getMrTimelogsQueryResponse = {
},
},
};
+
+export const deleteTimelogMutationResponse = {
+ data: {
+ timelogDelete: {
+ errors: [],
+ timelog: {
+ id: 'gid://gitlab/Issue/148',
+ issue: {},
+ mergeRequest: {},
+ },
+ },
+ },
+};
diff --git a/spec/frontend/sidebar/components/time_tracking/report_spec.js b/spec/frontend/sidebar/components/time_tracking/report_spec.js
index 713ae83cbf1..6f25c4a10fd 100644
--- a/spec/frontend/sidebar/components/time_tracking/report_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/report_spec.js
@@ -12,6 +12,7 @@ import getIssueTimelogsQuery from '~/sidebar/queries/get_issue_timelogs.query.gr
import getMrTimelogsQuery from '~/sidebar/queries/get_mr_timelogs.query.graphql';
import deleteTimelogMutation from '~/sidebar/queries/delete_timelog.mutation.graphql';
import {
+ deleteTimelogMutationResponse,
getIssueTimelogsQueryResponse,
getMrTimelogsQueryResponse,
timelogToRemoveId,
@@ -22,7 +23,7 @@ jest.mock('~/alert');
describe('Issuable Time Tracking Report', () => {
Vue.use(VueApollo);
let wrapper;
- let fakeApollo;
+
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDeleteButton = () => wrapper.findByTestId('deleteButton');
const successIssueQueryHandler = jest.fn().mockResolvedValue(getIssueTimelogsQueryResponse);
@@ -30,30 +31,27 @@ describe('Issuable Time Tracking Report', () => {
const mountComponent = ({
queryHandler = successIssueQueryHandler,
+ mutationHandler,
issuableType = 'issue',
mountFunction = shallowMount,
limitToHours = false,
} = {}) => {
- fakeApollo = createMockApollo([
- [getIssueTimelogsQuery, queryHandler],
- [getMrTimelogsQuery, queryHandler],
- ]);
wrapper = extendedWrapper(
mountFunction(Report, {
+ apolloProvider: createMockApollo([
+ [getIssueTimelogsQuery, queryHandler],
+ [getMrTimelogsQuery, queryHandler],
+ [deleteTimelogMutation, mutationHandler],
+ ]),
provide: {
issuableId: 1,
issuableType,
},
propsData: { limitToHours, issuableId: '1' },
- apolloProvider: fakeApollo,
}),
);
};
- afterEach(() => {
- fakeApollo = null;
- });
-
it('should render loading spinner', () => {
mountComponent();
@@ -135,50 +133,27 @@ describe('Issuable Time Tracking Report', () => {
});
describe('when clicking on the delete timelog button', () => {
- beforeEach(() => {
- mountComponent({ mountFunction: mount });
- });
-
it('calls `$apollo.mutate` with deleteTimelogMutation mutation and removes the row', async () => {
- const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({
- data: {
- timelogDelete: {
- errors: [],
- },
- },
- });
-
+ const mutateSpy = jest.fn().mockResolvedValue(deleteTimelogMutationResponse);
+ mountComponent({ mutationHandler: mutateSpy, mountFunction: mount });
await waitForPromises();
+
await findDeleteButton().trigger('click');
await waitForPromises();
expect(createAlert).not.toHaveBeenCalled();
- expect(mutateSpy).toHaveBeenCalledWith({
- mutation: deleteTimelogMutation,
- variables: {
- input: {
- id: timelogToRemoveId,
- },
- },
- });
+ expect(mutateSpy).toHaveBeenCalledWith({ input: { id: timelogToRemoveId } });
});
it('calls `createAlert` with errorMessage and does not remove the row on promise reject', async () => {
- const mutateSpy = jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue({});
-
+ const mutateSpy = jest.fn().mockRejectedValue({});
+ mountComponent({ mutationHandler: mutateSpy, mountFunction: mount });
await waitForPromises();
+
await findDeleteButton().trigger('click');
await waitForPromises();
- expect(mutateSpy).toHaveBeenCalledWith({
- mutation: deleteTimelogMutation,
- variables: {
- input: {
- id: timelogToRemoveId,
- },
- },
- });
-
+ expect(mutateSpy).toHaveBeenCalledWith({ input: { id: timelogToRemoveId } });
expect(createAlert).toHaveBeenCalledWith({
message: 'An error occurred while removing the timelog.',
captureError: true,
diff --git a/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap b/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap
index 846f45345e7..fd525474923 100644
--- a/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap
+++ b/spec/frontend/sidebar/components/todo_toggle/__snapshots__/todo_spec.js.snap
@@ -27,6 +27,7 @@ exports[`SidebarTodo template renders component container element with proper da
label="Loading"
size="sm"
style="display: none;"
+ variant="spinner"
/>
</button>
`;
diff --git a/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
index 472a89e9b21..4385db43a4a 100644
--- a/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/todo_button_spec.js
@@ -23,7 +23,6 @@ describe('Todo Button', () => {
afterEach(() => {
dispatchEventSpy = null;
- jest.clearAllMocks();
});
it('renders GlButton', () => {
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index f2003aee96e..9c12088216b 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -25,8 +25,6 @@ describe('Sidebar mediator', () => {
SidebarService.singleton = null;
SidebarStore.singleton = null;
SidebarMediator.singleton = null;
-
- jest.clearAllMocks();
});
it('assigns yourself', () => {
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index c8d972b19a3..05c1a6dd11d 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -24,7 +24,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
</div>
<div
- class="js-vue-markdown-field md-area position-relative gfm-form js-expanded"
+ class="js-vue-markdown-field md-area position-relative gfm-form gl-overflow-hidden js-expanded"
data-uploads-path=""
>
<markdown-header-stub
@@ -83,16 +83,17 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
<markdown-toolbar-stub
canattachfile="true"
markdowndocspath="help/"
- quickactionsdocspath=""
showcommenttoolbar="true"
/>
</div>
</div>
<div
- class="js-vue-md-preview md md-preview-holder gl-px-5"
+ class="js-vue-md-preview md-preview-holder gl-px-5 md"
style="display: none;"
- />
+ >
+ <div />
+ </div>
<!---->
diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
index 70eb719f706..e2a9967f6ad 100644
--- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
@@ -134,6 +134,17 @@ describe('Snippet Visibility Edit component', () => {
description: SNIPPET_VISIBILITY.private.description_project,
});
});
+
+ it('when project snippet, renders special public description', () => {
+ createComponent({ propsData: { isProjectSnippet: true }, deep: true });
+
+ expect(findRadiosData()[2]).toEqual({
+ value: VISIBILITY_LEVEL_PUBLIC_STRING,
+ icon: SNIPPET_VISIBILITY.public.icon,
+ text: SNIPPET_VISIBILITY.public.label,
+ description: SNIPPET_VISIBILITY.public.description_project,
+ });
+ });
});
});
diff --git a/spec/frontend/super_sidebar/components/create_menu_spec.js b/spec/frontend/super_sidebar/components/create_menu_spec.js
index fe2fd17ae4d..510a3f5b913 100644
--- a/spec/frontend/super_sidebar/components/create_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/create_menu_spec.js
@@ -20,8 +20,12 @@ describe('CreateMenu component', () => {
const findInviteMembersTrigger = () => wrapper.findComponent(InviteMembersTrigger);
const findGlTooltip = () => wrapper.findComponent(GlTooltip);
- const createWrapper = () => {
+ const createWrapper = ({ provide = {} } = {}) => {
wrapper = shallowMountExtended(CreateMenu, {
+ provide: {
+ isImpersonating: false,
+ ...provide,
+ },
propsData: {
groups: createNewMenuGroups,
},
@@ -90,4 +94,13 @@ describe('CreateMenu component', () => {
expect(findGlTooltip().exists()).toBe(true);
});
});
+
+ it('decreases the dropdown offset when impersonating a user', () => {
+ createWrapper({ provide: { isImpersonating: true } });
+
+ expect(findGlDisclosureDropdown().props('dropdownOffset')).toEqual({
+ crossAxis: -115,
+ mainAxis: 4,
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
index 21d085dc0fb..85eb7e2e241 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/command_palette_items_spec.js
@@ -6,18 +6,22 @@ import CommandPaletteItems from '~/super_sidebar/components/global_search/comman
import {
COMMAND_HANDLE,
USERS_GROUP_TITLE,
+ PATH_GROUP_TITLE,
USER_HANDLE,
+ PATH_HANDLE,
SEARCH_SCOPE,
+ MAX_ROWS,
} from '~/super_sidebar/components/global_search/command_palette/constants';
import {
commandMapper,
linksReducer,
+ fileMapper,
} from '~/super_sidebar/components/global_search/command_palette/utils';
import { getFormattedItem } from '~/super_sidebar/components/global_search/utils';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
-import { COMMANDS, LINKS, USERS } from './mock_data';
+import { COMMANDS, LINKS, USERS, FILES } from './mock_data';
const links = LINKS.reduce(linksReducer, []);
@@ -25,6 +29,8 @@ describe('CommandPaletteItems', () => {
let wrapper;
const autocompletePath = '/autocomplete';
const searchContext = { project: { id: 1 }, group: { id: 2 } };
+ const projectFilesPath = 'project/files/path';
+ const projectBlobPath = '/blob/main';
const createComponent = (props) => {
wrapper = shallowMount(CommandPaletteItems, {
@@ -42,6 +48,8 @@ describe('CommandPaletteItems', () => {
commandPaletteLinks: LINKS,
autocompletePath,
searchContext,
+ projectFilesPath,
+ projectBlobPath,
},
});
};
@@ -50,7 +58,7 @@ describe('CommandPaletteItems', () => {
const findGroups = () => wrapper.findAllComponents(GlDisclosureDropdownGroup);
const findLoader = () => wrapper.findComponent(GlLoadingIcon);
- describe('COMMANDS & LINKS', () => {
+ describe('Commands and links', () => {
it('renders all commands initially', () => {
createComponent();
const commandGroup = COMMANDS.map(commandMapper)[0];
@@ -90,7 +98,7 @@ describe('CommandPaletteItems', () => {
});
});
- describe('USERS, ISSUES, PROJECTS', () => {
+ describe('Users, issues, and projects', () => {
let mockAxios;
beforeEach(() => {
@@ -140,4 +148,83 @@ describe('CommandPaletteItems', () => {
expect(wrapper.text()).toBe('No results found');
});
});
+
+ describe('Project files', () => {
+ let mockAxios;
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ });
+
+ it('should request project files on first search', () => {
+ jest.spyOn(axios, 'get');
+ const searchQuery = 'gitlab-ci.yml';
+ createComponent({ handle: PATH_HANDLE, searchQuery });
+
+ expect(axios.get).toHaveBeenCalledWith(projectFilesPath);
+ expect(findLoader().exists()).toBe(true);
+ });
+
+ it(`should render all items when returned number of items is less than ${MAX_ROWS}`, async () => {
+ const numberOfItems = MAX_ROWS - 1;
+ const items = FILES.slice(0, numberOfItems).map(fileMapper.bind(null, projectBlobPath));
+ mockAxios.onGet().replyOnce(HTTP_STATUS_OK, FILES.slice(0, numberOfItems));
+ jest.spyOn(fuzzaldrinPlus, 'filter').mockReturnValue(items);
+
+ const searchQuery = 'gitlab-ci.yml';
+ createComponent({ handle: PATH_HANDLE, searchQuery });
+
+ await waitForPromises();
+
+ expect(findGroups().at(0).props('group')).toMatchObject({
+ name: PATH_GROUP_TITLE,
+ items: items.slice(0, MAX_ROWS),
+ });
+
+ expect(findItems()).toHaveLength(numberOfItems);
+ });
+
+ it(`should render first ${MAX_ROWS} returned items when number of returned items exceeds ${MAX_ROWS}`, async () => {
+ const items = FILES.map(fileMapper.bind(null, projectBlobPath));
+ mockAxios.onGet().replyOnce(HTTP_STATUS_OK, FILES);
+ jest.spyOn(fuzzaldrinPlus, 'filter').mockReturnValue(items);
+
+ const searchQuery = 'gitlab-ci.yml';
+ createComponent({ handle: PATH_HANDLE, searchQuery });
+
+ await waitForPromises();
+
+ expect(findItems()).toHaveLength(MAX_ROWS);
+ expect(findGroups().at(0).props('group')).toMatchObject({
+ name: PATH_GROUP_TITLE,
+ items: items.slice(0, MAX_ROWS),
+ });
+ });
+
+ it('should display no results message when no files matched the search query', async () => {
+ mockAxios.onGet().replyOnce(HTTP_STATUS_OK, []);
+ const searchQuery = 'gitlab-ci.yml';
+ createComponent({ handle: PATH_HANDLE, searchQuery });
+ await waitForPromises();
+ expect(wrapper.text()).toBe('No results found');
+ });
+
+ it('should not make additional server call on the search query change', async () => {
+ const searchQuery = 'gitlab-ci.yml';
+ const newSearchQuery = 'package.json';
+
+ jest.spyOn(axios, 'get');
+
+ createComponent({ handle: PATH_HANDLE, searchQuery });
+
+ mockAxios.onGet().replyOnce(HTTP_STATUS_OK, FILES);
+ await waitForPromises();
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+
+ await wrapper.setProps({ searchQuery: newSearchQuery });
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ });
+ });
});
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
index ec65a43d549..d01e5c85741 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/mock_data.js
@@ -131,3 +131,46 @@ export const ISSUE = {
project_name: 'Flight',
url: '/flightjs/Flight/-/issues/37',
};
+
+export const FILES = [
+ '.gitattributes',
+ '.gitignore',
+ '.gitmodules',
+ 'CHANGELOG',
+ 'CONTRIBUTING.md',
+ 'Gemfile.zip',
+ 'LICENSE',
+ 'MAINTENANCE.md',
+ 'PROCESS.md',
+ 'README',
+ 'README.md',
+ 'VERSION',
+ 'bar/branch-test.txt',
+ 'custom-highlighting/test.gitlab-custom',
+ 'encoding/feature-1.txt',
+ 'encoding/feature-2.txt',
+ 'encoding/hotfix-1.txt',
+ 'encoding/hotfix-2.txt',
+ 'encoding/iso8859.txt',
+ 'encoding/russian.rb',
+ 'encoding/test.txt',
+ 'encoding/テスト.txt',
+ 'encoding/テスト.xls',
+ 'files/flat/path/correct/content.txt',
+ 'files/html/500.html',
+ 'files/images/6049019_460s.jpg',
+ 'files/images/emoji.png',
+ 'files/images/logo-black.png',
+ 'files/images/logo-white.png',
+ 'files/images/wm.svg',
+ 'files/js/application.js',
+ 'files/js/commit.coffee',
+ 'files/lfs/lfs_object.iso',
+ 'files/markdown/ruby-style-guide.md',
+ 'files/ruby/popen.rb',
+ 'files/ruby/regex.rb',
+ 'files/ruby/version_info.rb',
+ 'files/whitespace',
+ 'foo/bar/.gitkeep',
+ 'with space/README.md',
+];
diff --git a/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js b/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
index 0b75787723e..ebc52e2d910 100644
--- a/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/command_palette/utils_spec.js
@@ -1,6 +1,7 @@
import {
commandMapper,
linksReducer,
+ fileMapper,
} from '~/super_sidebar/components/global_search/command_palette/utils';
import { COMMANDS, LINKS, TRANSFORMED_LINKS } from './mock_data';
@@ -16,3 +17,15 @@ describe('commandMapper', () => {
expect(COMMANDS.map(commandMapper)[0].items).toHaveLength(initialCommandsLength - 1);
});
});
+
+describe('fileMapper', () => {
+ it('should transform files', () => {
+ const file = 'file';
+ const projectBlobPath = 'project/blob/path';
+ expect(fileMapper(projectBlobPath, file)).toEqual({
+ icon: 'doc-code',
+ text: file,
+ href: `${projectBlobPath}/${file}`,
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
index 9b7b9e288df..55108e116bd 100644
--- a/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
+++ b/spec/frontend/super_sidebar/components/global_search/components/global_search_spec.js
@@ -12,6 +12,7 @@ import CommandPaletteItems from '~/super_sidebar/components/global_search/comman
import {
SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
COMMON_HANDLES,
+ PATH_HANDLE,
} from '~/super_sidebar/components/global_search/command_palette/constants';
import {
SEARCH_INPUT_DESCRIPTION,
@@ -20,8 +21,6 @@ import {
ICON_GROUP,
ICON_SUBGROUP,
SCOPE_TOKEN_MAX_LENGTH,
- IS_SEARCHING,
- SEARCH_SHORTCUTS_MIN_CHARACTERS,
} from '~/super_sidebar/components/global_search/constants';
import { SEARCH_GITLAB } from '~/vue_shared/global_search/constants';
import { truncate } from '~/lib/utils/text_utility';
@@ -33,7 +32,6 @@ import {
MOCK_USERNAME,
MOCK_DEFAULT_SEARCH_OPTIONS,
MOCK_SCOPED_SEARCH_OPTIONS,
- MOCK_SEARCH_CONTEXT_FULL,
MOCK_PROJECT,
MOCK_GROUP,
} from '../mock_data';
@@ -108,7 +106,6 @@ describe('GlobalSearchModal', () => {
const findGlobalSearchModal = () => wrapper.findComponent(GlModal);
- const findGlobalSearchForm = () => wrapper.findByTestId('global-search-form');
const findGlobalSearchInput = () => wrapper.findComponent(GlSearchBoxByType);
const findScopeToken = () => wrapper.findComponent(GlToken);
const findGlobalSearchDefaultItems = () => wrapper.findComponent(GlobalSearchDefaultItems);
@@ -203,103 +200,70 @@ describe('GlobalSearchModal', () => {
describe('input box', () => {
describe.each`
- search | searchOptions | hasToken
- ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true}
- ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[1]]} | ${true}
- ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true}
- ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true}
- ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${true}
- ${'te'} | ${[MOCK_SCOPED_SEARCH_OPTIONS[5]]} | ${false}
- ${'x'} | ${[]} | ${false}
- `('token', ({ search, searchOptions, hasToken }) => {
+ search | hasToken
+ ${MOCK_SEARCH} | ${true}
+ ${'te'} | ${false}
+ ${'x'} | ${false}
+ ${''} | ${false}
+ `('token', ({ search, hasToken }) => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
- createComponent(
- { search },
- {
- searchOptions: () => searchOptions,
- },
- );
+ createComponent({ search });
findGlobalSearchInput().vm.$emit('click');
});
- it(`${hasToken ? 'is' : 'is NOT'} rendered when data set has type "${
- searchOptions[0]?.html_id
- }"`, () => {
+ it(`${hasToken ? 'is' : 'is NOT'} rendered when search query is "${search}"`, () => {
expect(findScopeToken().exists()).toBe(hasToken);
});
-
- it(`text ${hasToken ? 'is correctly' : 'is NOT'} rendered when text is "${
- searchOptions[0]?.scope || searchOptions[0]?.description
- }"`, () => {
- expect(findScopeToken().exists() && findScopeToken().text()).toBe(
- formatScopeName(searchOptions[0]?.scope || searchOptions[0]?.description),
- );
- });
});
- });
- describe('form', () => {
- describe.each`
- searchContext | search | searchOptions
- ${MOCK_SEARCH_CONTEXT_FULL} | ${null} | ${[]}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${[]}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${MOCK_SEARCH_CONTEXT_FULL} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${null} | ${MOCK_SEARCH} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${null} | ${null} | ${MOCK_SCOPED_SEARCH_OPTIONS}
- ${null} | ${null} | ${[]}
- `('wrapper', ({ searchContext, search, searchOptions }) => {
+ describe.each(MOCK_SCOPED_SEARCH_OPTIONS)('token content', (searchOption) => {
beforeEach(() => {
window.gon.current_username = MOCK_USERNAME;
- createComponent({ search, searchContext }, { searchOptions: () => searchOptions });
+ createComponent(
+ { search: MOCK_SEARCH },
+ {
+ searchOptions: () => [searchOption],
+ },
+ );
+ findGlobalSearchInput().vm.$emit('click');
});
- const isSearching = search?.length > SEARCH_SHORTCUTS_MIN_CHARACTERS;
-
- it(`classes ${isSearching ? 'contain' : 'do not contain'} "${IS_SEARCHING}"`, () => {
- if (isSearching) {
- expect(findGlobalSearchForm().classes()).toContain(IS_SEARCHING);
- return;
- }
- if (!isSearching) {
- expect(findGlobalSearchForm().classes()).not.toContain(IS_SEARCHING);
+ it(`is correctly rendered`, () => {
+ if (searchOption.scope) {
+ expect(findScopeToken().text()).toBe(formatScopeName(searchOption.scope));
+ } else {
+ expect(findScopeToken().text()).toBe(formatScopeName(searchOption.description));
}
});
});
- });
- describe.each`
- search | searchOptions | hasIcon | iconName
- ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${true} | ${ICON_PROJECT}
- ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${true} | ${ICON_GROUP}
- ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${true} | ${ICON_SUBGROUP}
- ${MOCK_SEARCH} | ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${false} | ${false}
- `('token', ({ search, searchOptions, hasIcon, iconName }) => {
- beforeEach(() => {
- window.gon.current_username = MOCK_USERNAME;
- createComponent(
- { search },
- {
- searchOptions: () => searchOptions,
- },
- );
- findGlobalSearchInput().vm.$emit('click');
- });
-
- it(`icon for data set type "${searchOptions[0]?.html_id}" ${
- hasIcon ? 'is' : 'is NOT'
- } rendered`, () => {
- expect(findScopeToken().findComponent(GlIcon).exists()).toBe(hasIcon);
- });
+ describe.each`
+ searchOptions | iconName
+ ${[MOCK_SCOPED_SEARCH_OPTIONS[0]]} | ${ICON_PROJECT}
+ ${[MOCK_SCOPED_SEARCH_OPTIONS[2]]} | ${ICON_GROUP}
+ ${[MOCK_SCOPED_SEARCH_OPTIONS[3]]} | ${ICON_SUBGROUP}
+ ${[MOCK_SCOPED_SEARCH_OPTIONS[4]]} | ${false}
+ `('token', ({ searchOptions, iconName }) => {
+ beforeEach(() => {
+ window.gon.current_username = MOCK_USERNAME;
+ createComponent(
+ { search: MOCK_SEARCH },
+ {
+ searchOptions: () => searchOptions,
+ },
+ );
+ findGlobalSearchInput().vm.$emit('click');
+ });
- it(`render ${iconName ? `"${iconName}"` : 'NO'} icon for data set type "${
- searchOptions[0]?.html_id
- }"`, () => {
- expect(
- findScopeToken().findComponent(GlIcon).exists() &&
- findScopeToken().findComponent(GlIcon).attributes('name'),
- ).toBe(iconName);
+ it(`renders ${iconName ? `"${iconName}"` : 'NO'} icon for "${
+ searchOptions[0]?.text
+ }" scope`, () => {
+ expect(
+ findScopeToken().findComponent(GlIcon).exists() &&
+ findScopeToken().findComponent(GlIcon).attributes('name'),
+ ).toBe(iconName);
+ });
});
});
@@ -319,7 +283,7 @@ describe('GlobalSearchModal', () => {
});
});
- describe.each(COMMON_HANDLES)(
+ describe.each([...COMMON_HANDLES, PATH_HANDLE])(
'when FF `command_palette` is enabled and search handle is %s',
(handle) => {
beforeEach(() => {
@@ -338,6 +302,10 @@ describe('GlobalSearchModal', () => {
SEARCH_OR_COMMAND_MODE_PLACEHOLDER,
);
});
+
+ it('should not render the scope token', () => {
+ expect(findScopeToken().exists()).toBe(false);
+ });
},
);
});
@@ -389,33 +357,41 @@ describe('GlobalSearchModal', () => {
});
describe('Submitting a search', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('onKey-enter submits a search', () => {
+ const submitSearch = () =>
findGlobalSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
- expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
- });
-
- describe('with less than min characters', () => {
+ describe('in command mode', () => {
beforeEach(() => {
- createComponent({ search: 'x' });
+ createComponent({ search: '>' }, undefined, undefined, {
+ commandPalette: true,
+ });
+ submitSearch();
});
- it('onKey-enter will NOT submit a search', () => {
- findGlobalSearchInput().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
+ it('does not submit a search', () => {
+ expect(visitUrl).not.toHaveBeenCalled();
+ });
+ });
+ describe('in search mode', () => {
+ it('will NOT submit a search with less than min characters', () => {
+ createComponent({ search: 'x' });
+ submitSearch();
expect(visitUrl).not.toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
});
+
+ it('will submit a search with the sufficient number of characters', () => {
+ createComponent();
+ submitSearch();
+ expect(visitUrl).toHaveBeenCalledWith(MOCK_SEARCH_QUERY);
+ });
});
});
});
describe('Modal events', () => {
beforeEach(() => {
- createComponent();
+ createComponent({ search: 'searchQuery' });
});
it('should emit `shown` event when modal shown`', () => {
@@ -423,9 +399,10 @@ describe('GlobalSearchModal', () => {
expect(wrapper.emitted('shown')).toHaveLength(1);
});
- it('should emit `hidden` event when modal hidden`', () => {
- findGlobalSearchModal().vm.$emit('hidden');
+ it('should emit `hidden` event when modal hidden and clear the search input', () => {
+ findGlobalSearchModal().vm.$emit('hide');
expect(wrapper.emitted('hidden')).toHaveLength(1);
+ expect(actionSpies.setSearch).toHaveBeenCalledWith(expect.any(Object), '');
});
});
});
diff --git a/spec/frontend/super_sidebar/components/global_search/mock_data.js b/spec/frontend/super_sidebar/components/global_search/mock_data.js
index 0884fce567c..ad7e7b0b30b 100644
--- a/spec/frontend/super_sidebar/components/global_search/mock_data.js
+++ b/spec/frontend/super_sidebar/components/global_search/mock_data.js
@@ -62,20 +62,6 @@ export const MOCK_SEARCH_CONTEXT = {
group_metadata: {},
};
-export const MOCK_SEARCH_CONTEXT_FULL = {
- group: {
- id: 31,
- name: 'testGroup',
- full_name: 'testGroup',
- },
- group_metadata: {
- group_path: 'testGroup',
- name: 'testGroup',
- issues_path: '/groups/testGroup/-/issues',
- mr_path: '/groups/testGroup/-/merge_requests',
- },
-};
-
export const MOCK_DEFAULT_SEARCH_OPTIONS = [
{
text: MSG_ISSUES_ASSIGNED_TO_ME,
diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js
index 6af1172e4d8..c92f8a68678 100644
--- a/spec/frontend/super_sidebar/components/help_center_spec.js
+++ b/spec/frontend/super_sidebar/components/help_center_spec.js
@@ -104,7 +104,7 @@ describe('HelpCenter component', () => {
createWrapper({ ...sidebarData, show_tanuki_bot: true });
});
- it('shows Ask GitLab Chat with the help items', () => {
+ it('shows Ask GitLab Duo with the help items', () => {
expect(findDropdownGroup(0).props('group').items).toEqual([
expect.objectContaining({
icon: 'tanuki-ai',
@@ -115,9 +115,9 @@ describe('HelpCenter component', () => {
]);
});
- describe('when Ask GitLab Chat button is clicked', () => {
+ describe('when Ask GitLab Duo button is clicked', () => {
beforeEach(() => {
- findButton('Ask GitLab Chat').click();
+ findButton('Ask GitLab Duo').click();
});
it('sets helpCenterState.showTanukiBotChatDrawer to true', () => {
diff --git a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
index 047dc9a6599..abd9c1dc44d 100644
--- a/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
+++ b/spec/frontend/super_sidebar/components/sidebar_peek_behavior_spec.js
@@ -9,6 +9,7 @@ import SidebarPeek, {
STATE_OPEN,
STATE_WILL_CLOSE,
} from '~/super_sidebar/components/sidebar_peek_behavior.vue';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
// These are measured at runtime in the browser, but statically defined here
// since Jest does not do layout/styling.
@@ -32,6 +33,7 @@ jest.mock('~/lib/utils/css_utils', () => ({
describe('SidebarPeek component', () => {
let wrapper;
+ let trackingSpy = null;
const createComponent = () => {
wrapper = mount(SidebarPeek);
@@ -54,6 +56,11 @@ describe('SidebarPeek component', () => {
beforeEach(() => {
createComponent();
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
});
it('begins in the closed state', () => {
@@ -87,6 +94,11 @@ describe('SidebarPeek component', () => {
jest.advanceTimersByTime(1);
expect(lastNChangeEvents(2)).toEqual([STATE_WILL_OPEN, STATE_OPEN]);
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_peek', {
+ label: 'nav_hover',
+ property: 'nav_sidebar',
+ });
});
it('cancels transition will-open -> open if mouse out of peek region', () => {
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index b76c637caf4..0c785109b5e 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -19,6 +19,7 @@ import {
isCollapsed,
} from '~/super_sidebar/super_sidebar_collapsed_state_manager';
import { stubComponent } from 'helpers/stub_component';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { sidebarData as mockSidebarData } from '../mock_data';
const initialSidebarState = { ...sidebarState };
@@ -49,6 +50,7 @@ describe('SuperSidebar component', () => {
const findTrialStatusWidget = () => wrapper.findByTestId(trialStatusWidgetStubTestId);
const findTrialStatusPopover = () => wrapper.findByTestId(trialStatusPopoverStubTestId);
const findSidebarMenu = () => wrapper.findComponent(SidebarMenu);
+ let trackingSpy = null;
const createWrapper = ({
provide = {},
@@ -77,6 +79,11 @@ describe('SuperSidebar component', () => {
beforeEach(() => {
Object.assign(sidebarState, initialSidebarState);
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
});
describe('default', () => {
@@ -143,12 +150,20 @@ describe('SuperSidebar component', () => {
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(1);
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(true, true);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', {
+ label: 'nav_toggle_keyboard_shortcut',
+ property: 'nav_sidebar',
+ });
isCollapsed.mockReturnValue(true);
Mousetrap.trigger('mod+\\');
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledTimes(2);
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_show', {
+ label: 'nav_toggle_keyboard_shortcut',
+ property: 'nav_sidebar',
+ });
jest.spyOn(Mousetrap, 'unbind');
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
index 8bb20186e16..23b735c2773 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
@@ -7,6 +7,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { JS_TOGGLE_COLLAPSE_CLASS, JS_TOGGLE_EXPAND_CLASS } from '~/super_sidebar/constants';
import SuperSidebarToggle from '~/super_sidebar/components/super_sidebar_toggle.vue';
import { toggleSuperSidebarCollapsed } from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
jest.mock('~/super_sidebar/super_sidebar_collapsed_state_manager.js', () => ({
toggleSuperSidebarCollapsed: jest.fn(),
@@ -61,7 +62,7 @@ describe('SuperSidebarToggle component', () => {
});
});
- describe('toolip', () => {
+ describe('tooltip', () => {
it('displays collapse when expanded', () => {
createWrapper();
expect(getTooltip().title).toBe(__('Hide sidebar'));
@@ -74,15 +75,19 @@ describe('SuperSidebarToggle component', () => {
});
describe('toggle', () => {
+ let trackingSpy = null;
+
beforeEach(() => {
setHTMLFixture(`
<button class="${JS_TOGGLE_COLLAPSE_CLASS}">Hide</button>
<button class="${JS_TOGGLE_EXPAND_CLASS}">Show</button>
`);
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
});
afterEach(() => {
resetHTMLFixture();
+ unmockTracking();
});
it('collapses the sidebar and focuses the other toggle', async () => {
@@ -93,6 +98,10 @@ describe('SuperSidebarToggle component', () => {
expect(document.activeElement).toEqual(
document.querySelector(`.${JS_TOGGLE_COLLAPSE_CLASS}`),
);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', {
+ label: 'nav_toggle',
+ property: 'nav_sidebar',
+ });
});
it('expands the sidebar and focuses the other toggle', async () => {
@@ -101,6 +110,10 @@ describe('SuperSidebarToggle component', () => {
await nextTick();
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true);
expect(document.activeElement).toEqual(document.querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`));
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_show', {
+ label: 'nav_toggle',
+ property: 'nav_sidebar',
+ });
});
});
});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
index ae48c0f2a75..272e0237219 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -7,7 +7,6 @@ import CreateMenu from '~/super_sidebar/components/create_menu.vue';
import SearchModal from '~/super_sidebar/components/global_search/components/global_search.vue';
import BrandLogo from 'jh_else_ce/super_sidebar/components/brand_logo.vue';
import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue';
-import Counter from '~/super_sidebar/components/counter.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
@@ -19,10 +18,9 @@ describe('UserBar component', () => {
let wrapper;
const findCreateMenu = () => wrapper.findComponent(CreateMenu);
- const findCounter = (at) => wrapper.findAllComponents(Counter).at(at);
- const findIssuesCounter = () => findCounter(0);
- const findMRsCounter = () => findCounter(1);
- const findTodosCounter = () => findCounter(2);
+ const findIssuesCounter = () => wrapper.findByTestId('issues-shortcut-button');
+ const findMRsCounter = () => wrapper.findByTestId('merge-requests-shortcut-button');
+ const findTodosCounter = () => wrapper.findByTestId('todos-shortcut-button');
const findMergeRequestMenu = () => wrapper.findComponent(MergeRequestMenu);
const findBrandLogo = () => wrapper.findComponent(BrandLogo);
const findCollapseButton = () => wrapper.findByTestId('super-sidebar-collapse-button');
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
index f0f18ca9185..662677be40f 100644
--- a/spec/frontend/super_sidebar/components/user_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -20,7 +20,7 @@ describe('UserMenu component', () => {
const closeDropdownSpy = jest.fn();
- const createWrapper = (userDataChanges = {}, stubs = {}) => {
+ const createWrapper = (userDataChanges = {}, stubs = {}, provide = {}) => {
wrapper = mountExtended(UserMenu, {
propsData: {
data: {
@@ -35,6 +35,8 @@ describe('UserMenu component', () => {
},
provide: {
toggleNewNavEndpoint,
+ isImpersonating: false,
+ ...provide,
},
});
@@ -50,6 +52,15 @@ describe('UserMenu component', () => {
});
});
+ it('decreases the dropdown offset when impersonating a user', () => {
+ createWrapper(null, null, { isImpersonating: true });
+
+ expect(findDropdown().props('dropdownOffset')).toEqual({
+ crossAxis: -179,
+ mainAxis: 4,
+ });
+ });
+
describe('Toggle button', () => {
let toggle;
diff --git a/spec/frontend/super_sidebar/components/user_name_group_spec.js b/spec/frontend/super_sidebar/components/user_name_group_spec.js
index 6e3b18d3107..bd02f3c17e3 100644
--- a/spec/frontend/super_sidebar/components/user_name_group_spec.js
+++ b/spec/frontend/super_sidebar/components/user_name_group_spec.js
@@ -91,7 +91,7 @@ describe('UserNameGroup component', () => {
});
it('should render status message', () => {
- expect(findUserStatus().text()).toContain(userMenuMockData.status.message);
+ expect(findUserStatus().html()).toContain(userMenuMockData.status.message_html);
});
it("sets the tooltip's target to the status container", () => {
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index a3a74f7aac8..72c67e34038 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -126,6 +126,7 @@ export const userMenuMockStatus = {
customized: false,
emoji: 'art',
message: 'Working on user menu in super sidebar',
+ message_html: '<gl-emoji></gl-emoji> Working on user menu in super sidebar',
availability: 'busy',
clear_after: '2023-02-09 20:06:35 UTC',
};
diff --git a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
index 771d1f07fea..9388d837186 100644
--- a/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
+++ b/spec/frontend/super_sidebar/super_sidebar_collapsed_state_manager_spec.js
@@ -11,8 +11,10 @@ import {
findPage,
bindSuperSidebarCollapsedEvents,
} from '~/super_sidebar/super_sidebar_collapsed_state_manager';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
const { xl, sm } = breakpoints;
+let trackingSpy = null;
jest.mock('~/lib/utils/common_utils', () => ({
getCookie: jest.fn(),
@@ -27,6 +29,15 @@ const pageHasCollapsedClass = (hasClass) => {
}
};
+const tracksCollapse = (shouldTrack) => {
+ if (shouldTrack) {
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', {
+ label: 'browser_resize',
+ property: 'nav_sidebar',
+ });
+ }
+};
+
describe('Super Sidebar Collapsed State Manager', () => {
beforeEach(() => {
setHTMLFixture(`
@@ -34,10 +45,12 @@ describe('Super Sidebar Collapsed State Manager', () => {
<aside class="super-sidebar"></aside>
</div>
`);
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
});
afterEach(() => {
resetHTMLFixture();
+ unmockTracking();
});
describe('toggleSuperSidebarCollapsed', () => {
@@ -109,14 +122,20 @@ describe('Super Sidebar Collapsed State Manager', () => {
});
it.each`
- initialWindowWidth | updatedWindowWidth | hasClassBeforeResize | hasClassAfterResize
- ${xl} | ${sm} | ${false} | ${true}
- ${sm} | ${xl} | ${true} | ${false}
- ${xl} | ${xl} | ${false} | ${false}
- ${sm} | ${sm} | ${true} | ${true}
+ initialWindowWidth | updatedWindowWidth | hasClassBeforeResize | hasClassAfterResize | sendsTrackingEvent
+ ${xl} | ${sm} | ${false} | ${true} | ${true}
+ ${sm} | ${xl} | ${true} | ${false} | ${false}
+ ${xl} | ${xl} | ${false} | ${false} | ${false}
+ ${sm} | ${sm} | ${true} | ${true} | ${false}
`(
'when changing width from $initialWindowWidth to $updatedWindowWidth expect page to have collapsed class before resize to be $hasClassBeforeResize and after resize to be $hasClassAfterResize',
- ({ initialWindowWidth, updatedWindowWidth, hasClassBeforeResize, hasClassAfterResize }) => {
+ ({
+ initialWindowWidth,
+ updatedWindowWidth,
+ hasClassBeforeResize,
+ hasClassAfterResize,
+ sendsTrackingEvent,
+ }) => {
getCookie.mockReturnValue(undefined);
window.innerWidth = initialWindowWidth;
initSuperSidebarCollapsedState();
@@ -129,6 +148,7 @@ describe('Super Sidebar Collapsed State Manager', () => {
window.dispatchEvent(new Event('resize'));
pageHasCollapsedClass(hasClassAfterResize);
+ tracksCollapse(sendsTrackingEvent);
},
);
});
diff --git a/spec/frontend/tags/components/delete_tag_modal_spec.js b/spec/frontend/tags/components/delete_tag_modal_spec.js
index 8ec9925563a..5a3104fad9b 100644
--- a/spec/frontend/tags/components/delete_tag_modal_spec.js
+++ b/spec/frontend/tags/components/delete_tag_modal_spec.js
@@ -11,6 +11,9 @@ let wrapper;
const tagName = 'test-tag';
const path = '/path/to/tag';
const isProtected = false;
+const modalHideSpy = jest.fn();
+const modalShowSpy = jest.fn();
+const formSubmitSpy = jest.spyOn(HTMLFormElement.prototype, 'submit').mockImplementation();
const createComponent = (data = {}) => {
wrapper = extendedWrapper(
@@ -27,6 +30,10 @@ const createComponent = (data = {}) => {
GlModal: stubComponent(GlModal, {
template:
'<div><slot name="modal-title"></slot><slot></slot><slot name="modal-footer"></slot></div>',
+ methods: {
+ hide: modalHideSpy,
+ show: modalShowSpy,
+ },
}),
GlButton,
GlFormInput,
@@ -61,32 +68,26 @@ describe('Delete tag modal', () => {
});
it('submits the form when the delete button is clicked', () => {
- const submitFormSpy = jest.spyOn(wrapper.vm.$refs.form, 'submit');
-
findDeleteButton().trigger('click');
expect(findForm().attributes('action')).toBe(path);
- expect(submitFormSpy).toHaveBeenCalled();
+ expect(formSubmitSpy).toHaveBeenCalledTimes(1);
});
it('calls show on the modal when a `openModal` event is received through the event hub', () => {
- const showSpy = jest.spyOn(wrapper.vm.$refs.modal, 'show');
-
eventHub.$emit('openModal', {
isProtected,
tagName,
path,
});
- expect(showSpy).toHaveBeenCalled();
+ expect(modalShowSpy).toHaveBeenCalled();
});
it('calls hide on the modal when cancel button is clicked', () => {
- const closeModalSpy = jest.spyOn(wrapper.vm.$refs.modal, 'hide');
-
findCancelButton().trigger('click');
- expect(closeModalSpy).toHaveBeenCalled();
+ expect(modalHideSpy).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/token_access/outbound_token_access_spec.js b/spec/frontend/token_access/outbound_token_access_spec.js
index 7f321495d72..f9eb201eb5c 100644
--- a/spec/frontend/token_access/outbound_token_access_spec.js
+++ b/spec/frontend/token_access/outbound_token_access_spec.js
@@ -6,7 +6,6 @@ import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_help
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import OutboundTokenAccess from '~/token_access/components/outbound_token_access.vue';
-import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql';
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';
@@ -15,7 +14,6 @@ import {
enabledJobTokenScope,
disabledJobTokenScope,
projectsWithScope,
- addProjectSuccess,
removeProjectSuccess,
updateScopeSuccess,
} from './mock_data';
@@ -34,16 +32,13 @@ describe('TokenAccess component', () => {
const enabledJobTokenScopeHandler = jest.fn().mockResolvedValue(enabledJobTokenScope);
const disabledJobTokenScopeHandler = jest.fn().mockResolvedValue(disabledJobTokenScope);
const getProjectsWithScopeHandler = jest.fn().mockResolvedValue(projectsWithScope);
- const addProjectSuccessHandler = jest.fn().mockResolvedValue(addProjectSuccess);
const removeProjectSuccessHandler = jest.fn().mockResolvedValue(removeProjectSuccess);
const updateScopeSuccessHandler = jest.fn().mockResolvedValue(updateScopeSuccess);
const failureHandler = jest.fn().mockRejectedValue(error);
const findToggle = () => wrapper.findComponent(GlToggle);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findAddProjectBtn = () => wrapper.findByRole('button', { name: 'Add project' });
const findRemoveProjectBtn = () => wrapper.findByRole('button', { name: 'Remove access' });
- const findTokenDisabledAlert = () => wrapper.findByTestId('token-disabled-alert');
const findDeprecationAlert = () => wrapper.findByTestId('deprecation-alert');
const findProjectPathInput = () => wrapper.findByTestId('project-path-input');
@@ -51,19 +46,10 @@ describe('TokenAccess component', () => {
return createMockApollo(requestHandlers);
};
- const createComponent = (
- requestHandlers,
- mountFn = shallowMountExtended,
- frozenOutboundJobTokenScopes = false,
- frozenOutboundJobTokenScopesOverride = false,
- ) => {
+ const createComponent = (requestHandlers, mountFn = shallowMountExtended) => {
wrapper = mountFn(OutboundTokenAccess, {
provide: {
fullPath: projectPath,
- glFeatures: {
- frozenOutboundJobTokenScopes,
- frozenOutboundJobTokenScopesOverride,
- },
},
apolloProvider: createMockApolloProvider(requestHandlers),
data() {
@@ -141,19 +127,6 @@ describe('TokenAccess component', () => {
await waitForPromises();
expect(findToggle().props('value')).toBe(true);
- expect(findTokenDisabledAlert().exists()).toBe(false);
- });
-
- it('the toggle is off and the alert is visible', async () => {
- createComponent([
- [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
- ]);
-
- await waitForPromises();
-
- expect(findToggle().props('value')).toBe(false);
- expect(findTokenDisabledAlert().exists()).toBe(true);
});
describe('update ci job token scope', () => {
@@ -196,48 +169,37 @@ describe('TokenAccess component', () => {
expect(createAlert).toHaveBeenCalledWith({ message });
});
});
- });
- describe('add project', () => {
- it('calls add project mutation', async () => {
+ it('the toggle is off and the deprecation alert is visible', async () => {
createComponent(
[
- [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
[getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
- [addProjectCIJobTokenScopeMutation, addProjectSuccessHandler],
],
- mountExtended,
+ shallowMountExtended,
+ true,
);
await waitForPromises();
- findAddProjectBtn().trigger('click');
-
- expect(addProjectSuccessHandler).toHaveBeenCalledWith({
- input: {
- projectPath,
- targetProjectPath: 'root/test',
- },
- });
+ expect(findToggle().props('value')).toBe(false);
+ expect(findToggle().props('disabled')).toBe(true);
+ expect(findDeprecationAlert().exists()).toBe(true);
});
- it('add project handles error correctly', async () => {
+ it('contains a warning message about disabling the current configuration', async () => {
createComponent(
[
- [getCIJobTokenScopeQuery, enabledJobTokenScopeHandler],
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
[getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
- [addProjectCIJobTokenScopeMutation, failureHandler],
],
mountExtended,
+ true,
);
await waitForPromises();
- findAddProjectBtn().trigger('click');
-
- await waitForPromises();
-
- expect(createAlert).toHaveBeenCalledWith({ message });
+ expect(findToggle().text()).toContain('Disabling this feature is a permanent change.');
});
});
@@ -284,58 +246,21 @@ describe('TokenAccess component', () => {
});
});
- describe('with the frozenOutboundJobTokenScopes feature flag enabled', () => {
- describe('toggle', () => {
- it('the toggle is off and the deprecation alert is visible', async () => {
- createComponent(
- [
- [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
- ],
- shallowMountExtended,
- true,
- );
-
- await waitForPromises();
-
- expect(findToggle().props('value')).toBe(false);
- expect(findToggle().props('disabled')).toBe(true);
- expect(findDeprecationAlert().exists()).toBe(true);
- expect(findTokenDisabledAlert().exists()).toBe(false);
- });
-
- it('contains a warning message about disabling the current configuration', async () => {
- createComponent(
- [
- [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
- ],
- mountExtended,
- true,
- );
-
- await waitForPromises();
-
- expect(findToggle().text()).toContain('Disabling this feature is a permanent change.');
- });
- });
-
- describe('adding a new project', () => {
- it('disables the input to add new projects', async () => {
- createComponent(
- [
- [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
- [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
- ],
- mountExtended,
- true,
- false,
- );
+ describe('adding a new project', () => {
+ it('disables the input to add new projects', async () => {
+ createComponent(
+ [
+ [getCIJobTokenScopeQuery, disabledJobTokenScopeHandler],
+ [getProjectsWithCIJobTokenScopeQuery, getProjectsWithScopeHandler],
+ ],
+ mountExtended,
+ true,
+ false,
+ );
- await waitForPromises();
+ await waitForPromises();
- expect(findProjectPathInput().attributes('disabled')).toBe('disabled');
- });
+ expect(findProjectPathInput().attributes('disabled')).toBe('disabled');
});
});
});
diff --git a/spec/frontend/tracing/components/tracing_empty_state_spec.js b/spec/frontend/tracing/components/tracing_empty_state_spec.js
new file mode 100644
index 00000000000..c3df187e1c5
--- /dev/null
+++ b/spec/frontend/tracing/components/tracing_empty_state_spec.js
@@ -0,0 +1,44 @@
+import { GlButton, GlEmptyState } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TracingEmptyState from '~/tracing/components/tracing_empty_state.vue';
+
+describe('TracingEmptyState', () => {
+ let wrapper;
+
+ const findEnableButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ wrapper = shallowMountExtended(TracingEmptyState, {
+ propsData: {
+ enableTracing: jest.fn(),
+ },
+ stubs: { GlButton },
+ });
+ });
+
+ it('renders the component properly', () => {
+ expect(wrapper.exists()).toBe(true);
+ });
+
+ it('displays the correct title', () => {
+ const { title } = wrapper.findComponent(GlEmptyState).props();
+ expect(title).toBe('Get started with Tracing');
+ });
+
+ it('displays the correct description', () => {
+ const description = wrapper.find('span').text();
+ expect(description).toBe('Monitor your applications with GitLab Distributed Tracing.');
+ });
+
+ it('displays the enable button', () => {
+ const enableButton = findEnableButton();
+ expect(enableButton.exists()).toBe(true);
+ expect(enableButton.text()).toBe('Enable');
+ });
+
+ it('calls enableTracing method when enable button is clicked', () => {
+ findEnableButton().vm.$emit('click');
+
+ expect(wrapper.props().enableTracing).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/tracing/components/tracing_list_spec.js b/spec/frontend/tracing/components/tracing_list_spec.js
new file mode 100644
index 00000000000..183578cff31
--- /dev/null
+++ b/spec/frontend/tracing/components/tracing_list_spec.js
@@ -0,0 +1,131 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import TracingList from '~/tracing/components/tracing_list.vue';
+import TracingEmptyState from '~/tracing/components/tracing_empty_state.vue';
+import TracingTableList from '~/tracing/components/tracing_table_list.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+
+jest.mock('~/alert');
+
+describe('TracingList', () => {
+ let wrapper;
+ let observabilityClientMock;
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findEmptyState = () => wrapper.findComponent(TracingEmptyState);
+ const findTableList = () => wrapper.findComponent(TracingTableList);
+
+ const mountComponent = async () => {
+ wrapper = shallowMountExtended(TracingList, {
+ propsData: {
+ observabilityClient: observabilityClientMock,
+ stubs: {
+ GlLoadingIcon: true,
+ TracingEmptyState: true,
+ TracingTableList: true,
+ },
+ },
+ });
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ observabilityClientMock = {
+ isTracingEnabled: jest.fn(),
+ enableTraces: jest.fn(),
+ fetchTraces: jest.fn(),
+ };
+ });
+
+ it('renders the loading indicator while checking if tracing is enabled', () => {
+ mountComponent();
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(observabilityClientMock.isTracingEnabled).toHaveBeenCalled();
+ });
+
+ describe('when tracing is enabled', () => {
+ const mockTraces = ['trace1', 'trace2'];
+ beforeEach(async () => {
+ observabilityClientMock.isTracingEnabled.mockResolvedValueOnce(true);
+ observabilityClientMock.fetchTraces.mockResolvedValueOnce(mockTraces);
+
+ await mountComponent();
+ });
+ it('fetches the traces and renders the trace list', () => {
+ expect(observabilityClientMock.isTracingEnabled).toHaveBeenCalled();
+ expect(observabilityClientMock.fetchTraces).toHaveBeenCalled();
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findTableList().exists()).toBe(true);
+ expect(findTableList().props('traces')).toBe(mockTraces);
+ });
+
+ it('calls fetchTraces method when TracingTableList emits reload event', () => {
+ observabilityClientMock.fetchTraces.mockClear();
+ observabilityClientMock.fetchTraces.mockResolvedValueOnce(['trace1']);
+
+ findTableList().vm.$emit('reload');
+
+ expect(observabilityClientMock.fetchTraces).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when tracing is not enabled', () => {
+ beforeEach(async () => {
+ observabilityClientMock.isTracingEnabled.mockResolvedValueOnce(false);
+ observabilityClientMock.fetchTraces.mockResolvedValueOnce([]);
+
+ await mountComponent();
+ });
+
+ it('renders TracingEmptyState', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('set enableTracing as TracingEmptyState enable-tracing callback', () => {
+ findEmptyState().props('enableTracing')();
+
+ expect(observabilityClientMock.enableTraces).toHaveBeenCalled();
+ });
+ });
+
+ describe('error handling', () => {
+ it('if isTracingEnabled fails, it renders an alert and empty page', async () => {
+ observabilityClientMock.isTracingEnabled.mockRejectedValueOnce('error');
+
+ await mountComponent();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Failed to load page.' });
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
+ expect(findTableList().exists()).toBe(false);
+ });
+
+ it('if fetchTraces fails, it renders an alert and empty list', async () => {
+ observabilityClientMock.fetchTraces.mockRejectedValueOnce('error');
+ observabilityClientMock.isTracingEnabled.mockReturnValueOnce(true);
+
+ await mountComponent();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Failed to load traces.' });
+ expect(findTableList().exists()).toBe(true);
+ expect(findTableList().props('traces')).toEqual([]);
+ });
+
+ it('if enableTraces fails, it renders an alert and empty-state', async () => {
+ observabilityClientMock.isTracingEnabled.mockReturnValueOnce(false);
+ observabilityClientMock.enableTraces.mockRejectedValueOnce('error');
+
+ await mountComponent();
+
+ findEmptyState().props('enableTracing')();
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message: 'Failed to enable tracing.' });
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findTableList().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/tracing/components/tracing_table_list_spec.js b/spec/frontend/tracing/components/tracing_table_list_spec.js
new file mode 100644
index 00000000000..773b3eb8ed2
--- /dev/null
+++ b/spec/frontend/tracing/components/tracing_table_list_spec.js
@@ -0,0 +1,63 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import TracingTableList from '~/tracing/components/tracing_table_list.vue';
+
+describe('TracingTableList', () => {
+ let wrapper;
+ const mockTraces = [
+ {
+ timestamp: '2023-07-10T15:02:30.677538Z',
+ service_name: 'tracegen',
+ operation: 'lets-go',
+ duration: 150,
+ },
+ {
+ timestamp: '2023-07-10T15:02:30.677538Z',
+ service_name: 'tracegen',
+ operation: 'lets-go',
+ duration: 200,
+ },
+ ];
+
+ const mountComponent = ({ traces = mockTraces } = {}) => {
+ wrapper = mountExtended(TracingTableList, {
+ propsData: {
+ traces,
+ },
+ });
+ };
+
+ const getRows = () => wrapper.findComponent({ name: 'GlTable' }).find('tbody').findAll('tr');
+
+ const getCells = (trIdx) => getRows().at(trIdx).findAll('td');
+
+ const getCell = (trIdx, tdIdx) => {
+ return getCells(trIdx).at(tdIdx);
+ };
+
+ it('renders traces as table', () => {
+ mountComponent();
+
+ const rows = wrapper.findAll('table tbody tr');
+
+ expect(rows.length).toBe(mockTraces.length);
+
+ mockTraces.forEach((trace, i) => {
+ expect(getCells(i).length).toBe(4);
+ expect(getCell(i, 0).text()).toBe(trace.timestamp);
+ expect(getCell(i, 1).text()).toBe(trace.service_name);
+ expect(getCell(i, 2).text()).toBe(trace.operation);
+ expect(getCell(i, 3).text()).toBe(`${trace.duration} ms`);
+ });
+ });
+
+ it('renders the empty state when no traces are provided', () => {
+ mountComponent({ traces: [] });
+
+ expect(getCell(0, 0).text()).toContain('No traces to display');
+ const link = getCell(0, 0).findComponent({ name: 'GlLink' });
+ expect(link.text()).toBe('Check again');
+
+ link.trigger('click');
+ expect(wrapper.emitted('reload')).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/tracing/list_index_spec.js b/spec/frontend/tracing/list_index_spec.js
new file mode 100644
index 00000000000..a5759035c2f
--- /dev/null
+++ b/spec/frontend/tracing/list_index_spec.js
@@ -0,0 +1,37 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ListIndex from '~/tracing/list_index.vue';
+import TracingList from '~/tracing/components/tracing_list.vue';
+import ObservabilityContainer from '~/observability/components/observability_container.vue';
+
+describe('ListIndex', () => {
+ const props = {
+ oauthUrl: 'https://example.com/oauth',
+ tracingUrl: 'https://example.com/tracing',
+ provisioningUrl: 'https://example.com/provisioning',
+ };
+
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = shallowMountExtended(ListIndex, {
+ propsData: props,
+ });
+ };
+
+ it('renders ObservabilityContainer component', () => {
+ mountComponent();
+
+ const observabilityContainer = wrapper.findComponent(ObservabilityContainer);
+ expect(observabilityContainer.exists()).toBe(true);
+ expect(observabilityContainer.props('oauthUrl')).toBe(props.oauthUrl);
+ expect(observabilityContainer.props('tracingUrl')).toBe(props.tracingUrl);
+ expect(observabilityContainer.props('provisioningUrl')).toBe(props.provisioningUrl);
+ });
+
+ it('renders TracingList component inside ObservabilityContainer', () => {
+ mountComponent();
+
+ const observabilityContainer = wrapper.findComponent(ObservabilityContainer);
+ expect(observabilityContainer.findComponent(TracingList).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/tracking/internal_events_spec.js b/spec/frontend/tracking/internal_events_spec.js
new file mode 100644
index 00000000000..ad2ffa7cef4
--- /dev/null
+++ b/spec/frontend/tracking/internal_events_spec.js
@@ -0,0 +1,100 @@
+import API from '~/api';
+import { mockTracking } from 'helpers/tracking_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import InternalEvents from '~/tracking/internal_events';
+import { GITLAB_INTERNAL_EVENT_CATEGORY, SERVICE_PING_SCHEMA } from '~/tracking/constants';
+import * as utils from '~/tracking/utils';
+import { Tracker } from '~/tracking/tracker';
+
+jest.mock('~/api', () => ({
+ trackRedisHllUserEvent: jest.fn(),
+}));
+
+jest.mock('~/tracking/utils', () => ({
+ ...jest.requireActual('~/tracking/utils'),
+ getInternalEventHandlers: jest.fn(),
+}));
+
+Tracker.enabled = jest.fn();
+
+describe('InternalEvents', () => {
+ describe('track_event', () => {
+ it('track_event calls trackRedisHllUserEvent with correct arguments', () => {
+ const event = 'TestEvent';
+
+ InternalEvents.track_event(event);
+
+ expect(API.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
+ expect(API.trackRedisHllUserEvent).toHaveBeenCalledWith(event);
+ });
+
+ it('track_event calls tracking.event functions with correct arguments', () => {
+ const trackingSpy = mockTracking(GITLAB_INTERNAL_EVENT_CATEGORY, undefined, jest.spyOn);
+
+ const event = 'TestEvent';
+
+ InternalEvents.track_event(event);
+
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith(GITLAB_INTERNAL_EVENT_CATEGORY, event, {
+ context: {
+ schema: SERVICE_PING_SCHEMA,
+ data: {
+ event_name: event,
+ data_source: 'redis_hll',
+ },
+ },
+ });
+ });
+ });
+
+ describe('mixin', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ const Component = {
+ render() {},
+ mixins: [InternalEvents.mixin()],
+ };
+ wrapper = shallowMountExtended(Component);
+ });
+
+ it('this.track_event function calls InternalEvent`s track function with an event', () => {
+ const event = 'TestEvent';
+ const trackEventSpy = jest.spyOn(InternalEvents, 'track_event');
+
+ wrapper.vm.track_event(event);
+
+ expect(trackEventSpy).toHaveBeenCalledTimes(1);
+ expect(trackEventSpy).toHaveBeenCalledWith(event);
+ });
+ });
+
+ describe('bindInternalEventDocument', () => {
+ it('should not bind event handlers if tracker is not enabled', () => {
+ Tracker.enabled.mockReturnValue(false);
+ const result = InternalEvents.bindInternalEventDocument();
+ expect(result).toEqual([]);
+ expect(utils.getInternalEventHandlers).not.toHaveBeenCalled();
+ });
+
+ it('should not bind event handlers if already bound', () => {
+ Tracker.enabled.mockReturnValue(true);
+ document.internalEventsTrackingBound = true;
+ const result = InternalEvents.bindInternalEventDocument();
+ expect(result).toEqual([]);
+ expect(utils.getInternalEventHandlers).not.toHaveBeenCalled();
+ });
+
+ it('should bind event handlers when not bound yet', () => {
+ Tracker.enabled.mockReturnValue(true);
+ document.internalEventsTrackingBound = false;
+ const addEventListenerMock = jest.spyOn(document, 'addEventListener');
+
+ const result = InternalEvents.bindInternalEventDocument();
+
+ expect(addEventListenerMock).toHaveBeenCalledWith('click', expect.any(Function));
+ expect(result).toEqual({ name: 'click', func: expect.any(Function) });
+ });
+ });
+});
diff --git a/spec/frontend/tracking/tracking_spec.js b/spec/frontend/tracking/tracking_spec.js
index c23790bb589..55ce8039399 100644
--- a/spec/frontend/tracking/tracking_spec.js
+++ b/spec/frontend/tracking/tracking_spec.js
@@ -59,7 +59,6 @@ describe('Tracking', () => {
window.doNotTrack = undefined;
navigator.doNotTrack = undefined;
navigator.msDoNotTrack = undefined;
- jest.clearAllMocks();
});
it('tracks to snowplow (our current tracking system)', () => {
diff --git a/spec/frontend/tracking/utils_spec.js b/spec/frontend/tracking/utils_spec.js
index d6f2c5095b4..7ba65cce15d 100644
--- a/spec/frontend/tracking/utils_spec.js
+++ b/spec/frontend/tracking/utils_spec.js
@@ -4,6 +4,8 @@ import {
addExperimentContext,
addReferrersCacheEntry,
filterOldReferrersCacheEntries,
+ InternalEventHandler,
+ createInternalEventPayload,
} from '~/tracking/utils';
import { TRACKING_CONTEXT_SCHEMA } from '~/experimentation/constants';
import { REFERRER_TTL, URLS_CACHE_STORAGE_KEY } from '~/tracking/constants';
@@ -95,5 +97,40 @@ describe('~/tracking/utils', () => {
expect(cache[0].timestamp).toBeDefined();
});
});
+
+ describe('createInternalEventPayload', () => {
+ it('should return event name from element', () => {
+ const mockEl = { dataset: { eventTracking: 'click' } };
+ const result = createInternalEventPayload(mockEl);
+ expect(result).toEqual('click');
+ });
+ });
+
+ describe('InternalEventHandler', () => {
+ it.each([
+ ['should call the provided function with the correct event payload', 'click', true],
+ [
+ 'should not call the provided function if the closest matching element is not found',
+ null,
+ false,
+ ],
+ ])('%s', (_, payload, shouldCallFunc) => {
+ const mockFunc = jest.fn();
+ const mockEl = payload ? { dataset: { eventTracking: payload } } : null;
+ const mockEvent = {
+ target: {
+ closest: jest.fn().mockReturnValue(mockEl),
+ },
+ };
+
+ InternalEventHandler(mockEvent, mockFunc);
+
+ if (shouldCallFunc) {
+ expect(mockFunc).toHaveBeenCalledWith(payload);
+ } else {
+ expect(mockFunc).not.toHaveBeenCalled();
+ }
+ });
+ });
});
});
diff --git a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
index 2662711076b..7fef20c900e 100644
--- a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
+++ b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
@@ -19,7 +19,7 @@ function findStorageTypeUsagesSerialized() {
.wrappers.map((wp) => wp.element.style.flex);
}
-describe('Storage Counter usage graph component', () => {
+describe('UsageGraph', () => {
beforeEach(() => {
data = {
rootStorageStatistics: {
@@ -29,7 +29,6 @@ describe('Storage Counter usage graph component', () => {
containerRegistrySize: 2500,
lfsObjectsSize: 2000,
buildArtifactsSize: 700,
- pipelineArtifactsSize: 300,
snippetsSize: 2000,
storageSize: 17000,
},
@@ -43,7 +42,6 @@ describe('Storage Counter usage graph component', () => {
const {
buildArtifactsSize,
- pipelineArtifactsSize,
lfsObjectsSize,
packagesSize,
containerRegistrySize,
@@ -69,9 +67,6 @@ describe('Storage Counter usage graph component', () => {
expect(types.at(6).text()).toMatchInterpolatedText(
`Job artifacts ${numberToHumanSize(buildArtifactsSize)}`,
);
- expect(types.at(7).text()).toMatchInterpolatedText(
- `Pipeline artifacts ${numberToHumanSize(pipelineArtifactsSize)}`,
- );
});
describe('when storage type is not used', () => {
@@ -111,7 +106,6 @@ describe('Storage Counter usage graph component', () => {
'0.11764705882352941',
'0.11764705882352941',
'0.041176470588235294',
- '0.01764705882352941',
]);
});
});
@@ -131,7 +125,6 @@ describe('Storage Counter usage graph component', () => {
'0.11764705882352941',
'0.11764705882352941',
'0.041176470588235294',
- '0.01764705882352941',
]);
});
});
diff --git a/spec/frontend/usage_quotas/storage/mock_data.js b/spec/frontend/usage_quotas/storage/mock_data.js
index 8a7f941151b..452fa83b9a7 100644
--- a/spec/frontend/usage_quotas/storage/mock_data.js
+++ b/spec/frontend/usage_quotas/storage/mock_data.js
@@ -5,7 +5,7 @@ export const mockEmptyResponse = { data: { project: null } };
export const projectData = {
storage: {
- totalUsage: '13.8 MiB',
+ totalUsage: '13.4 MiB',
storageTypes: [
{
storageType: {
@@ -29,15 +29,6 @@ export const projectData = {
},
{
storageType: {
- id: 'pipelineArtifacts',
- name: 'Pipeline artifacts',
- description: 'Pipeline artifacts created by CI/CD.',
- helpPath: '/pipeline-artifacts',
- },
- value: 400000,
- },
- {
- storageType: {
id: 'lfsObjects',
name: 'LFS',
description: 'Audio samples, videos, datasets, and graphics.',
@@ -93,7 +84,6 @@ export const projectHelpLinks = {
containerRegistry: '/container_registry',
usageQuotas: '/usage-quotas',
buildArtifacts: '/build-artifacts',
- pipelineArtifacts: '/pipeline-artifacts',
lfsObjects: '/lsf-objects',
packages: '/packages',
repository: '/repository',
diff --git a/spec/frontend/users/profile/actions/components/user_actions_app_spec.js b/spec/frontend/users/profile/actions/components/user_actions_app_spec.js
new file mode 100644
index 00000000000..d27962440ee
--- /dev/null
+++ b/spec/frontend/users/profile/actions/components/user_actions_app_spec.js
@@ -0,0 +1,38 @@
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import UserActionsApp from '~/users/profile/actions/components/user_actions_app.vue';
+
+describe('User Actions App', () => {
+ let wrapper;
+
+ const USER_ID = 'test-id';
+
+ const createWrapper = (propsData = {}) => {
+ wrapper = mountExtended(UserActionsApp, {
+ propsData: {
+ userId: USER_ID,
+ ...propsData,
+ },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findActions = () => wrapper.findAllByTestId('disclosure-dropdown-item');
+ const findAction = (position = 0) => findActions().at(position);
+
+ it('shows dropdown', () => {
+ createWrapper();
+ expect(findDropdown().exists()).toBe(true);
+ });
+
+ it('shows actions correctly', () => {
+ createWrapper();
+ expect(findActions()).toHaveLength(1);
+ });
+
+ it('shows copy user id action', () => {
+ createWrapper();
+ expect(findAction().text()).toBe(`Copy user ID: ${USER_ID}`);
+ expect(findAction().findComponent('button').attributes('data-clipboard-text')).toBe(USER_ID);
+ });
+});
diff --git a/spec/frontend/vue_compat_test_setup.js b/spec/frontend/vue_compat_test_setup.js
index 6eba9465c80..fe43f8f2617 100644
--- a/spec/frontend/vue_compat_test_setup.js
+++ b/spec/frontend/vue_compat_test_setup.js
@@ -76,9 +76,77 @@ if (global.document) {
Vue.configureCompat(compatConfig);
installVTUCompat(VTU, fullCompatConfig, compatH);
+
+ jest.mock('vue', () => {
+ const actualVue = jest.requireActual('vue');
+ actualVue.configureCompat(compatConfig);
+ return actualVue;
+ });
+
+ jest.mock('@vue/test-utils', () => {
+ const actualVTU = jest.requireActual('@vue/test-utils');
+
+ return {
+ ...actualVTU,
+ RouterLinkStub: {
+ ...actualVTU.RouterLinkStub,
+ render() {
+ const { default: defaultSlot } = this.$slots ?? {};
+ const defaultSlotFn =
+ defaultSlot && typeof defaultSlot !== 'function' ? () => defaultSlot : defaultSlot;
+ return actualVTU.RouterLinkStub.render.call({
+ $slots: defaultSlot ? { default: defaultSlotFn } : undefined,
+ custom: this.custom,
+ });
+ },
+ },
+ };
+ });
+
+ jest.mock('portal-vue', () => ({
+ __esModule: true,
+ default: {
+ install: jest.fn(),
+ },
+ Portal: {},
+ PortalTarget: {},
+ MountingPortal: {
+ template: '<h1>MOUNTING-PORTAL</h1>',
+ },
+ Wormhole: {},
+ }));
+
VTU.config.global.renderStubDefaultSlot = true;
const noop = () => {};
+ const invalidProperties = new Set();
+
+ const getDescriptor = (root, prop) => {
+ let obj = root;
+ while (obj != null) {
+ const desc = Object.getOwnPropertyDescriptor(obj, prop);
+ if (desc) {
+ return desc;
+ }
+ obj = Object.getPrototypeOf(obj);
+ }
+ return null;
+ };
+
+ const isPropertyValidOnDomNode = (prop) => {
+ if (invalidProperties.has(prop)) {
+ return false;
+ }
+
+ const domNode = document.createElement('anonymous-stub');
+ const descriptor = getDescriptor(domNode, prop);
+ if (descriptor && descriptor.get && !descriptor.set) {
+ invalidProperties.add(prop);
+ return false;
+ }
+
+ return true;
+ };
VTU.config.plugins.createStubs = ({ name, component: rawComponent, registerStub }) => {
const component = unwrapLegacyVueExtendComponent(rawComponent);
@@ -126,7 +194,11 @@ if (global.document) {
.filter(Boolean)
: renderSlotByName('default');
- return Vue.h(`${hyphenatedName || 'anonymous'}-stub`, this.$props, slotContents);
+ const props = Object.fromEntries(
+ Object.entries(this.$props).filter(([prop]) => isPropertyValidOnDomNode(prop)),
+ );
+
+ return Vue.h(`${hyphenatedName || 'anonymous'}-stub`, props, slotContents);
},
});
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 e4febda1daa..b0f9f123950 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
@@ -1,22 +1,22 @@
-import { GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
const commits = [
{
title: 'Commit 1',
- short_id: '78d5b7',
+ shortId: '78d5b7',
message: 'Update test.txt',
},
{
title: 'Commit 2',
- short_id: '34cbe28b',
+ shortId: '34cbe28b',
message: 'Fixed test',
},
{
title: 'Commit 3',
- short_id: 'fa42932a',
+ shortId: 'fa42932a',
message: 'Added changelog',
},
];
@@ -25,10 +25,14 @@ describe('Commits message dropdown component', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMount(CommitMessageDropdown, {
+ wrapper = mount(CommitMessageDropdown, {
propsData: {
commits,
},
+ stubs: {
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
+ },
});
};
@@ -36,7 +40,7 @@ describe('Commits message dropdown component', () => {
createComponent();
});
- const findDropdownElements = () => wrapper.findAllComponents(GlDropdownItem);
+ const findDropdownElements = () => wrapper.findAllComponents(GlDisclosureDropdownItem);
const findFirstDropdownElement = () => findDropdownElements().at(0);
it('should have 3 elements in dropdown list', () => {
@@ -48,10 +52,9 @@ describe('Commits message dropdown component', () => {
expect(findFirstDropdownElement().text()).toContain('Commit 1');
});
- it('should emit a commit title on selecting commit', async () => {
- findFirstDropdownElement().vm.$emit('click');
+ it('should emit a commit title on selecting commit', () => {
+ findFirstDropdownElement().find('button').trigger('click');
- await nextTick();
expect(wrapper.emitted().input[0]).toEqual(['Update test.txt']);
});
});
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 38e5422325a..e1c88d7d3b6 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,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, mount } 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';
@@ -8,17 +8,14 @@ describe('MRWidgetFailedToMerge', () => {
const dummyIntervalId = 1337;
let wrapper;
- const createComponent = (props = {}, data = {}) => {
- wrapper = shallowMount(MrWidgetFailedToMerge, {
+ const createComponent = (props = {}, mountFn = shallowMount) => {
+ wrapper = mountFn(MrWidgetFailedToMerge, {
propsData: {
mr: {
mergeError: 'Merge error happened',
},
...props,
},
- data() {
- return data;
- },
});
};
@@ -121,7 +118,9 @@ describe('MRWidgetFailedToMerge', () => {
describe('while it is refreshing', () => {
it('renders Refresing now', async () => {
- createComponent({}, { isRefreshing: true });
+ createComponent({});
+
+ wrapper.vm.refresh();
await nextTick();
@@ -138,8 +137,10 @@ describe('MRWidgetFailedToMerge', () => {
createComponent();
});
- it('renders warning icon and disabled merge button', () => {
- expect(wrapper.find('.js-ci-status-icon-warning')).not.toBeNull();
+ it('renders failed icon', () => {
+ createComponent({}, mount);
+
+ expect(wrapper.find('[data-testid="status-failed-icon"]').exists()).toBe(true);
});
it('renders given error', () => {
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 07fc0be9e51..48b86d879ad 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
@@ -58,7 +58,7 @@ const createTestMr = (customConfig) => {
mergeImmediatelyDocsPath: 'path/to/merge/immediately/docs',
transitionStateMachine: (transition) => eventHub.$emit('StateMachineValueChanged', transition),
translateStateToMachine: () => this.transitionStateMachine(),
- state: 'open',
+ state: 'readyToMerge',
canMerge: true,
mergeable: true,
userPermissions: {
@@ -113,11 +113,6 @@ const createComponent = (customConfig = {}, createState = true) => {
GlSprintf,
},
apolloProvider: createMockApollo([[readyToMergeQuery, readyToMergeResponseSpy]]),
- provide: {
- glFeatures: {
- autoMergeLabelsMrWidget: false,
- },
- },
});
};
@@ -144,6 +139,7 @@ const findDeleteSourceBranchCheckbox = () =>
const triggerApprovalUpdated = () => eventHub.$emit('ApprovalUpdated');
const triggerEditCommitInput = () =>
wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+const findMergeHelperText = () => wrapper.find('[data-testid="auto-merge-helper-text"]');
describe('ReadyToMerge', () => {
beforeEach(() => {
@@ -185,47 +181,22 @@ describe('ReadyToMerge', () => {
expect(wrapper.vm.status).toEqual('failed');
});
});
-
- describe('status icon', () => {
- it('defaults to tick icon', () => {
- createComponent({ mr: { mergeable: true } });
-
- expect(wrapper.vm.iconClass).toEqual('success');
- });
-
- it('shows tick for success status', () => {
- createComponent({ mr: { pipeline: { status: 'SUCCESS' }, mergeable: true } });
-
- expect(wrapper.vm.iconClass).toEqual('success');
- });
-
- it('shows tick for pending status', () => {
- createComponent({ mr: { pipeline: { active: true }, mergeable: true } });
-
- expect(wrapper.vm.iconClass).toEqual('success');
- });
- });
});
describe('merge button text', () => {
it('should return "Merge" when no auto merge strategies are available', () => {
- createComponent({ mr: { availableAutoMergeStrategies: [] } });
-
- expect(findMergeButton().text()).toBe('Merge');
- });
-
- it('should return "Merge when pipeline succeeds" when the MWPS auto merge strategy is available', () => {
createComponent({
- mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY },
+ mr: { availableAutoMergeStrategies: [] },
});
- expect(findMergeButton().text()).toBe('Merge when pipeline succeeds');
+ expect(findMergeButton().text()).toBe('Merge');
});
- it('should return Merge when pipeline succeeds', () => {
+ it('should return Set to auto-merge in the button and Merge when pipeline succeeds in the helper text', () => {
createComponent({ mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY } });
- expect(findMergeButton().text()).toBe('Merge when pipeline succeeds');
+ expect(findMergeButton().text()).toBe('Set to auto-merge');
+ expect(findMergeHelperText().text()).toBe('Merge when pipeline succeeds');
});
});
@@ -258,10 +229,10 @@ describe('ReadyToMerge', () => {
expect(findMergeButton().props('disabled')).toBe(true);
});
- it('should be disabled if merge is not allowed', () => {
- createComponent({ mr: { preventMerge: true } });
+ it('should not exist if merge is not allowed', () => {
+ createComponent({ mr: { state: 'checking' } });
- expect(findMergeButton().props('disabled')).toBe(true);
+ expect(findMergeButton().exists()).toBe(false);
});
it('should be disabled when making request', async () => {
@@ -321,7 +292,7 @@ describe('ReadyToMerge', () => {
describe('Merge Button Variant', () => {
it('defaults to confirm class', () => {
createComponent({
- mr: { availableAutoMergeStrategies: [], mergeable: true },
+ mr: { availableAutoMergeStrategies: [] },
});
expect(findMergeButton().attributes('variant')).toBe('confirm');
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
index 296d7924243..02d17b8dfd2 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
+++ b/spec/frontend/vue_merge_request_widget/components/widget/__snapshots__/dynamic_content_spec.js.snap
@@ -16,14 +16,16 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render
</div>
<div class=\\"gl-display-flex gl-align-items-baseline\\">
<status-icon-stub level=\\"2\\" name=\\"MyWidget\\" iconname=\\"success\\"></status-icon-stub>
- <div class=\\"gl-display-flex gl-flex-direction-column\\">
- <div>
- <p class=\\"gl-mb-0\\">Main text for the row</p>
- <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
- <!---->
- <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\">
- Badge is optional. Text to be displayed inside badge
- </gl-badge-stub>
+ <div class=\\"gl-w-full gl-display-flex\\">
+ <div class=\\"gl-display-flex gl-flex-grow-1\\">
+ <div class=\\"gl-display-flex gl-flex-grow-1 gl-flex-direction-column\\">
+ <p class=\\"gl-mb-0 gl-mr-1\\">Main text for the row</p>
+ <gl-link-stub href=\\"https://gitlab.com\\">Optional link to display after text</gl-link-stub>
+ <!---->
+ <gl-badge-stub size=\\"md\\" variant=\\"info\\" iconsize=\\"md\\">
+ Badge is optional. Text to be displayed inside badge
+ </gl-badge-stub>
+ </div>
<actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
<p class=\\"gl-m-0 gl-font-sm\\">Optional: Smaller sub-text to be displayed below the main text</p>
</div>
@@ -40,12 +42,14 @@ exports[`~/vue_merge_request_widget/components/widget/dynamic_content.vue render
</div>
<div class=\\"gl-display-flex gl-align-items-baseline\\">
<!---->
- <div class=\\"gl-display-flex gl-flex-direction-column\\">
- <div>
- <p class=\\"gl-mb-0\\">This is recursive. It will be listed in level 3.</p>
- <!---->
- <!---->
- <!---->
+ <div class=\\"gl-w-full gl-display-flex\\">
+ <div class=\\"gl-display-flex gl-flex-grow-1\\">
+ <div class=\\"gl-display-flex gl-flex-grow-1 gl-flex-direction-column\\">
+ <p class=\\"gl-mb-0 gl-mr-1\\">This is recursive. It will be listed in level 3.</p>
+ <!---->
+ <!---->
+ <!---->
+ </div>
<actions-stub widget=\\"MyWidget\\" tertiarybuttons=\\"\\" class=\\"gl-ml-auto gl-pl-3\\"></actions-stub>
<!---->
</div>
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 4972c522733..9343a3a5e90 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
@@ -9,6 +9,7 @@ import ActionButtons from '~/vue_merge_request_widget/components/widget/action_b
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
import * as logger from '~/lib/logger';
+import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/vue_merge_request_widget/components/extensions/telemetry', () => ({
@@ -29,7 +30,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
const findHelpPopover = () => wrapper.findComponent(HelpPopover);
const findDynamicScroller = () => wrapper.findByTestId('dynamic-content-scroller');
- const createComponent = ({ propsData, slots, mountFn = shallowMountExtended } = {}) => {
+ const createComponent = async ({ propsData, slots, mountFn = shallowMountExtended } = {}) => {
wrapper = mountFn(Widget, {
propsData: {
isCollapsible: false,
@@ -49,6 +50,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
ContentRow: WidgetContentRow,
},
});
+
+ await axios.waitForAll();
};
describe('on mount', () => {
@@ -105,9 +108,9 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
},
});
- expect(wrapper.text()).not.toContain('Loading');
- await nextTick();
expect(wrapper.text()).toContain('Loading');
+ await axios.waitForAll();
+ expect(wrapper.text()).not.toContain('Loading');
});
it('validates widget name', () => {
@@ -185,10 +188,10 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
describe('content', () => {
- it('displays summary property when summary slot is not provided', () => {
- createComponent({
+ it('displays summary property when summary slot is not provided', async () => {
+ await createComponent({
propsData: {
- summary: 'Hello world',
+ summary: { title: 'Hello world' },
},
});
@@ -256,8 +259,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
describe('handle collapse toggle', () => {
- it('displays the toggle button correctly', () => {
- createComponent({
+ it('displays the toggle button correctly', async () => {
+ await createComponent({
propsData: {
isCollapsible: true,
},
@@ -271,7 +274,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
it('does not display the content slot until toggle is clicked', async () => {
- createComponent({
+ await createComponent({
propsData: {
isCollapsible: true,
},
@@ -286,8 +289,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
expect(findExpandedSection().text()).toBe('More complex content');
});
- it('emits a toggle even when button is toggled', () => {
- createComponent({
+ it('emits a toggle even when button is toggled', async () => {
+ await createComponent({
propsData: {
isCollapsible: true,
},
@@ -301,8 +304,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
expect(wrapper.emitted('toggle')).toEqual([[{ expanded: true }]]);
});
- it('does not display the toggle button if isCollapsible is false', () => {
- createComponent({
+ it('does not display the toggle button if isCollapsible is false', async () => {
+ await createComponent({
propsData: {
isCollapsible: false,
},
@@ -326,7 +329,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
const fetchExpandedData = jest.fn().mockResolvedValue(mockDataExpanded);
- createComponent({
+ await createComponent({
propsData: {
isCollapsible: true,
fetchCollapsedData: () => Promise.resolve(mockDataCollapsed),
@@ -358,7 +361,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('allows refetching when fetch expanded data returns an error', async () => {
const fetchExpandedData = jest.fn().mockRejectedValue({ error: true });
- createComponent({
+ await createComponent({
propsData: {
isCollapsible: true,
fetchExpandedData,
@@ -385,7 +388,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('resets the error message when another request is fetched', async () => {
const fetchExpandedData = jest.fn().mockRejectedValue({ error: true });
- createComponent({
+ await createComponent({
propsData: {
isCollapsible: true,
fetchExpandedData,
@@ -465,8 +468,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
},
];
- beforeEach(() => {
- createComponent({
+ beforeEach(async () => {
+ await createComponent({
mountFn: mountExtended,
propsData: {
isCollapsible: true,
diff --git a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
index 5baed8ff211..6aa12c37374 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
@@ -5,9 +5,7 @@ import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
-import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
-import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
-import terraformExtension from '~/vue_merge_request_widget/extensions/terraform';
+import terraformExtension from '~/vue_merge_request_widget/extensions/terraform/index.vue';
import {
plans,
validPlanWithName,
@@ -25,22 +23,20 @@ describe('Terraform extension', () => {
const endpoint = '/path/to/terraform/report.json';
const findListItem = (at) => wrapper.findAllByTestId('extension-list-item').at(at);
-
- registerExtension(terraformExtension);
+ const findActionButton = (at) => wrapper.findAllByTestId('extension-actions-button').at(at);
const mockPollingApi = (response, body, header) => {
mock.onGet(endpoint).reply(response, body, header);
};
const createComponent = () => {
- wrapper = mountExtended(extensionsContainer, {
+ wrapper = mountExtended(terraformExtension, {
propsData: {
mr: {
terraformReportsPath: endpoint,
},
},
});
- return axios.waitForAll();
};
beforeEach(() => {
@@ -54,24 +50,27 @@ describe('Terraform extension', () => {
describe('summary', () => {
describe('while loading', () => {
const loadingText = 'Loading Terraform reports...';
+
it('should render loading text', async () => {
mockPollingApi(HTTP_STATUS_OK, plans, {});
createComponent();
expect(wrapper.text()).toContain(loadingText);
+
await waitForPromises();
expect(wrapper.text()).not.toContain(loadingText);
});
});
describe('when the fetching fails', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockPollingApi(HTTP_STATUS_INTERNAL_SERVER_ERROR, null, {});
- return createComponent();
+ createComponent();
+ await axios.waitForAll();
});
- it('should generate one invalid plan and render correct summary text', () => {
- expect(wrapper.text()).toContain('1 Terraform report failed to generate');
+ it('should show the error text', () => {
+ expect(wrapper.text()).toContain('Failed to load Terraform reports');
});
});
@@ -82,9 +81,10 @@ describe('Terraform extension', () => {
${'2 valid reports'} | ${{ 0: validPlanWithName, 1: validPlanWithName }} | ${'2 Terraform reports were generated in your pipelines'} | ${''}
${'1 valid and 2 invalid reports'} | ${{ 0: validPlanWithName, 1: invalidPlanWithName, 2: invalidPlanWithName }} | ${'Terraform report was generated in your pipelines'} | ${'2 Terraform reports failed to generate'}
`('and received $responseType', ({ response, summaryTitle, summarySubtitle }) => {
- beforeEach(() => {
+ beforeEach(async () => {
mockPollingApi(HTTP_STATUS_OK, response, {});
- return createComponent();
+ createComponent();
+ await axios.waitForAll();
});
it(`should render correct summary text`, () => {
@@ -101,7 +101,8 @@ describe('Terraform extension', () => {
describe('expanded data', () => {
beforeEach(async () => {
mockPollingApi(HTTP_STATUS_OK, plans, {});
- await createComponent();
+ createComponent();
+ await axios.waitForAll();
wrapper.findByTestId('toggle-button').trigger('click');
});
@@ -136,7 +137,7 @@ describe('Terraform extension', () => {
api.trackRedisHllUserEvent.mockClear();
api.trackRedisCounterEvent.mockClear();
- findListItem(0).find('[data-testid="extension-actions-button"]').trigger('click');
+ findActionButton(0).trigger('click');
expect(api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
@@ -161,10 +162,10 @@ describe('Terraform extension', () => {
});
describe('successful poll', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockPollingApi(HTTP_STATUS_OK, plans, {});
-
- return createComponent();
+ createComponent();
+ await axios.waitForAll();
});
it('does not make additional requests after poll is successful', () => {
@@ -173,13 +174,14 @@ describe('Terraform extension', () => {
});
describe('polling fails', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockPollingApi(HTTP_STATUS_INTERNAL_SERVER_ERROR, null, {});
- return createComponent();
+ createComponent();
+ await axios.waitForAll();
});
- it('generates one broken plan', () => {
- expect(wrapper.text()).toContain('1 Terraform report failed to generate');
+ it('renders the error text', () => {
+ expect(wrapper.text()).toContain('Failed to load Terraform reports');
});
it('does not make additional requests after poll is unsuccessful', () => {
diff --git a/spec/frontend/vue_merge_request_widget/mock_data.js b/spec/frontend/vue_merge_request_widget/mock_data.js
index 47143bb2bb8..9da687c0ff8 100644
--- a/spec/frontend/vue_merge_request_widget/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/mock_data.js
@@ -188,7 +188,11 @@ export default {
coverage: '92.16',
path: '/root/acets-app/pipelines/172',
details: {
- artifacts,
+ artifacts: artifacts.map(({ text, url, ...rest }) => ({
+ name: text,
+ path: url,
+ ...rest,
+ })),
status: {
icon: 'status_success',
favicon: 'favicon_status_success',
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 0533471bece..ecb5a8448f9 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -1,5 +1,4 @@
import { GlBadge, GlLink, GlIcon, GlButton, GlDropdown } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -10,6 +9,7 @@ import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_stat
import readyToMergeResponse from 'test_fixtures/graphql/merge_requests/states/ready_to_merge.query.graphql.json';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK, HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
@@ -28,6 +28,8 @@ import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import Preparing from '~/vue_merge_request_widget/components/states/mr_widget_preparing.vue';
import WidgetContainer from '~/vue_merge_request_widget/components/widget/app.vue';
+import WidgetSuggestPipeline from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue';
+import MrWidgetAlertMessage from '~/vue_merge_request_widget/components/mr_widget_alert_message.vue';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
import getStateSubscription from '~/vue_merge_request_widget/queries/get_state.subscription.graphql';
@@ -76,6 +78,9 @@ describe('MrWidgetOptions', () => {
const COLLABORATION_MESSAGE = 'Members who can merge are allowed to add commits';
const findApprovalsWidget = () => wrapper.findComponent(Approvals);
const findPreparingWidget = () => wrapper.findComponent(Preparing);
+ const findMergedPipelineContainer = () => wrapper.findByTestId('merged-pipeline-container');
+ const findPipelineContainer = () => wrapper.findByTestId('pipeline-container');
+ const findAlertMessage = () => wrapper.findComponent(MrWidgetAlertMessage);
beforeEach(() => {
gl.mrWidgetData = { ...mockData };
@@ -95,7 +100,12 @@ describe('MrWidgetOptions', () => {
gl.mrWidgetData = {};
});
- const createComponent = (mrData = mockData, options = {}, data = {}, fullMount = true) => {
+ const createComponent = ({
+ mrData = mockData,
+ options = {},
+ data = {},
+ mountFn = shallowMountExtended,
+ } = {}) => {
const mockedApprovalsSubscription = createMockApolloSubscription();
queryResponse = {
data: {
@@ -114,7 +124,6 @@ describe('MrWidgetOptions', () => {
stateQueryHandler = jest.fn().mockResolvedValue(queryResponse);
stateSubscription = createMockApolloSubscription();
- const mounting = fullMount ? mount : shallowMount;
const queryHandlers = [
[approvalsQuery, jest.fn().mockResolvedValue(approvedByCurrentUser)],
[getStateQuery, stateQueryHandler],
@@ -143,7 +152,7 @@ describe('MrWidgetOptions', () => {
apolloProvider.defaultClient.setRequestHandler(query, stream);
});
- wrapper = mounting(MrWidgetOptions, {
+ wrapper = mountFn(MrWidgetOptions, {
propsData: {
mrData: { ...mrData },
},
@@ -165,8 +174,7 @@ describe('MrWidgetOptions', () => {
wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]');
const findExtensionLink = (linkHref) =>
wrapper.find(`[data-testid="widget-extension"] [href="${linkHref}"]`);
- const findSuggestPipeline = () => wrapper.find('[data-testid="mr-suggest-pipeline"]');
- const findSuggestPipelineButton = () => findSuggestPipeline().find('button');
+ const findSuggestPipeline = () => wrapper.findComponent(WidgetSuggestPipeline);
const findWidgetContainer = () => wrapper.findComponent(WidgetContainer);
describe('default', () => {
@@ -175,7 +183,7 @@ describe('MrWidgetOptions', () => {
return createComponent();
});
- // https://gitlab.com/gitlab-org/gitlab/-/issues/385238
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/385238
// eslint-disable-next-line jest/no-disabled-tests
describe.skip('data', () => {
it('should instantiate Store and Service', () => {
@@ -186,6 +194,7 @@ describe('MrWidgetOptions', () => {
describe('computed', () => {
describe('componentName', () => {
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/409365
// eslint-disable-next-line jest/no-disabled-tests
it.skip.each`
${'merged'} | ${'mr-widget-merged'}
@@ -206,60 +215,18 @@ describe('MrWidgetOptions', () => {
});
});
- describe('shouldRenderPipelines', () => {
- it('should return true when hasCI is true', () => {
+ describe('MrWidgetPipelineContainer', () => {
+ it('should return true when hasCI is true', async () => {
wrapper.vm.mr.hasCI = true;
-
- expect(wrapper.vm.shouldRenderPipelines).toBe(true);
+ await nextTick();
+ expect(findPipelineContainer().exists()).toBe(true);
});
- it('should return false when hasCI is false', () => {
+ it('should return false when hasCI is false', async () => {
wrapper.vm.mr.hasCI = false;
+ await nextTick();
- expect(wrapper.vm.shouldRenderPipelines).toBe(false);
- });
- });
-
- describe('shouldRenderSourceBranchRemovalStatus', () => {
- beforeEach(() => {
- wrapper.vm.mr.state = 'readyToMerge';
- });
-
- it('should return true when cannot remove source branch and branch will be removed', () => {
- wrapper.vm.mr.canRemoveSourceBranch = false;
- wrapper.vm.mr.shouldRemoveSourceBranch = true;
-
- expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(true);
- });
-
- it('should return false when can remove source branch and branch will be removed', () => {
- wrapper.vm.mr.canRemoveSourceBranch = true;
- wrapper.vm.mr.shouldRemoveSourceBranch = true;
-
- expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
- });
-
- it('should return false when cannot remove source branch and branch will not be removed', () => {
- wrapper.vm.mr.canRemoveSourceBranch = false;
- wrapper.vm.mr.shouldRemoveSourceBranch = false;
-
- expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
- });
-
- it('should return false when in merged state', () => {
- wrapper.vm.mr.canRemoveSourceBranch = false;
- wrapper.vm.mr.shouldRemoveSourceBranch = true;
- wrapper.vm.mr.state = 'merged';
-
- expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
- });
-
- it('should return false when in nothing to merge state', () => {
- wrapper.vm.mr.canRemoveSourceBranch = false;
- wrapper.vm.mr.shouldRemoveSourceBranch = true;
- wrapper.vm.mr.state = 'nothingToMerge';
-
- expect(wrapper.vm.shouldRenderSourceBranchRemovalStatus).toEqual(false);
+ expect(findPipelineContainer().exists()).toBe(false);
});
});
@@ -320,7 +287,7 @@ describe('MrWidgetOptions', () => {
});
it('should be false', () => {
- expect(wrapper.vm.showMergePipelineForkWarning).toEqual(false);
+ expect(findAlertMessage().exists()).toBe(false);
});
});
@@ -333,7 +300,7 @@ describe('MrWidgetOptions', () => {
});
it('should be false', () => {
- expect(wrapper.vm.showMergePipelineForkWarning).toEqual(false);
+ expect(findAlertMessage().exists()).toBe(false);
});
});
@@ -346,22 +313,30 @@ describe('MrWidgetOptions', () => {
});
it('should be true', () => {
- expect(wrapper.vm.showMergePipelineForkWarning).toEqual(true);
+ expect(findAlertMessage().exists()).toBe(true);
});
});
});
describe('formattedHumanAccess', () => {
- it('when user is a tool admin but not a member of project', () => {
+ it('when user is a tool admin but not a member of project', async () => {
wrapper.vm.mr.humanAccess = null;
+ wrapper.vm.mr.mergeRequestAddCiConfigPath = 'test';
+ wrapper.vm.mr.hasCI = false;
+ wrapper.vm.mr.isDismissedSuggestPipeline = false;
+ await nextTick();
- expect(wrapper.vm.formattedHumanAccess).toEqual('');
+ expect(findSuggestPipeline().props('humanAccess')).toBe('');
});
- it('when user a member of the project', () => {
+ it('when user a member of the project', async () => {
wrapper.vm.mr.humanAccess = 'Owner';
+ wrapper.vm.mr.mergeRequestAddCiConfigPath = 'test';
+ wrapper.vm.mr.hasCI = false;
+ wrapper.vm.mr.isDismissedSuggestPipeline = false;
+ await nextTick();
- expect(wrapper.vm.formattedHumanAccess).toEqual('owner');
+ expect(findSuggestPipeline().props('humanAccess')).toBe('owner');
});
});
});
@@ -570,10 +545,10 @@ describe('MrWidgetOptions', () => {
beforeEach(() => {
wrapper.destroy();
- return createComponent(
- mockData,
- {},
- {
+ return createComponent({
+ mrData: mockData,
+ options: {},
+ data: {
pollInterval: interval,
startingPollInterval: interval,
mr: {
@@ -584,8 +559,7 @@ describe('MrWidgetOptions', () => {
checkStatus: mockCheckStatus,
},
},
- false,
- );
+ });
});
describe('normal polling behavior', () => {
@@ -653,7 +627,7 @@ describe('MrWidgetOptions', () => {
environment_available: true,
};
- beforeEach(() => {
+ it('renders multiple deployments', async () => {
wrapper.vm.mr.deployments.push(
{
...deploymentMockData,
@@ -663,19 +637,10 @@ describe('MrWidgetOptions', () => {
id: deploymentMockData.id + 1,
},
);
-
- return nextTick();
- });
-
- it('renders multiple deployments', () => {
- expect(wrapper.findAll('.deploy-heading').length).toBe(2);
- });
-
- it('renders dropdpown with multiple file changes', () => {
- expect(
- wrapper.find('.js-mr-wigdet-deployment-dropdown').findAll('.js-filtered-dropdown-result')
- .length,
- ).toEqual(changes.length);
+ await nextTick();
+ expect(findPipelineContainer().props('isPostMerge')).toBe(false);
+ expect(findPipelineContainer().props('mr').deployments).toHaveLength(2);
+ expect(findPipelineContainer().props('mr').postMergeDeployments).toHaveLength(0);
});
});
@@ -793,7 +758,7 @@ describe('MrWidgetOptions', () => {
});
it('renders pipeline block', () => {
- expect(wrapper.find('.js-post-merge-pipeline').exists()).toBe(true);
+ expect(findMergedPipelineContainer().exists()).toBe(true);
});
describe('with post merge deployments', () => {
@@ -833,7 +798,7 @@ describe('MrWidgetOptions', () => {
});
it('renders post deployment information', () => {
- expect(wrapper.find('.js-post-deployment').exists()).toBe(true);
+ expect(findMergedPipelineContainer().exists()).toBe(true);
});
});
});
@@ -846,7 +811,7 @@ describe('MrWidgetOptions', () => {
});
it('does not render pipeline block', () => {
- expect(wrapper.find('.js-post-merge-pipeline').exists()).toBe(false);
+ expect(findMergedPipelineContainer().exists()).toBe(false);
});
});
@@ -858,11 +823,7 @@ describe('MrWidgetOptions', () => {
});
it('does not render pipeline block', () => {
- expect(wrapper.find('.js-post-merge-pipeline').exists()).toBe(false);
- });
-
- it('does not render post deployment information', () => {
- expect(wrapper.find('.js-post-deployment').exists()).toBe(false);
+ expect(findMergedPipelineContainer().exists()).toBe(false);
});
});
});
@@ -880,7 +841,6 @@ describe('MrWidgetOptions', () => {
describe('given feature flag is enabled', () => {
beforeEach(async () => {
await createComponent();
-
wrapper.vm.mr.hasCI = false;
});
@@ -901,7 +861,7 @@ describe('MrWidgetOptions', () => {
});
it('should allow dismiss of the suggest pipeline message', async () => {
- await findSuggestPipelineButton().trigger('click');
+ await findSuggestPipeline().vm.$emit('dismiss');
expect(findSuggestPipeline().exists()).toBe(false);
});
@@ -915,7 +875,7 @@ describe('MrWidgetOptions', () => {
${'merged'} | ${true} | ${'shows'}
${'open'} | ${true} | ${'shows'}
`('$showText merge error when state is $state', async ({ state, show }) => {
- createComponent({ ...mockData, state, mergeError: 'Error!' });
+ createComponent({ mrData: { ...mockData, state, mergeError: 'Error!' } });
await waitForPromises();
@@ -927,7 +887,7 @@ describe('MrWidgetOptions', () => {
beforeEach(() => {
registerExtension(workingExtension());
- createComponent();
+ createComponent({ mountFn: mountExtended });
});
afterEach(() => {
@@ -987,7 +947,7 @@ describe('MrWidgetOptions', () => {
it('shows collapse button', async () => {
registerExtension(workingExtension(true));
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(findExtensionToggleButton().exists()).toBe(true);
});
@@ -1026,7 +986,7 @@ describe('MrWidgetOptions', () => {
]),
);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(findWidgetTestExtension().html()).toContain(
'Multi polling test extension reports: parsed, count: 2',
);
@@ -1048,7 +1008,7 @@ describe('MrWidgetOptions', () => {
]),
);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(findWidgetTestExtension().html()).toContain('Test extension loading...');
});
});
@@ -1057,7 +1017,7 @@ describe('MrWidgetOptions', () => {
it('does not make additional requests after poll is successful', async () => {
registerExtension(pollingExtension);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(pollRequest).toHaveBeenCalledTimes(1);
});
@@ -1067,7 +1027,7 @@ describe('MrWidgetOptions', () => {
it('sets data when polling is complete', async () => {
registerExtension(pollingFullDataExtension);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
api.trackRedisHllUserEvent.mockClear();
api.trackRedisCounterEvent.mockClear();
@@ -1095,14 +1055,14 @@ describe('MrWidgetOptions', () => {
describe('error', () => {
it('does not make additional requests after poll has failed', async () => {
registerExtension(pollingErrorExtension);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(pollRequest).toHaveBeenCalledTimes(1);
});
it('captures sentry error and displays error when poll has failed', async () => {
registerExtension(pollingErrorExtension);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(Sentry.captureException).toHaveBeenCalled();
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('Fetch error'));
@@ -1118,7 +1078,7 @@ describe('MrWidgetOptions', () => {
it('handles collapsed data fetch errors', async () => {
registerExtension(collapsedDataErrorExtension);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(
wrapper.find('[data-testid="widget-extension"] [data-testid="toggle-button"]').exists(),
@@ -1130,7 +1090,7 @@ describe('MrWidgetOptions', () => {
it('handles full data fetch errors', async () => {
registerExtension(fullDataErrorExtension);
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(wrapper.findComponent(StatusIcon).props('iconName')).not.toBe('error');
wrapper
@@ -1153,7 +1113,7 @@ describe('MrWidgetOptions', () => {
it('triggers view events when mounted', () => {
registerExtension(workingExtension());
- createComponent();
+ createComponent({ mountFn: mountExtended });
expect(api.trackRedisHllUserEvent).toHaveBeenCalledTimes(1);
expect(api.trackRedisHllUserEvent).toHaveBeenCalledWith(
@@ -1168,7 +1128,7 @@ describe('MrWidgetOptions', () => {
describe('expand button', () => {
it('triggers expand events when clicked', async () => {
registerExtension(workingExtension());
- createComponent();
+ createComponent({ mountFn: mountExtended });
await waitForPromises();
@@ -1197,7 +1157,7 @@ describe('MrWidgetOptions', () => {
it('triggers the "full report clicked" events when the appropriate button is clicked', () => {
registerExtension(fullReportExtension);
- createComponent();
+ createComponent({ mountFn: mountExtended });
api.trackRedisHllUserEvent.mockClear();
api.trackRedisCounterEvent.mockClear();
@@ -1221,7 +1181,7 @@ describe('MrWidgetOptions', () => {
it("doesn't emit any telemetry events", async () => {
registerExtension(noTelemetryExtension);
- createComponent();
+ createComponent({ mountFn: mountExtended });
await waitForPromises();
@@ -1249,7 +1209,7 @@ describe('MrWidgetOptions', () => {
});
it('does not render the Preparing state component by default', async () => {
- await createComponent();
+ await createComponent({ mountFn: mountExtended });
expect(findApprovalsWidget().exists()).toBe(true);
expect(findPreparingWidget().exists()).toBe(false);
@@ -1257,9 +1217,11 @@ describe('MrWidgetOptions', () => {
it('renders the Preparing state component when the MR state is initially "preparing"', async () => {
await createComponent({
- ...mockData,
- state: 'opened',
- detailedMergeStatus: 'PREPARING',
+ mrData: {
+ ...mockData,
+ state: 'opened',
+ detailedMergeStatus: 'PREPARING',
+ },
});
expect(findApprovalsWidget().exists()).toBe(false);
@@ -1272,31 +1234,29 @@ describe('MrWidgetOptions', () => {
});
it("shows the Preparing widget when the MR reports it's not ready yet", async () => {
- await createComponent(
- {
+ await createComponent({
+ mrData: {
...mockData,
state: 'opened',
detailedMergeStatus: 'PREPARING',
},
- {},
- {},
- false,
- );
+ options: {},
+ data: {},
+ });
expect(wrapper.html()).toContain('mr-widget-preparing-stub');
});
it('removes the Preparing widget when the MR indicates it has been prepared', async () => {
- await createComponent(
- {
+ await createComponent({
+ mrData: {
...mockData,
state: 'opened',
detailedMergeStatus: 'PREPARING',
},
- {},
- {},
- false,
- );
+ options: {},
+ data: {},
+ });
expect(wrapper.html()).toContain('mr-widget-preparing-stub');
diff --git a/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
index 217103ab25c..cfd0d5bcf89 100644
--- a/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
@@ -1,5 +1,7 @@
import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
import SidebarTodo from '~/vue_shared/alert_details/components/sidebar/sidebar_todo.vue';
import createAlertTodoMutation from '~/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql';
@@ -9,41 +11,39 @@ const mockAlert = mockAlerts[0];
describe('Alert Details Sidebar To Do', () => {
let wrapper;
+ let requestHandler;
- function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) {
+ const defaultHandler = {
+ createAlertTodo: jest.fn().mockResolvedValue({}),
+ markAsDone: jest.fn().mockResolvedValue({}),
+ };
+
+ const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+
+ requestHandler = handler;
+
+ return createMockApollo([
+ [todoMarkDoneMutation, handler.markAsDone],
+ [createAlertTodoMutation, handler.createAlertTodo],
+ ]);
+ };
+
+ function mountComponent({ data, sidebarCollapsed = true, handler = defaultHandler } = {}) {
wrapper = mount(SidebarTodo, {
+ apolloProvider: createMockApolloProvider(handler),
propsData: {
alert: { ...mockAlert },
...data,
sidebarCollapsed,
projectPath: 'projectPath',
},
- mocks: {
- $apollo: {
- mutate: jest.fn(),
- queries: {
- alert: {
- loading,
- },
- },
- },
- },
- stubs,
});
}
const findToDoButton = () => wrapper.find('[data-testid="alert-todo-button"]');
describe('updating the alert to do', () => {
- const mockUpdatedMutationResult = {
- data: {
- updateAlertTodo: {
- errors: [],
- alert: {},
- },
- },
- };
-
describe('adding a todo', () => {
beforeEach(() => {
mountComponent({
@@ -60,18 +60,15 @@ describe('Alert Details Sidebar To Do', () => {
});
it('calls `$apollo.mutate` with `createAlertTodoMutation` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
-
findToDoButton().trigger('click');
await nextTick();
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: createAlertTodoMutation,
- variables: {
+ expect(requestHandler.createAlertTodo).toHaveBeenCalledWith(
+ expect.objectContaining({
iid: '1527542',
projectPath: 'projectPath',
- },
- });
+ }),
+ );
});
});
@@ -91,17 +88,11 @@ describe('Alert Details Sidebar To Do', () => {
});
it('calls `$apollo.mutate` with `todoMarkDoneMutation` mutation and variables containing `id`', async () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
-
findToDoButton().trigger('click');
await nextTick();
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: todoMarkDoneMutation,
- update: expect.anything(),
- variables: {
- id: '1234',
- },
+ expect(requestHandler.markAsDone).toHaveBeenCalledWith({
+ id: '1234',
});
});
});
diff --git a/spec/frontend/vue_shared/alert_details/alert_status_spec.js b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
index 98cb2f5cb0b..90d29f0bfd4 100644
--- a/spec/frontend/vue_shared/alert_details/alert_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
@@ -1,7 +1,9 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
import updateAlertStatusMutation from '~/graphql_shared//mutations/alert_status_update.mutation.graphql';
import Tracking from '~/tracking';
import AlertManagementStatus from '~/vue_shared/alert_details/components/alert_status.vue';
@@ -11,6 +13,27 @@ const mockAlert = mockAlerts[0];
describe('AlertManagementStatus', () => {
let wrapper;
+ let requestHandler;
+
+ const iid = '1527542';
+ const mockUpdatedMutationResult = ({ errors = [], nodes = [] } = {}) =>
+ jest.fn().mockResolvedValue({
+ data: {
+ updateAlertStatus: {
+ errors,
+ alert: {
+ id: '1',
+ iid,
+ status: 'acknowledged',
+ endedAt: 'endedAt',
+ notes: {
+ nodes,
+ },
+ },
+ },
+ },
+ });
+
const findStatusDropdown = () => wrapper.findComponent(GlDropdown);
const findFirstStatusOption = () => findStatusDropdown().findComponent(GlDropdownItem);
const findAllStatusOptions = () => findStatusDropdown().findAllComponents(GlDropdownItem);
@@ -22,8 +45,20 @@ describe('AlertManagementStatus', () => {
return waitForPromises();
};
- function mountComponent({ props = {}, provide = {}, loading = false, stubs = {} } = {}) {
+ const createMockApolloProvider = (handler) => {
+ Vue.use(VueApollo);
+ requestHandler = handler;
+
+ return createMockApollo([[updateAlertStatusMutation, handler]]);
+ };
+
+ function mountComponent({
+ props = {},
+ provide = {},
+ handler = mockUpdatedMutationResult(),
+ } = {}) {
wrapper = shallowMountExtended(AlertManagementStatus, {
+ apolloProvider: createMockApolloProvider(handler),
propsData: {
alert: { ...mockAlert },
projectPath: 'gitlab-org/gitlab',
@@ -31,17 +66,6 @@ describe('AlertManagementStatus', () => {
...props,
},
provide,
- mocks: {
- $apollo: {
- mutate: jest.fn(),
- queries: {
- alert: {
- loading,
- },
- },
- },
- },
- stubs,
});
}
@@ -63,43 +87,32 @@ describe('AlertManagementStatus', () => {
});
describe('updating the alert status', () => {
- const iid = '1527542';
- const mockUpdatedMutationResult = {
- data: {
- updateAlertStatus: {
- errors: [],
- alert: {
- iid,
- status: 'acknowledged',
- },
- },
- },
- };
-
beforeEach(() => {
- mountComponent({});
+ mountComponent();
});
- it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
+ it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', async () => {
findFirstStatusOption().vm.$emit('click');
+ await waitForPromises();
- expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
- mutation: updateAlertStatusMutation,
- variables: {
- iid,
- status: 'TRIGGERED',
- projectPath: 'gitlab-org/gitlab',
- },
+ expect(requestHandler).toHaveBeenCalledWith({
+ iid,
+ status: 'TRIGGERED',
+ projectPath: 'gitlab-org/gitlab',
});
});
describe('when a request fails', () => {
- beforeEach(() => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
+ beforeEach(async () => {
+ mountComponent({
+ handler: mockUpdatedMutationResult({ errors: ['<span data-testid="htmlError" />'] }),
+ });
+ await waitForPromises();
});
it('emits an error', async () => {
+ mountComponent({ handler: jest.fn().mockRejectedValue({}) });
+ await waitForPromises();
await selectFirstStatusOption();
expect(wrapper.emitted('alert-error')[0]).toEqual([
@@ -116,7 +129,6 @@ describe('AlertManagementStatus', () => {
it('emits an error when triggered a second time', async () => {
await selectFirstStatusOption();
- await nextTick();
await selectFirstStatusOption();
// Should emit two errors [0,1]
expect(wrapper.emitted('alert-error').length > 1).toBe(true);
@@ -124,19 +136,9 @@ describe('AlertManagementStatus', () => {
});
it('shows an error when response includes HTML errors', async () => {
- const mockUpdatedMutationErrorResult = {
- data: {
- updateAlertStatus: {
- errors: ['<span data-testid="htmlError" />'],
- alert: {
- iid,
- status: 'acknowledged',
- },
- },
- },
- };
-
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationErrorResult);
+ mountComponent({
+ handler: mockUpdatedMutationResult({ errors: ['<span data-testid="htmlError" />'] }),
+ });
await selectFirstStatusOption();
@@ -160,7 +162,7 @@ describe('AlertManagementStatus', () => {
mountComponent({
props: { alert: { ...mockAlert, status }, statuses: { [status]: translatedStatus } },
});
- expect(findAllStatusOptions().length).toBe(1);
+ expect(findAllStatusOptions()).toHaveLength(1);
expect(findFirstStatusOption().text()).toBe(translatedStatus);
});
});
@@ -173,10 +175,10 @@ describe('AlertManagementStatus', () => {
it('should not track alert status updates when the tracking options do not exist', async () => {
mountComponent({});
Tracking.event.mockClear();
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
+
findFirstStatusOption().vm.$emit('click');
- await nextTick();
+ await waitForPromises();
expect(Tracking.event).not.toHaveBeenCalled();
});
@@ -187,12 +189,14 @@ describe('AlertManagementStatus', () => {
action: 'update_alert_status',
label: 'Status',
};
- mountComponent({ provide: { trackAlertStatusUpdateOptions } });
+ mountComponent({
+ provide: { trackAlertStatusUpdateOptions },
+ handler: mockUpdatedMutationResult({ nodes: mockAlerts }),
+ });
Tracking.event.mockClear();
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
findFirstStatusOption().vm.$emit('click');
- await nextTick();
+ await waitForPromises();
const status = findFirstStatusOption().text();
const { category, action, label } = trackAlertStatusUpdateOptions;
diff --git a/spec/frontend/vue_shared/components/actions_button_spec.js b/spec/frontend/vue_shared/components/actions_button_spec.js
index e7663e2adb2..9f9a27c6997 100644
--- a/spec/frontend/vue_shared/components/actions_button_spec.js
+++ b/spec/frontend/vue_shared/components/actions_button_spec.js
@@ -31,12 +31,13 @@ const TEST_ACTION_2 = {
describe('vue_shared/components/actions_button', () => {
let wrapper;
- function createComponent(props) {
+ function createComponent({ props = {}, slots = {} } = {}) {
wrapper = shallowMountExtended(ActionsButton, {
propsData: { actions: [TEST_ACTION, TEST_ACTION_2], toggleText: 'Edit', ...props },
stubs: {
GlDisclosureDropdownItem,
},
+ slots,
});
}
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
@@ -47,11 +48,29 @@ describe('vue_shared/components/actions_button', () => {
expect(findDropdown().props().toggleText).toBe('Edit');
});
+ it('dropdown has a fluid width', () => {
+ createComponent();
+
+ expect(findDropdown().props().fluidWidth).toBe(true);
+ });
+
+ it('provides a default slot', () => {
+ const slotContent = 'default text';
+
+ createComponent({
+ slots: {
+ default: slotContent,
+ },
+ });
+
+ expect(findDropdown().text()).toContain(slotContent);
+ });
+
it('allows customizing variant and category', () => {
const variant = 'confirm';
const category = 'secondary';
- createComponent({ variant, category });
+ createComponent({ props: { variant, category } });
expect(findDropdown().props()).toMatchObject({ category, variant });
});
@@ -88,4 +107,13 @@ describe('vue_shared/components/actions_button', () => {
});
});
});
+
+ it.each(['shown', 'hidden'])(
+ 'bubbles up %s event from the disclosure dropdown component',
+ (event) => {
+ createComponent();
+ findDropdown().vm.$emit(event);
+ expect(wrapper.emitted(event)).toHaveLength(1);
+ },
+ );
});
diff --git a/spec/frontend/vue_shared/components/awards_list_spec.js b/spec/frontend/vue_shared/components/awards_list_spec.js
index da5516f8db1..6c28347503c 100644
--- a/spec/frontend/vue_shared/components/awards_list_spec.js
+++ b/spec/frontend/vue_shared/components/awards_list_spec.js
@@ -74,6 +74,7 @@ describe('vue_shared/components/awards_list', () => {
return {
classes: x.classes(),
title: x.attributes('title'),
+ emojiName: x.attributes('data-emoji-name'),
html: x.find('[data-testid="award-html"]').html(),
count: Number(x.find('.js-counter').text()),
};
@@ -96,48 +97,56 @@ describe('vue_shared/components/awards_list', () => {
count: 3,
html: matchingEmojiTag(EMOJI_THUMBSUP),
title: `Ada, Leonardo, and Marie reacted with :${EMOJI_THUMBSUP}:`,
+ emojiName: EMOJI_THUMBSUP,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 3,
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
title: `You, Ada, and Marie reacted with :${EMOJI_THUMBSDOWN}:`,
+ emojiName: EMOJI_THUMBSDOWN,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_100),
title: `Ada reacted with :${EMOJI_100}:`,
+ emojiName: EMOJI_100,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 2,
html: matchingEmojiTag(EMOJI_SMILE),
title: `Ada and Jane reacted with :${EMOJI_SMILE}:`,
+ emojiName: EMOJI_SMILE,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 4,
html: matchingEmojiTag(EMOJI_OK),
title: `You, Ada, Jane, and Leonardo reacted with :${EMOJI_OK}:`,
+ emojiName: EMOJI_OK,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 1,
html: matchingEmojiTag(EMOJI_CACTUS),
title: `You reacted with :${EMOJI_CACTUS}:`,
+ emojiName: EMOJI_CACTUS,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_A),
title: `Marie reacted with :${EMOJI_A}:`,
+ emojiName: EMOJI_A,
},
{
classes: [...REACTION_CONTROL_CLASSES, 'selected'],
count: 1,
html: matchingEmojiTag(EMOJI_B),
title: `You reacted with :${EMOJI_B}:`,
+ emojiName: EMOJI_B,
},
]);
});
@@ -226,12 +235,14 @@ describe('vue_shared/components/awards_list', () => {
count: 0,
html: matchingEmojiTag(EMOJI_THUMBSUP),
title: '',
+ emojiName: EMOJI_THUMBSUP,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 0,
html: matchingEmojiTag(EMOJI_THUMBSDOWN),
title: '',
+ emojiName: EMOJI_THUMBSDOWN,
},
// We expect the EMOJI_100 before the EMOJI_SMILE because it was given as a defaultAward
{
@@ -239,12 +250,14 @@ describe('vue_shared/components/awards_list', () => {
count: 1,
html: matchingEmojiTag(EMOJI_100),
title: `Marie reacted with :${EMOJI_100}:`,
+ emojiName: EMOJI_100,
},
{
classes: REACTION_CONTROL_CLASSES,
count: 1,
html: matchingEmojiTag(EMOJI_SMILE),
title: `Marie reacted with :${EMOJI_SMILE}:`,
+ emojiName: EMOJI_SMILE,
},
]);
});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
index 6acd1f51a86..1f3029435ee 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
@@ -1,3 +1,4 @@
+import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
import { handleBlobRichViewer } from '~/blob/viewer';
import RichViewer from '~/vue_shared/components/blob_viewers/rich_viewer.vue';
@@ -21,16 +22,24 @@ describe('Blob Rich Viewer component', () => {
}
beforeEach(() => {
+ const execImmediately = (callback) => callback();
+ jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
+
createComponent();
});
+ it('listens to requestIdleCallback', () => {
+ expect(window.requestIdleCallback).toHaveBeenCalled();
+ });
+
it('renders the passed content without transformations', () => {
expect(wrapper.html()).toContain(content);
});
- it('renders the richViewer if one is present', () => {
+ it('renders the richViewer if one is present', async () => {
const richViewer = '<div class="js-pdf-viewer"></div>';
createComponent('pdf', richViewer);
+ await nextTick();
expect(wrapper.html()).toContain(richViewer);
});
diff --git a/spec/frontend/vue_shared/components/ci_icon_spec.js b/spec/frontend/vue_shared/components/ci_icon_spec.js
index 31d63654168..c907b776b91 100644
--- a/spec/frontend/vue_shared/components/ci_icon_spec.js
+++ b/spec/frontend/vue_shared/components/ci_icon_spec.js
@@ -1,18 +1,23 @@
import { GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import ciIcon from '~/vue_shared/components/ci_icon.vue';
+import CiIcon from '~/vue_shared/components/ci_icon.vue';
describe('CI Icon component', () => {
let wrapper;
- const findIconWrapper = () => wrapper.find('[data-testid="ci-icon-wrapper"]');
+ const createComponent = (props) => {
+ wrapper = shallowMount(CiIcon, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
it('should render a span element with an svg', () => {
- wrapper = shallowMount(ciIcon, {
- propsData: {
- status: {
- icon: 'status_success',
- },
+ createComponent({
+ status: {
+ group: 'success',
+ icon: 'status_success',
},
});
@@ -20,49 +25,43 @@ describe('CI Icon component', () => {
expect(wrapper.findComponent(GlIcon).exists()).toBe(true);
});
- describe('active icons', () => {
- it.each`
- isActive | cssClass
- ${true} | ${'active'}
- ${false} | ${'active'}
- `('active should be $isActive', ({ isActive, cssClass }) => {
- wrapper = shallowMount(ciIcon, {
+ describe.each`
+ isActive
+ ${true}
+ ${false}
+ `('when isActive is $isActive', ({ isActive }) => {
+ it(`"active" class is ${isActive ? 'not ' : ''}added`, () => {
+ wrapper = shallowMount(CiIcon, {
propsData: {
status: {
+ group: 'success',
icon: 'status_success',
},
isActive,
},
});
- if (isActive) {
- expect(findIconWrapper().classes()).toContain(cssClass);
- } else {
- expect(findIconWrapper().classes()).not.toContain(cssClass);
- }
+ expect(wrapper.classes('active')).toBe(isActive);
});
});
- describe('interactive icons', () => {
- it.each`
- isInteractive | cssClass
- ${true} | ${'interactive'}
- ${false} | ${'interactive'}
- `('interactive should be $isInteractive', ({ isInteractive, cssClass }) => {
- wrapper = shallowMount(ciIcon, {
+ describe.each`
+ isInteractive
+ ${true}
+ ${false}
+ `('when isInteractive is $isInteractive', ({ isInteractive }) => {
+ it(`"interactive" class is ${isInteractive ? 'not ' : ''}added`, () => {
+ wrapper = shallowMount(CiIcon, {
propsData: {
status: {
+ group: 'success',
icon: 'status_success',
},
isInteractive,
},
});
- if (isInteractive) {
- expect(findIconWrapper().classes()).toContain(cssClass);
- } else {
- expect(findIconWrapper().classes()).not.toContain(cssClass);
- }
+ expect(wrapper.classes('interactive')).toBe(isInteractive);
});
});
@@ -79,7 +78,7 @@ describe('CI Icon component', () => {
${'status_canceled'} | ${'canceled'} | ${'ci-status-icon-canceled'}
${'status_manual'} | ${'manual'} | ${'ci-status-icon-manual'}
`('should render a $group status', ({ icon, group, cssClass }) => {
- wrapper = shallowMount(ciIcon, {
+ wrapper = shallowMount(CiIcon, {
propsData: {
status: {
icon,
diff --git a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
index 25283eb1211..5720f45f4dd 100644
--- a/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
+++ b/spec/frontend/vue_shared/components/code_block_highlighted_spec.js
@@ -58,4 +58,11 @@ describe('Code Block Highlighted', () => {
</code-block-stub>
`);
});
+
+ it('renders content as plain text language is not supported', () => {
+ const content = '<script>alert("xss")</script>';
+ createComponent({ code: content, language: 'foobar' });
+
+ expect(wrapper.text()).toContain(content);
+ });
});
diff --git a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
index d7f94c00d09..0b5c8d9afc3 100644
--- a/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
+++ b/spec/frontend/vue_shared/components/confirm_danger/confirm_danger_modal_spec.js
@@ -60,9 +60,7 @@ describe('Confirm Danger Modal', () => {
});
it('renders the correct confirmation phrase', () => {
- expect(findConfirmationPhrase().text()).toBe(
- `Please type ${phrase} to proceed or close this modal to cancel.`,
- );
+ expect(findConfirmationPhrase().text()).toBe(`Please type ${phrase} to proceed.`);
});
describe('without injected data', () => {
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 2a4037d76b7..40232eb367a 100644
--- a/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/diff_stats_dropdown_spec.js
@@ -8,6 +8,7 @@ import {
import fuzzaldrinPlus from 'fuzzaldrin-plus';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import DiffStatsDropdown, { i18n } from '~/vue_shared/components/diff_stats_dropdown.vue';
jest.mock('fuzzaldrin-plus', () => ({
@@ -38,6 +39,7 @@ const mockFiles = [
describe('Diff Stats Dropdown', () => {
let wrapper;
+ const focusInputMock = jest.fn();
const createComponent = ({ changed = 0, added = 0, deleted = 0, files = [] } = {}) => {
wrapper = shallowMountExtended(DiffStatsDropdown, {
@@ -50,6 +52,9 @@ describe('Diff Stats Dropdown', () => {
stubs: {
GlSprintf,
GlDropdown,
+ GlSearchBoxByType: stubComponent(GlSearchBoxByType, {
+ methods: { focusInput: focusInputMock },
+ }),
},
});
};
@@ -151,10 +156,8 @@ describe('Diff Stats Dropdown', () => {
});
it('should set the search input focus', () => {
- wrapper.vm.$refs.search.focusInput = jest.fn();
findChanged().vm.$emit('shown');
-
- expect(wrapper.vm.$refs.search.focusInput).toHaveBeenCalled();
+ expect(focusInputMock).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
index 6e2e854adae..36772ad03fe 100644
--- a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
@@ -125,7 +125,8 @@ describe('EntitySelect', () => {
it('emits `input` event with the select value', async () => {
createComponent();
await selectGroup();
- expect(wrapper.emitted('input')[0]).toEqual(['1']);
+
+ expect(wrapper.emitted('input')[0][0]).toMatchObject(itemMock);
});
it(`uses the selected group's name as the toggle text`, async () => {
@@ -153,14 +154,14 @@ describe('EntitySelect', () => {
expect(findListbox().props('toggleText')).toBe(defaultToggleText);
});
- it('emits `input` event with `null` on reset', async () => {
+ it('emits `input` event with an empty object on reset', async () => {
createComponent();
await selectGroup();
findListbox().vm.$emit('reset');
await nextTick();
- expect(wrapper.emitted('input')[2]).toEqual([null]);
+ expect(Object.keys(wrapper.emitted('input')[2][0]).length).toBe(0);
});
});
});
diff --git a/spec/frontend/vue_shared/components/entity_select/group_select_spec.js b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
index 83560e367ea..ae551116560 100644
--- a/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
@@ -39,6 +39,8 @@ describe('GroupSelect', () => {
const findEntitySelect = () => wrapper.findComponent(EntitySelect);
const findAlert = () => wrapper.findComponent(GlAlert);
+ const handleInput = jest.fn();
+
// Helpers
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(GroupSelect, {
@@ -52,6 +54,9 @@ describe('GroupSelect', () => {
GlAlert,
EntitySelect,
},
+ listeners: {
+ input: handleInput,
+ },
});
};
const openListbox = () => findListbox().vm.$emit('shown');
@@ -132,4 +137,11 @@ describe('GroupSelect', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(FETCH_GROUPS_ERROR);
});
+
+ it('forwards events to the parent scope via `v-on="$listeners"`', () => {
+ createComponent();
+ findEntitySelect().vm.$emit('input');
+
+ expect(handleInput).toHaveBeenCalledTimes(1);
+ });
});
diff --git a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
index 0a174c98efb..9113152c975 100644
--- a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
@@ -45,6 +45,8 @@ describe('ProjectSelect', () => {
const findEntitySelect = () => wrapper.findComponent(EntitySelect);
const findAlert = () => wrapper.findComponent(GlAlert);
+ const handleInput = jest.fn();
+
// Helpers
const createComponent = ({ props = {} } = {}) => {
wrapper = mountExtended(ProjectSelect, {
@@ -59,6 +61,9 @@ describe('ProjectSelect', () => {
GlAlert,
EntitySelect,
},
+ listeners: {
+ input: handleInput,
+ },
});
};
const openListbox = () => findListbox().vm.$emit('shown');
@@ -255,4 +260,11 @@ describe('ProjectSelect', () => {
expect(findAlert().exists()).toBe(true);
expect(findAlert().text()).toBe(FETCH_PROJECTS_ERROR);
});
+
+ it('forwards events to the parent scope via `v-on="$listeners"`', () => {
+ createComponent();
+ findEntitySelect().vm.$emit('input');
+
+ expect(handleInput).toHaveBeenCalledTimes(1);
+ });
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
index c0cb17f0d16..00a412d9de8 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js
@@ -125,46 +125,23 @@ describe('FilteredSearchBarRoot', () => {
});
describe('sortDirectionIcon', () => {
- it('returns string "sort-lowest" when `selectedSortDirection` is "ascending"', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- selectedSortDirection: SORT_DIRECTION.ascending,
- });
-
- expect(wrapper.vm.sortDirectionIcon).toBe('sort-lowest');
- });
-
- it('returns string "sort-highest" when `selectedSortDirection` is "descending"', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- selectedSortDirection: SORT_DIRECTION.descending,
+ it('renders `sort-highest` descending icon by default', () => {
+ expect(findGlButton().props('icon')).toBe('sort-highest');
+ expect(findGlButton().attributes()).toMatchObject({
+ 'aria-label': 'Sort direction: Descending',
+ title: 'Sort direction: Descending',
});
-
- expect(wrapper.vm.sortDirectionIcon).toBe('sort-highest');
});
- });
- describe('sortDirectionTooltip', () => {
- it('returns string "Sort direction: Ascending" when `selectedSortDirection` is "ascending"', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- selectedSortDirection: SORT_DIRECTION.ascending,
- });
-
- expect(wrapper.vm.sortDirectionTooltip).toBe('Sort direction: Ascending');
- });
+ it('renders `sort-lowest` ascending icon when the sort button is clicked', async () => {
+ findGlButton().vm.$emit('click');
+ await nextTick();
- it('returns string "Sort direction: Descending" when `selectedSortDirection` is "descending"', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- selectedSortDirection: SORT_DIRECTION.descending,
+ expect(findGlButton().props('icon')).toBe('sort-lowest');
+ expect(findGlButton().attributes()).toMatchObject({
+ 'aria-label': 'Sort direction: Ascending',
+ title: 'Sort direction: Ascending',
});
-
- expect(wrapper.vm.sortDirectionTooltip).toBe('Sort direction: Descending');
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
index fb8cea09a9b..d34d7ff48c2 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_contact_token_spec.js
@@ -39,7 +39,6 @@ describe('CrmContactToken', () => {
Vue.use(VueApollo);
let wrapper;
- let fakeApollo;
const getBaseToken = () => wrapper.findComponent(BaseToken);
@@ -58,9 +57,8 @@ describe('CrmContactToken', () => {
listeners = {},
queryHandler = searchGroupCrmContactsQueryHandler,
} = {}) => {
- fakeApollo = createMockApollo([[searchCrmContactsQuery, queryHandler]]);
-
wrapper = mount(CrmContactToken, {
+ apolloProvider: createMockApollo([[searchCrmContactsQuery, queryHandler]]),
propsData: {
config,
value,
@@ -75,14 +73,9 @@ describe('CrmContactToken', () => {
},
stubs,
listeners,
- apolloProvider: fakeApollo,
});
};
- afterEach(() => {
- fakeApollo = null;
- });
-
describe('methods', () => {
describe('fetchContacts', () => {
describe('for groups', () => {
@@ -160,9 +153,7 @@ describe('CrmContactToken', () => {
});
it('calls `createAlert` with alert error message when request fails', async () => {
- mountComponent();
-
- jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
getBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
@@ -173,12 +164,9 @@ describe('CrmContactToken', () => {
});
it('sets `loading` to false when request completes', async () => {
- mountComponent();
-
- jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
getBaseToken().vm.$emit('fetch-suggestions');
-
await waitForPromises();
expect(getBaseToken().props('suggestionsLoading')).toBe(false);
@@ -195,13 +183,7 @@ describe('CrmContactToken', () => {
value: { data: '1' },
});
- const baseTokenEl = wrapper.findComponent(BaseToken);
-
- expect(baseTokenEl.exists()).toBe(true);
- expect(baseTokenEl.props()).toMatchObject({
- suggestions: mockCrmContacts,
- getActiveTokenValue: wrapper.vm.getActiveContact,
- });
+ expect(getBaseToken().props('suggestions')).toEqual(mockCrmContacts);
});
it.each(mockCrmContacts)('renders token item when value is selected', (contact) => {
@@ -270,12 +252,9 @@ describe('CrmContactToken', () => {
it('emits listeners in the base-token', () => {
const mockInput = jest.fn();
- mountComponent({
- listeners: {
- input: mockInput,
- },
- });
- wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
+ mountComponent({ listeners: { input: mockInput } });
+
+ getBaseToken().vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
index 20369342220..17cf39e726c 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/crm_organization_token_spec.js
@@ -39,7 +39,6 @@ describe('CrmOrganizationToken', () => {
Vue.use(VueApollo);
let wrapper;
- let fakeApollo;
const getBaseToken = () => wrapper.findComponent(BaseToken);
@@ -58,8 +57,8 @@ describe('CrmOrganizationToken', () => {
listeners = {},
queryHandler = searchGroupCrmOrganizationsQueryHandler,
} = {}) => {
- fakeApollo = createMockApollo([[searchCrmOrganizationsQuery, queryHandler]]);
wrapper = mount(CrmOrganizationToken, {
+ apolloProvider: createMockApollo([[searchCrmOrganizationsQuery, queryHandler]]),
propsData: {
config,
value,
@@ -74,14 +73,9 @@ describe('CrmOrganizationToken', () => {
},
stubs,
listeners,
- apolloProvider: fakeApollo,
});
};
- afterEach(() => {
- fakeApollo = null;
- });
-
describe('methods', () => {
describe('fetchOrganizations', () => {
describe('for groups', () => {
@@ -159,9 +153,7 @@ describe('CrmOrganizationToken', () => {
});
it('calls `createAlert` when request fails', async () => {
- mountComponent();
-
- jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
getBaseToken().vm.$emit('fetch-suggestions');
await waitForPromises();
@@ -172,9 +164,7 @@ describe('CrmOrganizationToken', () => {
});
it('sets `loading` to false when request completes', async () => {
- mountComponent();
-
- jest.spyOn(wrapper.vm.$apollo, 'query').mockRejectedValue({});
+ mountComponent({ queryHandler: jest.fn().mockRejectedValue({}) });
getBaseToken().vm.$emit('fetch-suggestions');
@@ -194,13 +184,7 @@ describe('CrmOrganizationToken', () => {
value: { data: '1' },
});
- const baseTokenEl = wrapper.findComponent(BaseToken);
-
- expect(baseTokenEl.exists()).toBe(true);
- expect(baseTokenEl.props()).toMatchObject({
- suggestions: mockCrmOrganizations,
- getActiveTokenValue: wrapper.vm.getActiveOrganization,
- });
+ expect(getBaseToken().props('suggestions')).toEqual(mockCrmOrganizations);
});
it.each(mockCrmOrganizations)('renders token item when value is selected', (organization) => {
@@ -269,12 +253,9 @@ describe('CrmOrganizationToken', () => {
it('emits listeners in the base-token', () => {
const mockInput = jest.fn();
- mountComponent({
- listeners: {
- input: mockInput,
- },
- });
- wrapper.findComponent(BaseToken).vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
+ mountComponent({ listeners: { input: mockInput } });
+
+ getBaseToken().vm.$emit('input', [{ data: 'mockData', operator: '=' }]);
expect(mockInput).toHaveBeenLastCalledWith([{ data: 'mockData', operator: '=' }]);
});
diff --git a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
index 397fd270344..b782a2b19da 100644
--- a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
+++ b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
@@ -7,7 +7,7 @@ describe('ListboxInput', () => {
// Props
const label = 'label';
- const decription = 'decription';
+ const description = 'description';
const name = 'name';
const defaultToggleText = 'defaultToggleText';
const items = [
@@ -34,7 +34,7 @@ describe('ListboxInput', () => {
wrapper = shallowMount(ListboxInput, {
propsData: {
label,
- decription,
+ description,
name,
defaultToggleText,
items,
@@ -72,8 +72,8 @@ describe('ListboxInput', () => {
expect(findGlFormGroup().attributes('label')).toBe(label);
});
- it('passes the decription to the form group', () => {
- expect(findGlFormGroup().attributes('decription')).toBe(decription);
+ it('passes the description to the form group', () => {
+ expect(findGlFormGroup().attributes('description')).toBe(description);
});
it('sets the input name', () => {
@@ -89,6 +89,26 @@ describe('ListboxInput', () => {
});
});
+ describe('props', () => {
+ it.each([true, false])("passes %s to the listbox's fluidWidth prop", (fluidWidth) => {
+ createComponent({ fluidWidth });
+
+ expect(findGlListbox().props('fluidWidth')).toBe(fluidWidth);
+ });
+
+ it.each(['right', 'left'])("passes %s to the listbox's placement prop", (placement) => {
+ createComponent({ placement });
+
+ expect(findGlListbox().props('placement')).toBe(placement);
+ });
+
+ it.each([true, false])("passes %s to the listbox's block prop", (block) => {
+ createComponent({ block });
+
+ expect(findGlListbox().props('block')).toBe(block);
+ });
+ });
+
describe('toggle text', () => {
it('uses the default toggle text while no value is selected', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
index aea25abb324..2bef6dd15df 100644
--- a/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/comment_templates_dropdown_spec.js
@@ -4,13 +4,9 @@ import savedRepliesResponse from 'test_fixtures/graphql/comment_templates/saved_
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { updateText } from '~/lib/utils/text_markdown';
import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.vue';
import savedRepliesQuery from '~/vue_shared/components/markdown/saved_replies.query.graphql';
-jest.mock('~/lib/utils/text_markdown');
-
let wrapper;
let savedRepliesResp;
@@ -28,7 +24,6 @@ function createComponent(options = {}) {
const { mockApollo } = options;
return mountExtended(CommentTemplatesDropdown, {
- attachTo: '#root',
propsData: {
newCommentTemplatePath: '/new',
},
@@ -37,14 +32,6 @@ function createComponent(options = {}) {
}
describe('Comment templates dropdown', () => {
- beforeEach(() => {
- setHTMLFixture('<div class="md-area"><textarea></textarea><div id="root"></div></div>');
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
it('fetches data when dropdown gets opened', async () => {
const mockApollo = createMockApolloProvider(savedRepliesResponse);
wrapper = createComponent({ mockApollo });
@@ -56,7 +43,7 @@ describe('Comment templates dropdown', () => {
expect(savedRepliesResp).toHaveBeenCalled();
});
- it('adds content to textarea', async () => {
+ it('adds emits a select event on selecting a comment', async () => {
const mockApollo = createMockApolloProvider(savedRepliesResponse);
wrapper = createComponent({ mockApollo });
@@ -66,11 +53,6 @@ describe('Comment templates dropdown', () => {
wrapper.find('.gl-new-dropdown-item').trigger('click');
- expect(updateText).toHaveBeenCalledWith({
- textArea: document.querySelector('textarea'),
- tag: savedRepliesResponse.data.currentUser.savedReplies.nodes[0].content,
- cursorOffset: 0,
- wrap: false,
- });
+ expect(wrapper.emitted().select[0]).toEqual(['Saved Reply Content']);
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
index 693353ed604..712e78458c6 100644
--- a/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
@@ -1,25 +1,47 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { GlButton, GlLink, GlPopover } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
+import { counter } from '~/vue_shared/components/markdown/utils';
+import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import { stubComponent } from 'helpers/stub_component';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+
+jest.mock('~/vue_shared/components/markdown/utils', () => ({
+ counter: jest.fn().mockReturnValue(0),
+}));
describe('vue_shared/component/markdown/editor_mode_switcher', () => {
let wrapper;
+ useLocalStorageSpy();
- const createComponent = ({ value } = {}) => {
- wrapper = shallowMount(EditorModeSwitcher, {
+ const createComponent = ({
+ value,
+ userCalloutDismisserSlotProps = { dismiss: jest.fn() },
+ } = {}) => {
+ wrapper = mount(EditorModeSwitcher, {
propsData: {
value,
},
+ stubs: {
+ UserCalloutDismisser: stubComponent(UserCalloutDismisser, {
+ render() {
+ return this.$scopedSlots.default(userCalloutDismisserSlotProps);
+ },
+ }),
+ },
});
};
const findSwitcherButton = () => wrapper.findComponent(GlButton);
+ const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser);
+ const findCalloutPopover = () => wrapper.findComponent(GlPopover);
describe.each`
- modeText | value | buttonText
- ${'Rich text'} | ${'richText'} | ${'Switch to Markdown'}
- ${'Markdown'} | ${'markdown'} | ${'Switch to rich text'}
- `('when $modeText', ({ modeText, value, buttonText }) => {
+ value | buttonText
+ ${'richText'} | ${'Switch to plain text editing'}
+ ${'markdown'} | ${'Switch to rich text editing'}
+ `('when $value', ({ value, buttonText }) => {
beforeEach(() => {
createComponent({ value });
});
@@ -28,10 +50,66 @@ describe('vue_shared/component/markdown/editor_mode_switcher', () => {
expect(findSwitcherButton().text()).toEqual(buttonText);
});
- it('emits event on click', () => {
- findSwitcherButton(modeText).vm.$emit('click');
+ it('emits event on click', async () => {
+ await nextTick();
+ findSwitcherButton().vm.$emit('click');
+
+ expect(wrapper.emitted().switch).toEqual([[false]]);
+ });
+ });
+
+ describe('rich text editor callout', () => {
+ let dismiss;
+
+ beforeEach(() => {
+ dismiss = jest.fn();
+ createComponent({ value: 'markdown', userCalloutDismisserSlotProps: { dismiss } });
+ });
+
+ it('does not skip the user_callout_dismisser query', () => {
+ expect(findUserCalloutDismisser().props()).toMatchObject({
+ skipQuery: false,
+ featureName: 'rich_text_editor',
+ });
+ });
+
+ it('mounts new rich text editor popover', () => {
+ expect(findCalloutPopover().props()).toMatchObject({
+ showCloseButton: '',
+ triggers: 'manual',
+ target: 'switch-to-rich-text-editor',
+ });
+ });
+
+ it('dismisses the callout and emits "switch" event when popover close button is clicked', async () => {
+ await findCalloutPopover().findComponent(GlLink).vm.$emit('click');
+
+ expect(wrapper.emitted().switch).toEqual([[true]]);
+ expect(dismiss).toHaveBeenCalled();
+ });
+
+ it('dismisses the callout when action button is clicked', () => {
+ findSwitcherButton().vm.$emit('click');
+
+ expect(dismiss).toHaveBeenCalled();
+ });
+
+ it('does not show the callout if rich text is already enabled', async () => {
+ await wrapper.setProps({ value: 'richText' });
+
+ expect(findCalloutPopover().props()).toMatchObject({
+ show: false,
+ });
+ });
+
+ it('does not show the callout if already displayed once on the page', () => {
+ counter.mockReturnValue(1);
+
+ createComponent({ value: 'markdown' });
- expect(wrapper.emitted().input).toEqual([[]]);
+ expect(findCalloutPopover().props()).toMatchObject({
+ show: false,
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index b29f0d58d77..4ade8f28fd0 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -65,6 +65,16 @@ describe('Markdown field component', () => {
enablePreview,
restrictedToolBarItems,
showContentEditorSwitcher,
+ supportsQuickActions: true,
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ currentUser: {
+ loading: false,
+ },
+ },
+ },
},
},
);
@@ -206,12 +216,12 @@ describe('Markdown field component', () => {
expect(findMarkdownToolbar().props()).toEqual({
canAttachFile: true,
markdownDocsPath,
- quickActionsDocsPath: '',
showCommentToolBar: true,
+ showContentEditorSwitcher: false,
});
expect(findMarkdownHeader().props()).toMatchObject({
- showContentEditorSwitcher: false,
+ supportsQuickActions: true,
});
});
});
@@ -368,13 +378,13 @@ describe('Markdown field component', () => {
it('defaults to false', () => {
createSubject();
- expect(findMarkdownHeader().props('showContentEditorSwitcher')).toBe(false);
+ expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(false);
});
it('passes showContentEditorSwitcher', () => {
createSubject({ showContentEditorSwitcher: true });
- expect(findMarkdownHeader().props('showContentEditorSwitcher')).toBe(true);
+ expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/header_spec.js b/spec/frontend/vue_shared/components/markdown/header_spec.js
index 48fe5452e74..eb728879fb7 100644
--- a/spec/frontend/vue_shared/components/markdown/header_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/header_spec.js
@@ -1,22 +1,28 @@
import $ from 'jquery';
import { nextTick } from 'vue';
-import { GlToggle } from '@gitlab/ui';
+import { GlToggle, GlButton } from '@gitlab/ui';
import HeaderComponent from '~/vue_shared/components/markdown/header.vue';
+import CommentTemplatesDropdown from '~/vue_shared/components/markdown/comment_templates_dropdown.vue';
import ToolbarButton from '~/vue_shared/components/markdown/toolbar_button.vue';
import DrawioToolbarButton from '~/vue_shared/components/markdown/drawio_toolbar_button.vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
+import { updateText } from '~/lib/utils/text_markdown';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+
+jest.mock('~/lib/utils/text_markdown');
describe('Markdown field header component', () => {
let wrapper;
- const createWrapper = (props) => {
+ const createWrapper = ({ props = {}, provide = {}, attachTo = document.body } = {}) => {
wrapper = shallowMountExtended(HeaderComponent, {
+ attachTo,
propsData: {
previewMarkdown: false,
...props,
},
stubs: { GlToggle },
+ provide,
});
};
@@ -28,6 +34,7 @@ describe('Markdown field header component', () => {
.filter((button) => button.props(prop) === value)
.at(0);
const findDrawioToolbarButton = () => wrapper.findComponent(DrawioToolbarButton);
+ const findCommentTemplatesDropdown = () => wrapper.findComponent(CommentTemplatesDropdown);
beforeEach(() => {
window.gl = {
@@ -65,6 +72,39 @@ describe('Markdown field header component', () => {
});
});
+ it('renders correct title on non MacOS systems', () => {
+ window.gl = {
+ client: {
+ isMac: false,
+ },
+ };
+
+ createWrapper();
+
+ const buttons = [
+ 'Insert suggestion',
+ 'Add bold text (Ctrl+B)',
+ 'Add italic text (Ctrl+I)',
+ 'Add strikethrough text (Ctrl+Shift+X)',
+ 'Insert a quote',
+ 'Insert code',
+ 'Add a link (Ctrl+K)',
+ 'Add a bullet list',
+ 'Add a numbered list',
+ 'Add a checklist',
+ 'Indent line (Ctrl+])',
+ 'Outdent line (Ctrl+[)',
+ 'Add a collapsible section',
+ 'Add a table',
+ 'Go full screen',
+ ];
+ const elements = findToolbarButtons();
+
+ elements.wrappers.forEach((buttonEl, index) => {
+ expect(buttonEl.props('buttonTitle')).toBe(buttons[index]);
+ });
+ });
+
it('renders "Attach a file or image" button using gl-button', () => {
const button = wrapper.findByTestId('button-attach-file');
@@ -92,15 +132,16 @@ describe('Markdown field header component', () => {
});
it('shows markdown preview when previewMarkdown is true', () => {
- createWrapper({ previewMarkdown: true });
+ createWrapper({ props: { previewMarkdown: true } });
expect(findPreviewToggle().text()).toBe('Continue editing');
});
it('hides toolbar in preview mode', () => {
- createWrapper({ previewMarkdown: true });
+ createWrapper({ props: { previewMarkdown: true } });
- expect(findToolbar().classes().includes('gl-display-none!')).toBe(true);
+ // only one button is rendered in preview mode
+ expect(findToolbar().findAllComponents(GlButton)).toHaveLength(1);
});
it('emits toggle markdown event when clicking preview toggle', async () => {
@@ -150,7 +191,9 @@ describe('Markdown field header component', () => {
it('does not render suggestion button if `canSuggest` is set to false', () => {
createWrapper({
- canSuggest: false,
+ props: {
+ canSuggest: false,
+ },
});
expect(wrapper.find('.js-suggestion-btn').exists()).toBe(false);
@@ -158,7 +201,9 @@ describe('Markdown field header component', () => {
it('hides markdown preview when previewMarkdown property is false', () => {
createWrapper({
- enablePreview: false,
+ props: {
+ enablePreview: false,
+ },
});
expect(wrapper.findByTestId('preview-toggle').exists()).toBe(false);
@@ -173,7 +218,9 @@ describe('Markdown field header component', () => {
it('restricts items as per input', () => {
createWrapper({
- restrictedToolBarItems: ['quote'],
+ props: {
+ restrictedToolBarItems: ['quote'],
+ },
});
expect(findToolbarButtons().length).toBe(defaultCount - 1);
@@ -192,9 +239,11 @@ describe('Markdown field header component', () => {
beforeEach(() => {
createWrapper({
- drawioEnabled: true,
- uploadsPath,
- markdownPreviewPath,
+ props: {
+ drawioEnabled: true,
+ uploadsPath,
+ markdownPreviewPath,
+ },
});
});
@@ -206,17 +255,46 @@ describe('Markdown field header component', () => {
});
});
- describe('with content editor switcher', () => {
+ describe('when selecting a saved reply from the comment templates dropdown', () => {
beforeEach(() => {
+ setHTMLFixture('<div class="md-area"><textarea></textarea><div id="root"></div></div>');
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('updates the textarea with the saved comment', async () => {
createWrapper({
- showContentEditorSwitcher: true,
+ attachTo: '#root',
+ provide: {
+ newCommentTemplatePath: 'some/path',
+ glFeatures: {
+ savedReplies: true,
+ },
+ },
+ });
+
+ await findCommentTemplatesDropdown().vm.$emit('select', 'Some saved comment');
+
+ expect(updateText).toHaveBeenCalledWith({
+ textArea: document.querySelector('textarea'),
+ tag: 'Some saved comment',
+ cursorOffset: 0,
+ wrap: false,
});
});
- it('re-emits event from switcher', () => {
- wrapper.findComponent(EditorModeSwitcher).vm.$emit('input', 'richText');
+ it('does not show the saved replies button if newCommentTemplatePath is not defined', () => {
+ createWrapper({
+ provide: {
+ glFeatures: {
+ savedReplies: true,
+ },
+ },
+ });
- expect(wrapper.emitted('enableContentEditor')).toEqual([[]]);
+ expect(findCommentTemplatesDropdown().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index e54e261b8e4..31c0fa6f699 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -21,6 +21,7 @@ import waitForPromises from 'helpers/wait_for_promises';
jest.mock('~/emoji');
jest.mock('autosize');
+jest.mock('~/lib/graphql');
describe('vue_shared/component/markdown/markdown_editor', () => {
useLocalStorageSpy();
@@ -29,7 +30,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const value = 'test markdown';
const renderMarkdownPath = '/api/markdown';
const markdownDocsPath = '/help/markdown';
- const quickActionsDocsPath = '/help/quickactions';
const enableAutocomplete = true;
const enablePreview = false;
const formFieldId = 'markdown_field';
@@ -43,7 +43,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
value,
renderMarkdownPath,
markdownDocsPath,
- quickActionsDocsPath,
enableAutocomplete,
autocompleteDataSources,
enablePreview,
@@ -65,6 +64,15 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
BubbleMenu: stubComponent(BubbleMenu),
...stubs,
},
+ mocks: {
+ $apollo: {
+ queries: {
+ currentUser: {
+ loading: false,
+ },
+ },
+ },
+ },
});
};
@@ -110,7 +118,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findMarkdownField().props()).toMatchObject({
autocompleteDataSources,
markdownPreviewPath: renderMarkdownPath,
- quickActionsDocsPath,
+ supportsQuickActions: true,
canAttachFile: true,
enableAutocomplete,
textareaValue: value,
@@ -120,7 +128,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
});
- // quarantine flaky spec:https://gitlab.com/gitlab-org/gitlab/-/issues/412618
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/412618
// eslint-disable-next-line jest/no-disabled-tests
it.skip('passes render_quick_actions param to renderMarkdownPath if quick actions are enabled', async () => {
buildWrapper({ propsData: { supportsQuickActions: true } });
@@ -131,7 +139,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(mock.history.post[0].url).toContain(`render_quick_actions=true`);
});
- // quarantine flaky spec: https://gitlab.com/gitlab-org/gitlab/-/issues/411565
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/411565
// eslint-disable-next-line jest/no-disabled-tests
it.skip('does not pass render_quick_actions param to renderMarkdownPath if quick actions are disabled', async () => {
buildWrapper({ propsData: { supportsQuickActions: false } });
@@ -145,27 +153,31 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
it('enables content editor switcher when contentEditorEnabled prop is true', () => {
buildWrapper({ propsData: { enableContentEditor: true } });
- expect(findMarkdownField().text()).toContain('Switch to rich text');
+ expect(findMarkdownField().text()).toContain('Switch to rich text editing');
});
it('hides content editor switcher when contentEditorEnabled prop is false', () => {
buildWrapper({ propsData: { enableContentEditor: false } });
- expect(findMarkdownField().text()).not.toContain('Switch to rich text');
+ expect(findMarkdownField().text()).not.toContain('Switch to rich text editing');
});
it('passes down any additional props to markdown field component', () => {
- const propsData = {
+ const codeSuggestionsConfig = {
line: { text: 'hello world', richText: 'hello world' },
lines: [{ text: 'hello world', richText: 'hello world' }],
canSuggest: true,
};
buildWrapper({
- propsData: { ...propsData, myCustomProp: 'myCustomValue', 'data-testid': 'custom id' },
+ propsData: {
+ codeSuggestionsConfig,
+ myCustomProp: 'myCustomValue',
+ 'data-testid': 'custom id',
+ },
});
- expect(findMarkdownField().props()).toMatchObject(propsData);
+ expect(findMarkdownField().props()).toMatchObject(codeSuggestionsConfig);
expect(findMarkdownField().vm.$attrs).toMatchObject({
myCustomProp: 'myCustomValue',
@@ -201,7 +213,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findMarkdownField().find('textarea').attributes('disabled')).toBe(undefined);
});
- // quarantine flaky spec: https://gitlab.com/gitlab-org/gitlab/-/issues/404734
+ // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/404734
// eslint-disable-next-line jest/no-disabled-tests
it.skip('disables content editor when disabled prop is true', async () => {
buildWrapper({ propsData: { disabled: true } });
@@ -436,8 +448,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
describe('when contentEditor is disabled', () => {
it('resets the editingMode to markdownField', () => {
- localStorage.setItem('gl-markdown-editor-mode', 'contentEditor');
-
buildWrapper({ propsData: { autosaveKey: 'issue/1234', enableContentEditor: false } });
expect(wrapper.vm.editingMode).toBe(EDITING_MODE_MARKDOWN_FIELD);
diff --git a/spec/frontend/vue_shared/components/markdown/non_gfm_markdown_spec.js b/spec/frontend/vue_shared/components/markdown/non_gfm_markdown_spec.js
new file mode 100644
index 00000000000..cd73ef6892a
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/non_gfm_markdown_spec.js
@@ -0,0 +1,157 @@
+import { nextTick } from 'vue';
+import Markdown from '~/vue_shared/components/markdown/non_gfm_markdown.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CodeBlockHighlighted from '~/vue_shared/components/code_block_highlighted.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
+describe('NonGitlabMarkdown', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = shallowMountExtended(Markdown, {
+ propsData,
+ });
+ };
+
+ const codeBlockContent = 'stages:\n - build\n - test\n - deploy\n';
+ const codeBlockLanguage = 'yaml';
+ const nonCodeContent =
+ "Certainly! Here's an updated GitLab CI/CD configuration in YAML format that includes Kubernetes deployment:";
+ const testMarkdownWithCodeBlock = `${nonCodeContent}\n\n\`\`\`${codeBlockLanguage}\n${codeBlockContent}\n\`\`\`\n\nIn this updated configuration, we have added a \`deploy\` job that deploys the Python app to a Kubernetes cluster. The \`script\` section of the job includes commands to authenticate with GCP, set the project and zone, configure kubectl to use the GKE cluster, and deploy the application using a deployment.yaml file.\n\nNote that you will need to modify this configuration to fit your specific deployment needs, including replacing the placeholders (\`<PROJECT_ID>\`, \`<COMPUTE_ZONE>\`, \`<CLUSTER_NAME>\`, and \`<COMPUTE_REGION>\`) with your GCP and Kubernetes deployment information, and creating the deployment.yaml file with your Kubernetes deployment configuration.`;
+ const codeOnlyMarkdown = `\`\`\`${codeBlockLanguage}\n${codeBlockContent}\n\`\`\``;
+ const markdownWithMultipleCodeSnippets = `${testMarkdownWithCodeBlock}\n${testMarkdownWithCodeBlock}`;
+ const codeBlockNoLanguage = `
+ \`\`\`
+ const foo = 'bar';
+ \`\`\`
+ `;
+
+ const findCodeBlock = () => wrapper.findComponent(CodeBlockHighlighted);
+ const findCopyCodeButton = () => wrapper.findComponent(ModalCopyButton);
+ const findCodeBlockWrapper = () => wrapper.findByTestId('code-block-wrapper');
+ const findMarkdownBlock = () => wrapper.findByTestId('non-code-markdown');
+
+ describe('rendering markdown without code snippet', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { markdown: nonCodeContent } });
+ });
+ it('should render non-code content', () => {
+ const markdownBlock = findMarkdownBlock();
+ expect(markdownBlock.exists()).toBe(true);
+ expect(markdownBlock.text()).toBe(nonCodeContent);
+ });
+ it('should not render code block', () => {
+ const codeBlock = findCodeBlock();
+ expect(codeBlock.exists()).toBe(false);
+ });
+ });
+
+ describe('rendering code snippet without other markdown', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { markdown: codeOnlyMarkdown } });
+ });
+ it('should not render non-code content', () => {
+ const markdownBlock = findMarkdownBlock();
+ expect(markdownBlock.exists()).toBe(false);
+ });
+ it('should render code block', () => {
+ const codeBlock = findCodeBlock();
+ expect(codeBlock.exists()).toBe(true);
+ });
+ });
+
+ describe('rendering code snippet with no language specified', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { markdown: codeBlockNoLanguage } });
+ });
+
+ it('should render code block', () => {
+ const codeBlock = findCodeBlock();
+ expect(codeBlock.exists()).toBe(true);
+ expect(codeBlock.props('language')).toBe('text');
+ });
+ });
+
+ describe.each`
+ markdown | codeBlocksCount | markdownBlocksCount
+ ${testMarkdownWithCodeBlock} | ${1} | ${2}
+ ${markdownWithMultipleCodeSnippets} | ${2} | ${3}
+ ${codeOnlyMarkdown} | ${1} | ${0}
+ ${nonCodeContent} | ${0} | ${1}
+ `(
+ 'extracting tokens in markdownBlocks computed',
+ ({ markdown, codeBlocksCount, markdownBlocksCount }) => {
+ beforeEach(() => {
+ createComponent({ propsData: { markdown } });
+ });
+
+ it('should create correct number of tokens', () => {
+ const findAllCodeBlocks = () => wrapper.findAllByTestId('code-block-wrapper');
+ const findAllMarkdownBlocks = () => wrapper.findAllByTestId('non-code-markdown');
+
+ expect(findAllCodeBlocks()).toHaveLength(codeBlocksCount);
+ expect(findAllMarkdownBlocks()).toHaveLength(markdownBlocksCount);
+ });
+ },
+ );
+
+ describe('rendering markdown with multiple code snippets', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { markdown: markdownWithMultipleCodeSnippets } });
+ });
+
+ it('should render code block with correct props', () => {
+ const codeBlock = findCodeBlock();
+ expect(codeBlock.exists()).toBe(true);
+ expect(codeBlock.props()).toEqual(
+ expect.objectContaining({
+ language: codeBlockLanguage,
+ code: codeBlockContent,
+ }),
+ );
+ expect(wrapper.findAllComponents(CodeBlockHighlighted)).toHaveLength(2);
+ });
+
+ it('should not show copy code button', () => {
+ const copyCodeButton = findCopyCodeButton();
+ expect(copyCodeButton.exists()).toBe(false);
+ });
+
+ it('should render non-code content', () => {
+ const markdownBlock = findMarkdownBlock();
+ expect(markdownBlock.exists()).toBe(true);
+ expect(markdownBlock.text()).toContain(nonCodeContent);
+ });
+
+ describe('copy code button', () => {
+ beforeEach(() => {
+ const codeBlock = findCodeBlockWrapper();
+ codeBlock.trigger('mouseenter');
+ });
+
+ it('should render only one copy button per code block', () => {
+ const copyCodeButtons = wrapper.findAllComponents(ModalCopyButton);
+ expect(copyCodeButtons).toHaveLength(1);
+ });
+
+ it('should render code block button with correct props', () => {
+ const copyCodeButton = findCopyCodeButton();
+ expect(copyCodeButton.exists()).toBe(true);
+ expect(copyCodeButton.props()).toEqual(
+ expect.objectContaining({
+ text: codeBlockContent,
+ title: 'Copy code',
+ }),
+ );
+ });
+
+ it('should hide code block button on mouseleave', async () => {
+ const codeBlock = findCodeBlockWrapper();
+ codeBlock.trigger('mouseleave');
+ await nextTick();
+ const copyCodeButton = findCopyCodeButton();
+ expect(copyCodeButton.exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
index 2489421b697..5bf11ff2b26 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -1,18 +1,33 @@
import { mount } from '@vue/test-utils';
import Toolbar from '~/vue_shared/components/markdown/toolbar.vue';
+import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
+import { updateText } from '~/lib/utils/text_markdown';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+
+jest.mock('~/lib/utils/text_markdown');
describe('toolbar', () => {
let wrapper;
- const createMountedWrapper = (props = {}) => {
+ const createWrapper = (props = {}, attachTo = document.body) => {
wrapper = mount(Toolbar, {
+ attachTo,
propsData: { markdownDocsPath: '', ...props },
+ mocks: {
+ $apollo: {
+ queries: {
+ currentUser: {
+ loading: false,
+ },
+ },
+ },
+ },
});
};
describe('user can attach file', () => {
beforeEach(() => {
- createMountedWrapper();
+ createWrapper();
});
it('should render uploading-container', () => {
@@ -22,7 +37,7 @@ describe('toolbar', () => {
describe('user cannot attach file', () => {
beforeEach(() => {
- createMountedWrapper({ canAttachFile: false });
+ createWrapper({ canAttachFile: false });
});
it('should not render uploading-container', () => {
@@ -32,15 +47,63 @@ describe('toolbar', () => {
describe('comment tool bar settings', () => {
it('does not show comment tool bar div', () => {
- createMountedWrapper({ showCommentToolBar: false });
+ createWrapper({ showCommentToolBar: false });
expect(wrapper.find('.comment-toolbar').exists()).toBe(false);
});
it('shows comment tool bar by default', () => {
- createMountedWrapper();
+ createWrapper();
expect(wrapper.find('.comment-toolbar').exists()).toBe(true);
});
});
+
+ describe('with content editor switcher', () => {
+ beforeEach(() => {
+ setHTMLFixture(
+ '<div class="md-area"><textarea>some value</textarea><div id="root"></div></div>',
+ );
+ createWrapper(
+ {
+ showContentEditorSwitcher: true,
+ },
+ '#root',
+ );
+ });
+
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('re-emits event from switcher', () => {
+ wrapper.findComponent(EditorModeSwitcher).vm.$emit('switch');
+
+ expect(wrapper.emitted('enableContentEditor')).toEqual([[]]);
+ expect(updateText).not.toHaveBeenCalled();
+ });
+
+ it('does not insert a template text if textarea has some value', () => {
+ wrapper.findComponent(EditorModeSwitcher).vm.$emit('switch', true);
+
+ expect(updateText).not.toHaveBeenCalled();
+ });
+
+ it('inserts a "getting started with rich text" template when switched for the first time', () => {
+ document.querySelector('textarea').value = '';
+
+ wrapper.findComponent(EditorModeSwitcher).vm.$emit('switch', true);
+
+ expect(updateText).toHaveBeenCalledWith(
+ expect.objectContaining({
+ tag: `### Rich text editor
+
+Try out **styling** _your_ content right here or read the [direction](https://about.gitlab.com/direction/plan/knowledge/content_editor/).`,
+ textArea: document.querySelector('textarea'),
+ cursorOffset: 0,
+ wrap: false,
+ }),
+ );
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
index a116233a065..f04e1976a5f 100644
--- a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
@@ -38,7 +38,6 @@ describe('NewResourceDropdown component', () => {
};
const mountComponent = ({
- search = '',
query = searchUserProjectsWithIssuesEnabledQuery,
queryResponse = searchProjectsQueryResponse,
mountFn = shallowMount,
@@ -47,16 +46,14 @@ describe('NewResourceDropdown component', () => {
const requestHandlers = [[query, jest.fn().mockResolvedValue(queryResponse)]];
const apolloProvider = createMockApollo(requestHandlers);
- return mountFn(NewResourceDropdown, {
+ wrapper = mountFn(NewResourceDropdown, {
apolloProvider,
propsData,
- data() {
- return { search };
- },
});
};
const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findGlDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findInput = () => wrapper.findComponent(GlSearchBoxByType);
const showDropdown = async () => {
findDropdown().vm.$emit('shown');
@@ -70,13 +67,13 @@ describe('NewResourceDropdown component', () => {
});
it('renders a split dropdown', () => {
- wrapper = mountComponent();
+ mountComponent();
expect(findDropdown().props('split')).toBe(true);
});
it('renders a label for the dropdown toggle button', () => {
- wrapper = mountComponent();
+ mountComponent();
expect(findDropdown().attributes('toggle-text')).toBe(
NewResourceDropdown.i18n.toggleButtonLabel,
@@ -84,7 +81,7 @@ describe('NewResourceDropdown component', () => {
});
it('focuses on input when dropdown is shown', async () => {
- wrapper = mountComponent({ mountFn: mount });
+ mountComponent({ mountFn: mount });
const inputSpy = jest.spyOn(findInput().vm, 'focusInput');
@@ -99,7 +96,7 @@ describe('NewResourceDropdown component', () => {
${'within a group'} | ${withinGroupProps} | ${searchProjectsWithinGroupQuery} | ${searchProjectsWithinGroupQueryResponse} | ${emptySearchProjectsWithinGroupQueryResponse}
`('$description', ({ propsData, query, queryResponse, emptyResponse }) => {
it('renders projects options', async () => {
- wrapper = mountComponent({ mountFn: mount, query, queryResponse, propsData });
+ mountComponent({ mountFn: mount, query, queryResponse, propsData });
await showDropdown();
const listItems = wrapper.findAll('li');
@@ -110,14 +107,14 @@ describe('NewResourceDropdown component', () => {
});
it('renders `No matches found` when there are no matches', async () => {
- wrapper = mountComponent({
- search: 'no matches',
+ mountComponent({
query,
queryResponse: emptyResponse,
mountFn: mount,
propsData,
});
+ await findInput().vm.$emit('input', 'no matches');
await showDropdown();
expect(wrapper.find('li').text()).toBe(NewResourceDropdown.i18n.noMatchesFound);
@@ -133,7 +130,7 @@ describe('NewResourceDropdown component', () => {
({ resourceType, expectedDefaultLabel, expectedPath, expectedLabel }) => {
describe('when no project is selected', () => {
beforeEach(() => {
- wrapper = mountComponent({
+ mountComponent({
query,
queryResponse,
propsData: { ...propsData, resourceType },
@@ -151,7 +148,7 @@ describe('NewResourceDropdown component', () => {
describe('when a project is selected', () => {
beforeEach(async () => {
- wrapper = mountComponent({
+ mountComponent({
mountFn: mount,
query,
queryResponse,
@@ -159,7 +156,7 @@ describe('NewResourceDropdown component', () => {
});
await showDropdown();
- wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ findGlDropdownItem().vm.$emit('click', project1);
});
it('dropdown button is a link', () => {
@@ -178,12 +175,12 @@ describe('NewResourceDropdown component', () => {
describe('without localStorage', () => {
beforeEach(() => {
- wrapper = mountComponent({ mountFn: mount });
+ mountComponent({ mountFn: mount });
});
it('does not attempt to save the selected project to the localStorage', async () => {
await showDropdown();
- wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ findGlDropdownItem().vm.$emit('click', project1);
expect(localStorage.setItem).not.toHaveBeenCalled();
});
@@ -198,7 +195,7 @@ describe('NewResourceDropdown component', () => {
name: project1.name,
}),
);
- wrapper = mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
+ mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
await nextTick();
const dropdown = findDropdown();
@@ -216,7 +213,7 @@ describe('NewResourceDropdown component', () => {
name: project1.name,
}),
);
- wrapper = mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
+ mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
await nextTick();
const dropdown = findDropdown();
@@ -228,12 +225,12 @@ describe('NewResourceDropdown component', () => {
describe.each(RESOURCE_TYPES)('with resource type %s', (resourceType) => {
it('computes the local storage key without a group', async () => {
- wrapper = mountComponent({
+ mountComponent({
mountFn: mount,
propsData: { resourceType, withLocalStorage: true },
});
await showDropdown();
- wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ findGlDropdownItem().vm.$emit('click', project1);
await nextTick();
expect(localStorage.setItem).toHaveBeenLastCalledWith(
@@ -244,12 +241,12 @@ describe('NewResourceDropdown component', () => {
it('computes the local storage key with a group', async () => {
const groupId = '22';
- wrapper = mountComponent({
+ mountComponent({
mountFn: mount,
propsData: { groupId, resourceType, withLocalStorage: true },
});
await showDropdown();
- wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ findGlDropdownItem().vm.$emit('click', project1);
await nextTick();
expect(localStorage.setItem).toHaveBeenLastCalledWith(
diff --git a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
index a27877e7ba8..e5b641c61fd 100644
--- a/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
+++ b/spec/frontend/vue_shared/components/paginated_table_with_search_and_tabs/paginated_table_with_search_and_tabs_spec.js
@@ -300,6 +300,7 @@ describe('AlertManagementEmptyState', () => {
unique: true,
symbol: '@',
token: UserToken,
+ dataType: 'user',
operators: OPERATORS_IS,
fetchPath: '/link',
fetchUsers: expect.any(Function),
@@ -311,6 +312,7 @@ describe('AlertManagementEmptyState', () => {
unique: true,
symbol: '@',
token: UserToken,
+ dataType: 'user',
operators: OPERATORS_IS,
fetchPath: '/link',
fetchUsers: expect.any(Function),
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
index 3e4d5c558f6..0e387d1c139 100644
--- a/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_item_spec.js
@@ -38,6 +38,7 @@ describe('ProjectsListItem', () => {
const findProjectTopics = () => wrapper.findByTestId('project-topics');
const findPopover = () => findProjectTopics().findComponent(GlPopover);
const findProjectDescription = () => wrapper.findByTestId('project-description');
+ const findVisibilityIcon = () => findAvatarLabeled().findComponent(GlIcon);
it('renders project avatar', () => {
createComponent();
@@ -48,11 +49,11 @@ describe('ProjectsListItem', () => {
label: project.name,
labelLink: project.webUrl,
});
+
expect(avatarLabeled.attributes()).toMatchObject({
'entity-id': project.id.toString(),
'entity-name': project.name,
shape: 'rect',
- size: '48',
});
});
@@ -66,6 +67,19 @@ describe('ProjectsListItem', () => {
expect(tooltip.value).toBe(PROJECT_VISIBILITY_TYPE[VISIBILITY_LEVEL_PRIVATE_STRING]);
});
+ describe('when visibility is not provided', () => {
+ it('does not render visibility icon', () => {
+ const { visibility, ...projectWithoutVisibility } = project;
+ createComponent({
+ propsData: {
+ project: projectWithoutVisibility,
+ },
+ });
+
+ expect(findVisibilityIcon().exists()).toBe(false);
+ });
+ });
+
it('renders access role badge', () => {
createComponent();
@@ -113,6 +127,19 @@ describe('ProjectsListItem', () => {
expect(wrapper.findComponent(TimeAgoTooltip).props('time')).toBe(project.updatedAt);
});
+ describe('when updated at is not available', () => {
+ it('does not render updated at', () => {
+ const { updatedAt, ...projectWithoutUpdatedAt } = project;
+ createComponent({
+ propsData: {
+ project: projectWithoutUpdatedAt,
+ },
+ });
+
+ expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(false);
+ });
+ });
+
describe('when issues are enabled', () => {
it('renders issues count', () => {
createComponent();
@@ -263,4 +290,20 @@ describe('ProjectsListItem', () => {
expect(findProjectDescription().exists()).toBe(false);
});
});
+
+ describe('when `showProjectIcon` prop is `true`', () => {
+ it('shows project icon', () => {
+ createComponent({ propsData: { showProjectIcon: true } });
+
+ expect(wrapper.findByTestId('project-icon').exists()).toBe(true);
+ });
+ });
+
+ describe('when `showProjectIcon` prop is `false`', () => {
+ it('does not show project icon', () => {
+ createComponent();
+
+ expect(wrapper.findByTestId('project-icon').exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js b/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
index 9380e19c39e..a0adbb89894 100644
--- a/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
+++ b/spec/frontend/vue_shared/components/projects_list/projects_list_spec.js
@@ -28,6 +28,7 @@ describe('ProjectsList', () => {
expect(expectedProps).toEqual(
defaultPropsData.projects.map((project) => ({
project,
+ showProjectIcon: false,
})),
);
});
diff --git a/spec/frontend/vue_shared/components/registry/list_item_spec.js b/spec/frontend/vue_shared/components/registry/list_item_spec.js
index 298fa163d59..4a230f72f21 100644
--- a/spec/frontend/vue_shared/components/registry/list_item_spec.js
+++ b/spec/frontend/vue_shared/components/registry/list_item_spec.js
@@ -1,6 +1,7 @@
import { GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import component from '~/vue_shared/components/registry/list_item.vue';
describe('list item', () => {
@@ -27,6 +28,9 @@ describe('list item', () => {
'right-action': '<div data-testid="right-action" />',
...slots,
},
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
@@ -90,6 +94,48 @@ describe('list item', () => {
expect(findToggleDetailsButton().exists()).toBe(true);
});
+ describe('when visible', () => {
+ beforeEach(async () => {
+ mountComponent({}, { 'details-foo': '<span></span>' });
+ await nextTick();
+ });
+
+ it('has tooltip', () => {
+ const tooltip = getBinding(findToggleDetailsButton().element, 'gl-tooltip');
+
+ expect(tooltip).toBeDefined();
+ expect(findToggleDetailsButton().attributes('title')).toBe(
+ component.i18n.toggleDetailsLabel,
+ );
+ });
+
+ it('has correct attributes and props', () => {
+ expect(findToggleDetailsButton().props()).toMatchObject({
+ selected: false,
+ });
+
+ expect(findToggleDetailsButton().attributes()).toMatchObject({
+ title: component.i18n.toggleDetailsLabel,
+ 'aria-label': component.i18n.toggleDetailsLabel,
+ });
+ });
+
+ it('has correct attributes and props when clicked', async () => {
+ findToggleDetailsButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findToggleDetailsButton().props()).toMatchObject({
+ selected: true,
+ });
+
+ expect(findToggleDetailsButton().attributes()).toMatchObject({
+ title: component.i18n.toggleDetailsLabel,
+ 'aria-label': component.i18n.toggleDetailsLabel,
+ 'aria-expanded': 'true',
+ });
+ });
+ });
+
it('is hidden without details slot', () => {
mountComponent();
expect(findToggleDetailsButton().exists()).toBe(false);
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js
index 94823bb640b..b94d8c1de21 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import RunnerDockerInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue';
+import { DOCS_URL } from 'jh_else_ce/lib/utils/url_utility';
describe('RunnerDockerInstructions', () => {
let wrapper;
@@ -25,8 +26,6 @@ describe('RunnerDockerInstructions', () => {
});
it('renders link', () => {
- expect(findButton().attributes('href')).toBe(
- 'https://docs.gitlab.com/runner/install/docker.html',
- );
+ expect(findButton().attributes('href')).toBe(`${DOCS_URL}/runner/install/docker.html`);
});
});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js
index 9d6658e002c..f0b033a2ca2 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import RunnerKubernetesInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue';
+import { DOCS_URL } from 'jh_else_ce/lib/utils/url_utility';
describe('RunnerKubernetesInstructions', () => {
let wrapper;
@@ -25,8 +26,6 @@ describe('RunnerKubernetesInstructions', () => {
});
it('renders link', () => {
- expect(findButton().attributes('href')).toBe(
- 'https://docs.gitlab.com/runner/install/kubernetes.html',
- );
+ expect(findButton().attributes('href')).toBe(`${DOCS_URL}/runner/install/kubernetes.html`);
});
});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index 2eaf46e6209..e307e53147b 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -90,14 +90,6 @@ describe('RunnerInstructionsModal component', () => {
await waitForPromises();
});
- it('should not show alert', () => {
- expect(findAlert().exists()).toBe(false);
- });
-
- it('should not show deprecation alert', () => {
- expect(findAlert('warning').exists()).toBe(false);
- });
-
it('should contain a number of platforms buttons', () => {
expect(runnerPlatformsHandler).toHaveBeenCalledWith({});
@@ -112,19 +104,8 @@ describe('RunnerInstructionsModal component', () => {
expect(architectures).toEqual(mockPlatformList[0].architectures.nodes);
});
- describe.each`
- glFeatures | deprecationAlertExists
- ${{}} | ${false}
- ${{ createRunnerWorkflowForAdmin: true }} | ${true}
- ${{ createRunnerWorkflowForNamespace: true }} | ${true}
- `('with features $glFeatures', ({ glFeatures, deprecationAlertExists }) => {
- beforeEach(() => {
- createComponent({ provide: { glFeatures } });
- });
-
- it(`alert is ${deprecationAlertExists ? 'shown' : 'not shown'}`, () => {
- expect(findAlert('warning').exists()).toBe(deprecationAlertExists);
- });
+ it('alert is shown', () => {
+ expect(findAlert('warning').exists()).toBe(true);
});
describe('when the modal resizes', () => {
diff --git a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
deleted file mode 100644
index 66d27b5d605..00000000000
--- a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
+++ /dev/null
@@ -1,144 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}0 Critical%{criticalEnd} %{highStart}1 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 0, "high": 1, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = `
-<span>
- Security scanning detected
- <strong>
- 1
- </strong>
- potential vulnerability
- <span
- class="gl-font-sm"
- >
- <span>
- <span
- class="gl-pl-4"
- >
-
- 0 Critical
-
- </span>
- </span>
-
- <span>
- <strong
- class="gl-text-red-600 gl-px-2"
- >
-
- 1 High
-
- </strong>
- </span>
- and
- <span>
- <span
- class="gl-px-2"
- >
-
- 0 Others
-
- </span>
- </span>
- </span>
-</span>
-`;
-
-exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}0 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 0, "message": "Security scanning detected %{totalStart}1%{totalEnd} potential vulnerability", "other": 0, "status": "", "total": 1} interpolates correctly 1`] = `
-<span>
- Security scanning detected
- <strong>
- 1
- </strong>
- potential vulnerability
- <span
- class="gl-font-sm"
- >
- <span>
- <strong
- class="gl-text-red-800 gl-pl-4"
- >
-
- 1 Critical
-
- </strong>
- </span>
-
- <span>
- <span
- class="gl-px-2"
- >
-
- 0 High
-
- </span>
- </span>
- and
- <span>
- <span
- class="gl-px-2"
- >
-
- 0 Others
-
- </span>
- </span>
- </span>
-</span>
-`;
-
-exports[`SecuritySummary component given the message {"countMessage": "%{criticalStart}1 Critical%{criticalEnd} %{highStart}2 High%{highEnd} and %{otherStart}0 Others%{otherEnd}", "critical": 1, "high": 2, "message": "Security scanning detected %{totalStart}3%{totalEnd} potential vulnerabilities", "other": 0, "status": "", "total": 3} interpolates correctly 1`] = `
-<span>
- Security scanning detected
- <strong>
- 3
- </strong>
- potential vulnerabilities
- <span
- class="gl-font-sm"
- >
- <span>
- <strong
- class="gl-text-red-800 gl-pl-4"
- >
-
- 1 Critical
-
- </strong>
- </span>
-
- <span>
- <strong
- class="gl-text-red-600 gl-px-2"
- >
-
- 2 High
-
- </strong>
- </span>
- and
- <span>
- <span
- class="gl-px-2"
- >
-
- 0 Others
-
- </span>
- </span>
- </span>
-</span>
-`;
-
-exports[`SecuritySummary component given the message {"message": ""} interpolates correctly 1`] = `
-<span>
-
- <!---->
-</span>
-`;
-
-exports[`SecuritySummary component given the message {"message": "foo"} interpolates correctly 1`] = `
-<span>
- foo
- <!---->
-</span>
-`;
diff --git a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js b/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
deleted file mode 100644
index 6eebd129beb..00000000000
--- a/spec/frontend/vue_shared/components/security_reports/artifact_downloads/merge_request_artifact_download_spec.js
+++ /dev/null
@@ -1,104 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import {
- expectedDownloadDropdownPropsWithTitle,
- securityReportMergeRequestDownloadPathsQueryResponse,
-} from 'jest/vue_shared/security_reports/mock_data';
-import { createAlert } from '~/alert';
-import Component from '~/vue_shared/security_reports/components/artifact_downloads/merge_request_artifact_download.vue';
-import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
-import {
- REPORT_TYPE_SAST,
- REPORT_TYPE_SECRET_DETECTION,
-} from '~/vue_shared/security_reports/constants';
-import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
-
-jest.mock('~/alert');
-
-describe('Merge request artifact Download', () => {
- let wrapper;
-
- const defaultProps = {
- reportTypes: [REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION],
- targetProjectFullPath: '/path',
- mrIid: 123,
- };
-
- const createWrapper = ({ propsData, options }) => {
- wrapper = shallowMount(Component, {
- stubs: {
- SecurityReportDownloadDropdown,
- },
- propsData: {
- ...defaultProps,
- ...propsData,
- },
- ...options,
- });
- };
-
- const pendingHandler = () => new Promise(() => {});
- const successHandler = () =>
- Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryResponse });
- const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
- const createMockApolloProvider = (handler) => {
- Vue.use(VueApollo);
- const requestHandlers = [[securityReportMergeRequestDownloadPathsQuery, handler]];
-
- return createMockApollo(requestHandlers);
- };
-
- const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown);
-
- describe('given the query is loading', () => {
- beforeEach(() => {
- createWrapper({
- options: {
- apolloProvider: createMockApolloProvider(pendingHandler),
- },
- });
- });
-
- it('loading is true', () => {
- expect(findDownloadDropdown().props('loading')).toBe(true);
- });
- });
-
- describe('given the query loads successfully', () => {
- beforeEach(() => {
- createWrapper({
- options: {
- apolloProvider: createMockApolloProvider(successHandler),
- },
- });
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithTitle);
- });
- });
-
- describe('given the query fails', () => {
- beforeEach(() => {
- createWrapper({
- options: {
- apolloProvider: createMockApolloProvider(failureHandler),
- },
- });
- });
-
- it('calls createAlert correctly', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: Component.i18n.apiError,
- captureError: true,
- error: expect.any(Error),
- });
- });
-
- it('renders nothing', () => {
- expect(findDownloadDropdown().props('artifacts')).toEqual([]);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js b/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
deleted file mode 100644
index 2f6e633fb34..00000000000
--- a/spec/frontend/vue_shared/components/security_reports/help_icon_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { GlLink, GlPopover } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
-
-const helpPath = '/docs';
-const discoverProjectSecurityPath = '/discoverProjectSecurityPath';
-
-describe('HelpIcon component', () => {
- let wrapper;
-
- const createWrapper = (props) => {
- wrapper = shallowMount(HelpIcon, {
- propsData: {
- helpPath,
- ...props,
- },
- });
- };
-
- const findLink = () => wrapper.findComponent(GlLink);
- const findPopover = () => wrapper.findComponent(GlPopover);
- const findPopoverTarget = () => wrapper.findComponent({ ref: 'discoverProjectSecurity' });
-
- describe('given a help path only', () => {
- beforeEach(() => {
- createWrapper();
- });
-
- it('does not render a popover', () => {
- expect(findPopover().exists()).toBe(false);
- });
-
- it('renders a help link', () => {
- expect(findLink().attributes()).toMatchObject({
- href: helpPath,
- target: '_blank',
- });
- });
- });
-
- describe('given a help path and discover project security path', () => {
- beforeEach(() => {
- createWrapper({ discoverProjectSecurityPath });
- });
-
- it('renders a popover', () => {
- const popover = findPopover();
- expect(popover.props('target')()).toBe(findPopoverTarget().element);
- expect(popover.attributes()).toMatchObject({
- title: HelpIcon.i18n.upgradeToManageVulnerabilities,
- triggers: 'click blur',
- });
- expect(popover.text()).toContain(HelpIcon.i18n.upgradeToInteract);
- });
-
- it('renders a link to the discover path', () => {
- expect(findLink().attributes()).toMatchObject({
- href: discoverProjectSecurityPath,
- target: '_blank',
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js b/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js
deleted file mode 100644
index 61cdc329220..00000000000
--- a/spec/frontend/vue_shared/components/security_reports/security_summary_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import { GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import SecuritySummary from '~/vue_shared/security_reports/components/security_summary.vue';
-import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
-
-describe('SecuritySummary component', () => {
- let wrapper;
-
- const createWrapper = (message) => {
- wrapper = shallowMount(SecuritySummary, {
- propsData: { message },
- stubs: {
- GlSprintf,
- },
- });
- };
-
- describe.each([
- { message: '' },
- { message: 'foo' },
- groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 0, total: 1 }),
- groupedTextBuilder({ reportType: 'Security scanning', critical: 0, high: 1, total: 1 }),
- groupedTextBuilder({ reportType: 'Security scanning', critical: 1, high: 2, total: 3 }),
- ])('given the message %p', (message) => {
- beforeEach(() => {
- createWrapper(message);
- });
-
- it('interpolates correctly', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-});
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
index 919abc26e05..1154c930e5d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_new_spec.js
@@ -40,8 +40,6 @@ describe('Chunk component', () => {
describe('rendering', () => {
it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => {
- jest.clearAllMocks();
-
expect(window.requestIdleCallback).not.toHaveBeenCalled();
expect(findContent().text()).toBe(CHUNK_1.highlightedContent);
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
index d2dd4afe09e..49e3083f8ed 100644
--- a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
@@ -1,10 +1,11 @@
-import hljs from 'highlight.js';
+import hljs from 'highlight.js/lib/core';
import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import { highlight } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
import { LINES_PER_CHUNK, NEWLINE } from '~/vue_shared/components/source_viewer/constants';
-jest.mock('highlight.js', () => ({
+jest.mock('highlight.js/lib/core', () => ({
highlight: jest.fn().mockReturnValue({ value: 'highlighted content' }),
+ registerLanguage: jest.fn(),
}));
jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({
@@ -14,11 +15,15 @@ jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({
const fileType = 'text';
const rawContent = 'function test() { return true }; \n // newline';
const highlightedContent = 'highlighted content';
-const language = 'javascript';
+const language = 'json';
describe('Highlight utility', () => {
beforeEach(() => highlight(fileType, rawContent, language));
+ it('registers the language', () => {
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(language, expect.any(Function));
+ });
+
it('registers the plugins', () => {
expect(registerPlugins).toHaveBeenCalled();
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
index 9d2bf002d73..45fef09aa84 100644
--- a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_child_nodes_spec.js
@@ -8,14 +8,15 @@ describe('Highlight.js plugin for wrapping _emitter nodes', () => {
children: [
{ scope: 'string', children: ['Text 1'] },
{ scope: 'string', children: ['Text 2', { scope: 'comment', children: ['Text 3'] }] },
- { scope: undefined, sublanguage: true, children: ['Text 3 (sublanguage)'] },
- 'Text4\nText5',
+ { scope: undefined, sublanguage: true, children: ['Text 4 (sublanguage)'] },
+ { scope: undefined, sublanguage: undefined, children: ['Text 5'] },
+ 'Text6\nText7',
],
},
},
};
- const outputValue = `<span class="hljs-string">Text 1</span><span class="hljs-string"><span class="hljs-string">Text 2</span><span class="hljs-comment">Text 3</span></span><span class="">Text 3 (sublanguage)</span><span class="">Text4</span>\n<span class="">Text5</span>`;
+ const outputValue = `<span class="hljs-string">Text 1</span><span class="hljs-string"><span class="hljs-string">Text 2</span><span class="hljs-comment">Text 3</span></span><span class="">Text 4 (sublanguage)</span><span class="">Text 5</span><span class="">Text6</span>\n<span class="">Text7</span>`;
wrapChildNodes(hljsResultMock);
expect(hljsResultMock.value).toBe(outputValue);
diff --git a/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_lines_spec.js b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_lines_spec.js
new file mode 100644
index 00000000000..def76856dba
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/plugins/wrap_lines_spec.js
@@ -0,0 +1,12 @@
+import wrapLines from '~/vue_shared/components/source_viewer/plugins/wrap_lines';
+
+describe('Highlight.js plugin for wrapping lines', () => {
+ it('mutates the input value by wrapping each line in a div with the correct attributes', () => {
+ const inputValue = `// some content`;
+ const outputValue = `<div id="LC1" lang="javascript" class="line">${inputValue}</div>`;
+ const hljsResultMock = { value: inputValue, language: 'javascript' };
+
+ wrapLines(hljsResultMock);
+ expect(hljsResultMock.value).toBe(outputValue);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
index 715234e56fd..6b711b6b6b2 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_new_spec.js
@@ -3,9 +3,11 @@ import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_ne
import Chunk from '~/vue_shared/components/source_viewer/components/chunk_new.vue';
import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants';
import Tracking from '~/tracking';
+import LineHighlighter from '~/blob/line_highlighter';
import addBlobLinksTracking from '~/blob/blob_links_tracking';
import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data';
+jest.mock('~/blob/line_highlighter');
jest.mock('~/blob/blob_links_tracking');
describe('Source Viewer component', () => {
@@ -25,6 +27,10 @@ describe('Source Viewer component', () => {
return createComponent();
});
+ it('instantiates the lineHighlighter class', () => {
+ expect(LineHighlighter).toHaveBeenCalled();
+ });
+
describe('event tracking', () => {
it('fires a tracking event when the component is created', () => {
const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK };
diff --git a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
index 24f96195e05..776395b9717 100644
--- a/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
+++ b/spec/frontend/vue_shared/components/upload_dropzone/upload_dropzone_spec.js
@@ -1,6 +1,6 @@
import { GlIcon, GlSprintf } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
describe('Upload dropzone component', () => {
@@ -11,13 +11,13 @@ describe('Upload dropzone component', () => {
};
const findDropzoneCard = () => wrapper.find('.upload-dropzone-card');
- const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]');
+ const findDropzoneArea = () => wrapper.findByTestId('dropzone-area');
const findIcon = () => wrapper.findComponent(GlIcon);
- const findUploadText = () => wrapper.find('[data-testid="upload-text"]').text();
+ const findUploadText = () => wrapper.findByTestId('upload-text').text();
const findFileInput = () => wrapper.find('input[type="file"]');
- function createComponent({ slots = {}, data = {}, props = {} } = {}) {
- wrapper = shallowMount(UploadDropzone, {
+ function createComponent({ slots = {}, props = {} } = {}) {
+ wrapper = shallowMountExtended(UploadDropzone, {
slots,
propsData: {
displayAsCard: true,
@@ -26,9 +26,6 @@ describe('Upload dropzone component', () => {
stubs: {
GlSprintf,
},
- data() {
- return data;
- },
});
}
@@ -112,53 +109,50 @@ describe('Upload dropzone component', () => {
wrapper.trigger('drop', mockEvent);
await nextTick();
- expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
+ expect(wrapper.emitted('change')).toEqual([[[mockFile]]]);
});
});
describe('ondrop', () => {
- const mockData = { dragCounter: 1, isDragDataValid: true };
-
describe('when drag data is valid', () => {
it('emits upload event for valid files', () => {
- createComponent({ data: mockData });
+ createComponent();
const mockFile = { type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile] });
- wrapper.vm.ondrop(mockEvent);
- expect(wrapper.emitted().change[0]).toEqual([[mockFile]]);
+ wrapper.trigger('drop', mockEvent);
+ expect(wrapper.emitted('change')).toEqual([[[mockFile]]]);
});
it('emits error event when files are invalid', () => {
- createComponent({ data: mockData });
+ createComponent();
const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] });
- wrapper.vm.ondrop(mockEvent);
+ wrapper.trigger('drop', mockEvent);
expect(wrapper.emitted()).toHaveProperty('error');
});
it('allows validation function to be overwritten', () => {
- createComponent({ data: mockData, props: { isFileValid: () => true } });
+ createComponent({ props: { isFileValid: () => true } });
const mockEvent = mockDragEvent({ files: [{ type: 'audio/midi' }] });
- wrapper.vm.ondrop(mockEvent);
+ wrapper.trigger('drop', mockEvent);
expect(wrapper.emitted()).not.toHaveProperty('error');
});
describe('singleFileSelection = true', () => {
it('emits a single file on drop', () => {
createComponent({
- data: mockData,
props: { singleFileSelection: true },
});
const mockFile = { type: 'image/jpg' };
const mockEvent = mockDragEvent({ files: [mockFile] });
- wrapper.vm.ondrop(mockEvent);
- expect(wrapper.emitted().change[0]).toEqual([mockFile]);
+ wrapper.trigger('drop', mockEvent);
+ expect(wrapper.emitted('change')).toEqual([[mockFile]]);
});
});
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
index 90f9156af38..443d4e32580 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -95,4 +95,18 @@ describe('User Avatar Link Component', () => {
expect(wrapper.html()).toContain(badge);
});
});
+
+ describe('when popover props provided', () => {
+ beforeEach(() => {
+ createWrapper({ popoverUserId: 1, popoverUsername: defaultProps.username });
+ });
+
+ it('should render GlAvatarLink with popover support', () => {
+ expect(wrapper.attributes()).toMatchObject({
+ href: defaultProps.linkHref,
+ 'data-user-id': '1',
+ 'data-username': `${defaultProps.username}`,
+ });
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
index 075cb753301..32f9df8a63c 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
@@ -14,6 +14,7 @@ const DEFAULT_EMPTY_MESSAGE = 'None';
const createUser = (id) => ({
id,
name: 'Lorem',
+ username: 'lorem.ipsum',
web_url: `${TEST_HOST}/${id}`,
avatar_url: `${TEST_HOST}/${id}/avatar`,
});
@@ -90,6 +91,8 @@ describe('UserAvatarList', () => {
imgAlt: x.name,
tooltipText: x.name,
imgSize: TEST_IMAGE_SIZE,
+ popoverUserId: x.id,
+ popoverUsername: x.username,
}),
),
);
@@ -107,6 +110,8 @@ describe('UserAvatarList', () => {
imgAlt: x.name,
tooltipText: x.name,
imgSize: TEST_IMAGE_SIZE,
+ popoverUserId: x.id,
+ popoverUsername: x.username,
}),
),
);
diff --git a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
index 41181ab9a68..0457044f985 100644
--- a/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
+++ b/spec/frontend/vue_shared/components/user_popover/user_popover_spec.js
@@ -31,6 +31,7 @@ const DEFAULT_PROPS = {
name: 'Administrator',
location: 'Vienna',
localTime: '2:30 PM',
+ webUrl: '/root',
bot: false,
bio: null,
workInformation: null,
@@ -71,11 +72,11 @@ describe('User Popover Component', () => {
});
};
- const createWrapper = (props = {}) => {
+ const createWrapper = (props = {}, target = findTarget()) => {
wrapper = mountExtended(UserPopover, {
propsData: {
...DEFAULT_PROPS,
- target: findTarget(),
+ target,
...props,
},
});
@@ -518,4 +519,35 @@ describe('User Popover Component', () => {
expect(findToggleFollowButton().exists()).toBe(false);
});
});
+
+ describe('when current user is assignee/reviewer in a Merge Request', () => {
+ const { id, username, webUrl } = DEFAULT_PROPS.user;
+ const target = document.createElement('a');
+ target.setAttribute('href', webUrl);
+ target.classList.add('js-user-link');
+ target.dataset.currentUserId = id;
+ target.dataset.currentUsername = username;
+
+ it('renders popover with warning when user unable to merge', () => {
+ target.dataset.cannotMerge = 'true';
+
+ createWrapper({}, target);
+
+ const cannotMergeWarning = wrapper.findByTestId('cannot-merge');
+
+ expect(cannotMergeWarning.exists()).toBe(true);
+ expect(cannotMergeWarning.text()).toContain('Cannot merge');
+ expect(cannotMergeWarning.findComponent(GlIcon).props('name')).toBe('warning-solid');
+ });
+
+ it('renders popover without any warning when user is able to merge', () => {
+ delete target.dataset.cannotMerge;
+
+ createWrapper({}, target);
+
+ const cannotMergeWarning = wrapper.findByTestId('cannot-merge');
+
+ expect(cannotMergeWarning.exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index e881bfed35e..8c7657da8bc 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -1,10 +1,10 @@
import { GlSearchBoxByType } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import searchUsersQuery from '~/graphql_shared/queries/users_search.query.graphql';
import searchUsersQueryOnMR from '~/graphql_shared/queries/users_search_with_mr_permissions.graphql';
import { TYPE_MERGE_REQUEST } from '~/issues/constants';
@@ -44,20 +44,20 @@ Vue.use(VueApollo);
describe('User select dropdown', () => {
let wrapper;
let fakeApollo;
+ const hideDropdownMock = jest.fn();
const findSearchField = () => wrapper.findComponent(GlSearchBoxByType);
- const findParticipantsLoading = () => wrapper.find('[data-testid="loading-participants"]');
- const findSelectedParticipants = () => wrapper.findAll('[data-testid="selected-participant"]');
+ const findParticipantsLoading = () => wrapper.findByTestId('loading-participants');
+ const findSelectedParticipants = () => wrapper.findAllByTestId('selected-participant');
const findSelectedParticipantByIndex = (index) =>
findSelectedParticipants().at(index).findComponent(SidebarParticipant);
- const findUnselectedParticipants = () =>
- wrapper.findAll('[data-testid="unselected-participant"]');
+ const findUnselectedParticipants = () => wrapper.findAllByTestId('unselected-participant');
const findUnselectedParticipantByIndex = (index) =>
findUnselectedParticipants().at(index).findComponent(SidebarParticipant);
- const findCurrentUser = () => wrapper.findAll('[data-testid="current-user"]');
- const findIssuableAuthor = () => wrapper.findAll('[data-testid="issuable-author"]');
- const findUnassignLink = () => wrapper.find('[data-testid="unassign"]');
- const findEmptySearchResults = () => wrapper.find('[data-testid="empty-results"]');
+ const findCurrentUser = () => wrapper.findAllByTestId('current-user');
+ const findIssuableAuthor = () => wrapper.findAllByTestId('issuable-author');
+ const findUnassignLink = () => wrapper.findByTestId('unassign');
+ const findEmptySearchResults = () => wrapper.findAllByTestId('empty-results');
const searchQueryHandlerSuccess = jest.fn().mockResolvedValue(projectMembersResponse);
const participantsQueryHandlerSuccess = jest.fn().mockResolvedValue(participantsQueryResponse);
@@ -72,7 +72,7 @@ describe('User select dropdown', () => {
[searchUsersQueryOnMR, jest.fn().mockResolvedValue(searchResponseOnMR)],
[getIssueParticipantsQuery, participantsQueryHandler],
]);
- wrapper = shallowMount(UserSelect, {
+ wrapper = shallowMountExtended(UserSelect, {
apolloProvider: fakeApollo,
propsData: {
headerText: 'test',
@@ -97,7 +97,7 @@ describe('User select dropdown', () => {
</div>
`,
methods: {
- hide: jest.fn(),
+ hide: hideDropdownMock,
},
},
},
@@ -106,6 +106,7 @@ describe('User select dropdown', () => {
afterEach(() => {
fakeApollo = null;
+ hideDropdownMock.mockClear();
});
it('renders a loading spinner if participants are loading', () => {
@@ -290,12 +291,12 @@ describe('User select dropdown', () => {
value: [assignee],
},
});
- wrapper.vm.$refs.dropdown.hide = jest.fn();
+
await waitForPromises();
findUnassignLink().trigger('click');
- expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalledTimes(1);
+ expect(hideDropdownMock).toHaveBeenCalledTimes(1);
});
it('emits an empty array after unselecting the only selected assignee', async () => {
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index e54de25dc0d..b6c22ceaa23 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -85,7 +85,7 @@ describe('vue_shared/components/web_ide_link', () => {
let wrapper;
- function createComponent(props, { mountFn = shallowMountExtended, glFeatures = {} } = {}) {
+ function createComponent(props, { mountFn = shallowMountExtended, slots = {} } = {}) {
const fakeApollo = createMockApollo([
[getWritableForksQuery, jest.fn().mockResolvedValue(getWritableForksResponse)],
]);
@@ -98,9 +98,7 @@ describe('vue_shared/components/web_ide_link', () => {
forkPath,
...props,
},
- provide: {
- glFeatures,
- },
+ slots,
stubs: {
GlModal: stubComponent(GlModal, {
template: `
@@ -215,6 +213,27 @@ describe('vue_shared/components/web_ide_link', () => {
expect(findActionsButton().props('actions')).toEqual(expectedActions);
});
+ it('bubbles up shown and hidden events triggered by actions button component', () => {
+ createComponent();
+
+ expect(wrapper.emitted('shown')).toBe(undefined);
+ expect(wrapper.emitted('hidden')).toBe(undefined);
+
+ findActionsButton().vm.$emit('shown');
+ findActionsButton().vm.$emit('hidden');
+
+ expect(wrapper.emitted('shown')).toHaveLength(1);
+ expect(wrapper.emitted('hidden')).toHaveLength(1);
+ });
+
+ it('exposes a default slot', () => {
+ const slotContent = 'default slot content';
+
+ createComponent({}, { slots: { default: slotContent } });
+
+ expect(wrapper.text()).toContain(slotContent);
+ });
+
describe('when pipeline editor action is available', () => {
beforeEach(() => {
createComponent({
diff --git a/spec/frontend/vue_shared/issuable/list/mock_data.js b/spec/frontend/vue_shared/issuable/list/mock_data.js
index 964b48f4275..f8cf3ba5271 100644
--- a/spec/frontend/vue_shared/issuable/list/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/list/mock_data.js
@@ -5,6 +5,7 @@ import {
} from 'jest/vue_shared/components/filtered_search_bar/mock_data';
export const mockAuthor = {
+ __typename: 'UserCore',
id: 'gid://gitlab/User/1',
avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
name: 'Administrator',
@@ -13,6 +14,7 @@ export const mockAuthor = {
};
export const mockRegularLabel = {
+ __typename: 'Label',
id: 'gid://gitlab/GroupLabel/2048',
title: 'Documentation Update',
description: null,
@@ -21,6 +23,7 @@ export const mockRegularLabel = {
};
export const mockScopedLabel = {
+ __typename: 'Label',
id: 'gid://gitlab/ProjectLabel/2049',
title: 'status::confirmed',
description: null,
@@ -31,6 +34,7 @@ export const mockScopedLabel = {
export const mockLabels = [mockRegularLabel, mockScopedLabel];
export const mockCurrentUserTodo = {
+ __typename: 'Todo',
id: 'gid://gitlab/Todo/489',
state: 'done',
};
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
index fa38ab8d44d..d2b7b2e89c8 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
@@ -1,13 +1,16 @@
import { GlButton, GlBadge, GlIcon, GlAvatarLabeled, GlAvatarLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { TYPE_ISSUE, WORKSPACE_PROJECT } from '~/issues/constants';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import IssuableHeader from '~/vue_shared/issuable/show/components/issuable_header.vue';
-
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
const issuableHeaderProps = {
...mockIssuable,
...mockIssuableShowProps,
+ issuableType: TYPE_ISSUE,
+ workspaceType: WORKSPACE_PROJECT,
};
describe('IssuableHeader', () => {
@@ -53,6 +56,14 @@ describe('IssuableHeader', () => {
setHTMLFixture('<button class="js-toggle-right-sidebar-button">Collapse sidebar</button>');
});
+ it('emits a "toggle" event', () => {
+ createComponent();
+
+ findButton().vm.$emit('click');
+
+ expect(wrapper.emitted('toggle')).toEqual([[]]);
+ });
+
it('dispatches `click` event on sidebar toggle button', () => {
createComponent();
const toggleSidebarButtonEl = document.querySelector('.js-toggle-right-sidebar-button');
@@ -94,14 +105,12 @@ describe('IssuableHeader', () => {
});
it('renders confidential icon when issuable is confidential', () => {
- createComponent({
- confidential: true,
- });
+ createComponent({ confidential: true });
- const confidentialEl = wrapper.findByTestId('confidential');
-
- expect(confidentialEl.exists()).toBe(true);
- expect(confidentialEl.findComponent(GlIcon).props('name')).toBe('eye-slash');
+ expect(wrapper.findComponent(ConfidentialityBadge).props()).toEqual({
+ issuableType: 'issue',
+ workspaceType: 'project',
+ });
});
it('renders issuable author avatar', () => {
diff --git a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
index cc8a8d86d19..3306e316ed0 100644
--- a/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
+++ b/spec/frontend/vue_shared/new_namespace/components/welcome_spec.js
@@ -39,6 +39,18 @@ describe('Welcome page', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_tab', { label: 'test' });
});
+ it('renders image', () => {
+ const mockImgSrc = 'image1.svg';
+
+ createComponent({
+ propsData: {
+ panels: [{ name: 'test', href: '#', imageSrc: mockImgSrc }],
+ },
+ });
+
+ expect(wrapper.find('img').attributes('src')).toBe(mockImgSrc);
+ });
+
it('renders footer slot if provided', () => {
const DUMMY = 'Test message';
createComponent({
diff --git a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
index abc69da7a58..a7ddcbdd8bc 100644
--- a/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
+++ b/spec/frontend/vue_shared/new_namespace/new_namespace_page_spec.js
@@ -15,6 +15,7 @@ describe('Experimental new namespace creation app', () => {
const findWelcomePage = () => wrapper.findComponent(WelcomePage);
const findLegacyContainer = () => wrapper.findComponent(LegacyContainer);
const findBreadcrumb = () => wrapper.findComponent(GlBreadcrumb);
+ const findImage = () => wrapper.find('img');
const findNewTopLevelGroupAlert = () => wrapper.findComponent(NewTopLevelGroupAlert);
const findSuperSidebarToggle = () => wrapper.findComponent(SuperSidebarToggle);
@@ -22,8 +23,8 @@ describe('Experimental new namespace creation app', () => {
title: 'Create something',
initialBreadcrumbs: [{ text: 'Something', href: '#' }],
panels: [
- { name: 'panel1', selector: '#some-selector1' },
- { name: 'panel2', selector: '#some-selector2' },
+ { name: 'panel1', selector: '#some-selector1', imageSrc: 'panel1.svg' },
+ { name: 'panel2', selector: '#some-selector2', imageSrc: 'panel2.svg' },
],
persistenceKey: 'DEMO-PERSISTENCE-KEY',
};
@@ -82,6 +83,10 @@ describe('Experimental new namespace creation app', () => {
expect(breadcrumb.exists()).toBe(true);
expect(breadcrumb.props().items[0].text).toBe(DEFAULT_PROPS.initialBreadcrumbs[0].text);
});
+
+ it('renders images', () => {
+ expect(findImage().attributes('src')).toBe(DEFAULT_PROPS.panels[1].imageSrc);
+ });
});
it('renders extra description if provided', () => {
diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js
index a9ad675e538..533d312a4de 100644
--- a/spec/frontend/vue_shared/security_reports/mock_data.js
+++ b/spec/frontend/vue_shared/security_reports/mock_data.js
@@ -341,120 +341,6 @@ export const securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse = {
},
};
-export const securityReportMergeRequestDownloadPathsQueryResponse = {
- project: {
- id: '1',
- mergeRequest: {
- id: 'mr-1',
- headPipeline: {
- id: 'gid://gitlab/Ci::Pipeline/176',
- jobs: {
- nodes: [
- {
- id: 'job-1',
- name: 'secret_detection',
- artifacts: {
- nodes: [
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=trace',
- fileType: 'TRACE',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1399/artifacts/download?file_type=secret_detection',
- fileType: 'SECRET_DETECTION',
- __typename: 'CiJobArtifact',
- },
- ],
- __typename: 'CiJobArtifactConnection',
- },
- __typename: 'CiJob',
- },
- {
- id: 'job-2',
- name: 'bandit-sast',
- artifacts: {
- nodes: [
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=trace',
- fileType: 'TRACE',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1400/artifacts/download?file_type=sast',
- fileType: 'SAST',
- __typename: 'CiJobArtifact',
- },
- ],
- __typename: 'CiJobArtifactConnection',
- },
- __typename: 'CiJob',
- },
- {
- id: 'job-3',
- name: 'eslint-sast',
- artifacts: {
- nodes: [
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=trace',
- fileType: 'TRACE',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1401/artifacts/download?file_type=sast',
- fileType: 'SAST',
- __typename: 'CiJobArtifact',
- },
- ],
- __typename: 'CiJobArtifactConnection',
- },
- __typename: 'CiJob',
- },
- {
- id: 'job-4',
- name: 'all_artifacts',
- artifacts: {
- nodes: [
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=archive',
- fileType: 'ARCHIVE',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=trace',
- fileType: 'TRACE',
- __typename: 'CiJobArtifact',
- },
- {
- downloadPath:
- '/gitlab-org/secrets-detection-test/-/jobs/1402/artifacts/download?file_type=metadata',
- fileType: 'METADATA',
- __typename: 'CiJobArtifact',
- },
- ],
- __typename: 'CiJobArtifactConnection',
- },
- __typename: 'CiJob',
- },
- ],
- __typename: 'CiJobConnection',
- },
- __typename: 'Pipeline',
- },
- __typename: 'MergeRequest',
- },
- __typename: 'Project',
- },
-};
-
export const securityReportPipelineDownloadPathsQueryResponse = {
project: {
id: 'project-1',
@@ -566,9 +452,6 @@ export const securityReportPipelineDownloadPathsQueryResponse = {
__typename: 'Project',
};
-/**
- * These correspond to SAST jobs in the securityReportMergeRequestDownloadPathsQueryResponse above.
- */
export const sastArtifacts = [
{
name: 'bandit-sast',
@@ -582,9 +465,6 @@ export const sastArtifacts = [
},
];
-/**
- * These correspond to Secret Detection jobs in the securityReportMergeRequestDownloadPathsQueryResponse above.
- */
export const secretDetectionArtifacts = [
{
name: 'secret_detection',
@@ -594,13 +474,6 @@ export const secretDetectionArtifacts = [
},
];
-export const expectedDownloadDropdownPropsWithTitle = {
- loading: false,
- artifacts: [...secretDetectionArtifacts, ...sastArtifacts],
- text: '',
- title: 'Download results',
-};
-
export const expectedDownloadDropdownPropsWithText = {
loading: false,
artifacts: [...secretDetectionArtifacts, ...sastArtifacts],
@@ -608,9 +481,6 @@ export const expectedDownloadDropdownPropsWithText = {
text: 'Download results',
};
-/**
- * These correspond to any jobs with zip archives in the securityReportMergeRequestDownloadPathsQueryResponse above.
- */
export const archiveArtifacts = [
{
name: 'all_artifacts Archive',
@@ -619,9 +489,6 @@ export const archiveArtifacts = [
},
];
-/**
- * These correspond to any jobs with trace data in the securityReportMergeRequestDownloadPathsQueryResponse above.
- */
export const traceArtifacts = [
{
name: 'secret_detection Trace',
@@ -645,9 +512,6 @@ export const traceArtifacts = [
},
];
-/**
- * These correspond to any jobs with metadata data in the securityReportMergeRequestDownloadPathsQueryResponse above.
- */
export const metadataArtifacts = [
{
name: 'all_artifacts Metadata',
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
deleted file mode 100644
index 257f59612e8..00000000000
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ /dev/null
@@ -1,267 +0,0 @@
-import { mount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import { merge } from 'lodash';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import Vuex from 'vuex';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import { trimText } from 'helpers/text_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import {
- expectedDownloadDropdownPropsWithText,
- securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse,
- securityReportMergeRequestDownloadPathsQueryResponse,
- sastDiffSuccessMock,
- secretDetectionDiffSuccessMock,
-} from 'jest/vue_shared/security_reports/mock_data';
-import { createAlert } from '~/alert';
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
-import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
-import {
- REPORT_TYPE_SAST,
- REPORT_TYPE_SECRET_DETECTION,
-} from '~/vue_shared/security_reports/constants';
-import securityReportMergeRequestDownloadPathsQuery from '~/vue_shared/security_reports/graphql/queries/security_report_merge_request_download_paths.query.graphql';
-import SecurityReportsApp from '~/vue_shared/security_reports/security_reports_app.vue';
-
-jest.mock('~/alert');
-
-Vue.use(VueApollo);
-Vue.use(Vuex);
-
-const SAST_COMPARISON_PATH = '/sast.json';
-const SECRET_DETECTION_COMPARISON_PATH = '/secret_detection.json';
-
-describe('Security reports app', () => {
- let wrapper;
-
- const props = {
- pipelineId: 123,
- projectId: 456,
- securityReportsDocsPath: '/docs',
- discoverProjectSecurityPath: '/discoverProjectSecurityPath',
- };
-
- const createComponent = (options) => {
- wrapper = mount(
- SecurityReportsApp,
- merge(
- {
- propsData: { ...props },
- stubs: {
- HelpIcon: true,
- },
- },
- options,
- ),
- );
- };
-
- const pendingHandler = () => new Promise(() => {});
- const successHandler = () =>
- Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryResponse });
- const successEmptyHandler = () =>
- Promise.resolve({ data: securityReportMergeRequestDownloadPathsQueryNoArtifactsResponse });
- const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
- const createMockApolloProvider = (handler) => {
- const requestHandlers = [[securityReportMergeRequestDownloadPathsQuery, handler]];
-
- return createMockApollo(requestHandlers);
- };
-
- const findDownloadDropdown = () => wrapper.findComponent(SecurityReportDownloadDropdown);
- const findHelpIconComponent = () => wrapper.findComponent(HelpIcon);
-
- describe('given the artifacts query is loading', () => {
- beforeEach(() => {
- createComponent({
- apolloProvider: createMockApolloProvider(pendingHandler),
- });
- });
-
- // TODO: Remove this assertion as part of
- // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
- it('initially renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
- });
-
- describe('given the artifacts query loads successfully', () => {
- beforeEach(() => {
- createComponent({
- apolloProvider: createMockApolloProvider(successHandler),
- });
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
- });
-
- it('renders the expected message', () => {
- expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
- });
-
- it('renders a help link', () => {
- expect(findHelpIconComponent().props()).toEqual({
- helpPath: props.securityReportsDocsPath,
- discoverProjectSecurityPath: props.discoverProjectSecurityPath,
- });
- });
- });
-
- describe('given the artifacts query loads successfully with no artifacts', () => {
- beforeEach(() => {
- createComponent({
- apolloProvider: createMockApolloProvider(successEmptyHandler),
- });
- });
-
- // TODO: Remove this assertion as part of
- // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
- it('initially renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
- });
-
- describe('given the artifacts query fails', () => {
- beforeEach(() => {
- createComponent({
- apolloProvider: createMockApolloProvider(failureHandler),
- });
- });
-
- it('calls createAlert correctly', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: SecurityReportsApp.i18n.apiError,
- captureError: true,
- error: expect.any(Error),
- });
- });
-
- // TODO: Remove this assertion as part of
- // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
- it('renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
- });
-
- describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => {
- let mock;
-
- const createComponentWithFlagEnabled = (options) =>
- createComponent(
- merge(options, {
- provide: {
- glFeatures: {
- coreSecurityMrWidgetCounts: true,
- },
- },
- apolloProvider: createMockApolloProvider(successHandler),
- }),
- );
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- const SAST_SUCCESS_MESSAGE =
- 'Security scanning detected 1 potential vulnerability 1 Critical 0 High and 0 Others';
- const SECRET_DETECTION_SUCCESS_MESSAGE =
- 'Security scanning detected 2 potential vulnerabilities 1 Critical 1 High and 0 Others';
- describe.each`
- reportType | pathProp | path | successResponse | successMessage
- ${REPORT_TYPE_SAST} | ${'sastComparisonPath'} | ${SAST_COMPARISON_PATH} | ${sastDiffSuccessMock} | ${SAST_SUCCESS_MESSAGE}
- ${REPORT_TYPE_SECRET_DETECTION} | ${'secretDetectionComparisonPath'} | ${SECRET_DETECTION_COMPARISON_PATH} | ${secretDetectionDiffSuccessMock} | ${SECRET_DETECTION_SUCCESS_MESSAGE}
- `(
- 'given a $pathProp and $reportType artifact',
- ({ pathProp, path, successResponse, successMessage }) => {
- describe('when loading', () => {
- beforeEach(() => {
- mock = new MockAdapter(axios, { delayResponse: 1 });
- mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse);
-
- createComponentWithFlagEnabled({
- propsData: {
- [pathProp]: path,
- },
- });
-
- return waitForPromises();
- });
-
- it('should have loading message', () => {
- expect(wrapper.text()).toContain('Security scanning is loading');
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
- });
- });
-
- describe('when successfully loaded', () => {
- beforeEach(() => {
- mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse);
-
- createComponentWithFlagEnabled({
- propsData: {
- [pathProp]: path,
- },
- });
-
- return waitForPromises();
- });
-
- it('should show counts', () => {
- expect(trimText(wrapper.text())).toContain(successMessage);
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
- });
- });
-
- describe('when an error occurs', () => {
- beforeEach(() => {
- mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- createComponentWithFlagEnabled({
- propsData: {
- [pathProp]: path,
- },
- });
-
- return waitForPromises();
- });
-
- it('should show error message', () => {
- expect(trimText(wrapper.text())).toContain('Loading resulted in an error');
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownPropsWithText);
- });
- });
-
- describe('when the comparison endpoint is not provided', () => {
- beforeEach(() => {
- mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
-
- createComponentWithFlagEnabled();
-
- return waitForPromises();
- });
-
- it('renders the basic scansHaveRun message', () => {
- expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
- });
- });
- },
- );
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/store/getters_spec.js b/spec/frontend/vue_shared/security_reports/store/getters_spec.js
deleted file mode 100644
index bcc8955ba02..00000000000
--- a/spec/frontend/vue_shared/security_reports/store/getters_spec.js
+++ /dev/null
@@ -1,182 +0,0 @@
-import {
- groupedSummaryText,
- allReportsHaveError,
- areReportsLoading,
- anyReportHasError,
- areAllReportsLoading,
- anyReportHasIssues,
- summaryCounts,
-} from '~/vue_shared/security_reports/store/getters';
-import createSastState from '~/vue_shared/security_reports/store/modules/sast/state';
-import createSecretDetectionState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
-import createState from '~/vue_shared/security_reports/store/state';
-import { groupedTextBuilder } from '~/vue_shared/security_reports/store/utils';
-import { CRITICAL, HIGH, LOW } from '~/vulnerabilities/constants';
-
-const generateVuln = (severity) => ({ severity });
-
-describe('Security reports getters', () => {
- let state;
-
- beforeEach(() => {
- state = createState();
- state.sast = createSastState();
- state.secretDetection = createSecretDetectionState();
- });
-
- describe('summaryCounts', () => {
- it('returns 0 count for empty state', () => {
- expect(summaryCounts(state)).toEqual({
- critical: 0,
- high: 0,
- other: 0,
- });
- });
-
- describe('combines all reports', () => {
- it('of the same severity', () => {
- state.sast.newIssues = [generateVuln(CRITICAL)];
- state.secretDetection.newIssues = [generateVuln(CRITICAL)];
-
- expect(summaryCounts(state)).toEqual({
- critical: 2,
- high: 0,
- other: 0,
- });
- });
-
- it('of different severities', () => {
- state.sast.newIssues = [generateVuln(CRITICAL)];
- state.secretDetection.newIssues = [generateVuln(HIGH), generateVuln(LOW)];
-
- expect(summaryCounts(state)).toEqual({
- critical: 1,
- high: 1,
- other: 1,
- });
- });
- });
- });
-
- describe('groupedSummaryText', () => {
- it('returns failed text', () => {
- expect(
- groupedSummaryText(state, {
- allReportsHaveError: true,
- areReportsLoading: false,
- summaryCounts: {},
- }),
- ).toEqual({ message: 'Security scanning failed loading any results' });
- });
-
- it('returns `is loading` as status text', () => {
- expect(
- groupedSummaryText(state, {
- allReportsHaveError: false,
- areReportsLoading: true,
- summaryCounts: {},
- }),
- ).toEqual(
- groupedTextBuilder({
- reportType: 'Security scanning',
- critical: 0,
- high: 0,
- other: 0,
- status: 'is loading',
- }),
- );
- });
-
- it('returns no new status text if there are existing ones', () => {
- expect(
- groupedSummaryText(state, {
- allReportsHaveError: false,
- areReportsLoading: false,
- summaryCounts: {},
- }),
- ).toEqual(
- groupedTextBuilder({
- reportType: 'Security scanning',
- critical: 0,
- high: 0,
- other: 0,
- status: '',
- }),
- );
- });
- });
-
- describe('areReportsLoading', () => {
- it('returns true when any report is loading', () => {
- state.sast.isLoading = true;
-
- expect(areReportsLoading(state)).toEqual(true);
- });
-
- it('returns false when none of the reports are loading', () => {
- expect(areReportsLoading(state)).toEqual(false);
- });
- });
-
- describe('areAllReportsLoading', () => {
- it('returns true when all reports are loading', () => {
- state.sast.isLoading = true;
- state.secretDetection.isLoading = true;
-
- expect(areAllReportsLoading(state)).toEqual(true);
- });
-
- it('returns false when some of the reports are loading', () => {
- state.sast.isLoading = true;
-
- expect(areAllReportsLoading(state)).toEqual(false);
- });
-
- it('returns false when none of the reports are loading', () => {
- expect(areAllReportsLoading(state)).toEqual(false);
- });
- });
-
- describe('allReportsHaveError', () => {
- it('returns true when all reports have error', () => {
- state.sast.hasError = true;
- state.secretDetection.hasError = true;
-
- expect(allReportsHaveError(state)).toEqual(true);
- });
-
- it('returns false when none of the reports have error', () => {
- expect(allReportsHaveError(state)).toEqual(false);
- });
-
- it('returns false when one of the reports does not have error', () => {
- state.secretDetection.hasError = true;
-
- expect(allReportsHaveError(state)).toEqual(false);
- });
- });
-
- describe('anyReportHasError', () => {
- it('returns true when any of the reports has error', () => {
- state.sast.hasError = true;
-
- expect(anyReportHasError(state)).toEqual(true);
- });
-
- it('returns false when none of the reports has error', () => {
- expect(anyReportHasError(state)).toEqual(false);
- });
- });
-
- describe('anyReportHasIssues', () => {
- it('returns true when any of the reports has new issues', () => {
- state.sast.newIssues.push(generateVuln(LOW));
-
- expect(anyReportHasIssues(state)).toEqual(true);
- });
-
- it('returns false when none of the reports has error', () => {
- expect(anyReportHasIssues(state)).toEqual(false);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
deleted file mode 100644
index 0cab950cb77..00000000000
--- a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
+++ /dev/null
@@ -1,197 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
-
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import * as actions from '~/vue_shared/security_reports/store/modules/sast/actions';
-import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types';
-import createState from '~/vue_shared/security_reports/store/modules/sast/state';
-
-const diffEndpoint = 'diff-endpoint.json';
-const blobPath = 'blob-path.json';
-const reports = {
- base: 'base',
- head: 'head',
- enrichData: 'enrichData',
- diff: 'diff',
-};
-const error = 'Something went wrong';
-const vulnerabilityFeedbackPath = 'vulnerability-feedback-path';
-const rootState = { vulnerabilityFeedbackPath, blobPath };
-
-let state;
-
-describe('sast report actions', () => {
- beforeEach(() => {
- state = createState();
- });
-
- describe('setDiffEndpoint', () => {
- it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => {
- return testAction(
- actions.setDiffEndpoint,
- diffEndpoint,
- state,
- [
- {
- type: types.SET_DIFF_ENDPOINT,
- payload: diffEndpoint,
- },
- ],
- [],
- );
- });
- });
-
- describe('requestDiff', () => {
- it(`should commit ${types.REQUEST_DIFF}`, () => {
- return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []);
- });
- });
-
- describe('receiveDiffSuccess', () => {
- it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => {
- return testAction(
- actions.receiveDiffSuccess,
- reports,
- state,
- [
- {
- type: types.RECEIVE_DIFF_SUCCESS,
- payload: reports,
- },
- ],
- [],
- );
- });
- });
-
- describe('receiveDiffError', () => {
- it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => {
- return testAction(
- actions.receiveDiffError,
- error,
- state,
- [
- {
- type: types.RECEIVE_DIFF_ERROR,
- payload: error,
- },
- ],
- [],
- );
- });
- });
-
- describe('fetchDiff', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- state.paths.diffEndpoint = diffEndpoint;
- rootState.canReadVulnerabilityFeedback = true;
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('when diff and vulnerability feedback endpoints respond successfully', () => {
- beforeEach(() => {
- mock
- .onGet(diffEndpoint)
- .replyOnce(HTTP_STATUS_OK, reports.diff)
- .onGet(vulnerabilityFeedbackPath)
- .replyOnce(HTTP_STATUS_OK, reports.enrichData);
- });
-
- it('should dispatch the `receiveDiffSuccess` action', () => {
- const { diff, enrichData } = reports;
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [
- { type: 'requestDiff' },
- {
- type: 'receiveDiffSuccess',
- payload: {
- diff,
- enrichData,
- },
- },
- ],
- );
- });
- });
-
- describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => {
- beforeEach(() => {
- rootState.canReadVulnerabilityFeedback = false;
- mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff);
- });
-
- it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => {
- const { diff } = reports;
- const enrichData = [];
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [
- { type: 'requestDiff' },
- {
- type: 'receiveDiffSuccess',
- payload: {
- diff,
- enrichData,
- },
- },
- ],
- );
- });
- });
-
- describe('when the vulnerability feedback endpoint fails', () => {
- beforeEach(() => {
- mock
- .onGet(diffEndpoint)
- .replyOnce(HTTP_STATUS_OK, reports.diff)
- .onGet(vulnerabilityFeedbackPath)
- .replyOnce(HTTP_STATUS_NOT_FOUND);
- });
-
- it('should dispatch the `receiveError` action', () => {
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
- );
- });
- });
-
- describe('when the diff endpoint fails', () => {
- beforeEach(() => {
- mock
- .onGet(diffEndpoint)
- .replyOnce(HTTP_STATUS_NOT_FOUND)
- .onGet(vulnerabilityFeedbackPath)
- .replyOnce(HTTP_STATUS_OK, reports.enrichData);
- });
-
- it('should dispatch the `receiveDiffError` action', () => {
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
- );
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js
deleted file mode 100644
index d6119f44619..00000000000
--- a/spec/frontend/vue_shared/security_reports/store/modules/sast/mutations_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types';
-import mutations from '~/vue_shared/security_reports/store/modules/sast/mutations';
-import createState from '~/vue_shared/security_reports/store/modules/sast/state';
-
-const createIssue = ({ ...config }) => ({ changed: false, ...config });
-
-describe('sast module mutations', () => {
- const path = 'path';
- let state;
-
- beforeEach(() => {
- state = createState();
- });
-
- describe(types.SET_DIFF_ENDPOINT, () => {
- it('should set the SAST diff endpoint', () => {
- mutations[types.SET_DIFF_ENDPOINT](state, path);
-
- expect(state.paths.diffEndpoint).toBe(path);
- });
- });
-
- describe(types.REQUEST_DIFF, () => {
- it('should set the `isLoading` status to `true`', () => {
- mutations[types.REQUEST_DIFF](state);
-
- expect(state.isLoading).toBe(true);
- });
- });
-
- describe(types.RECEIVE_DIFF_SUCCESS, () => {
- beforeEach(() => {
- const reports = {
- diff: {
- added: [
- createIssue({ cve: 'CVE-1' }),
- createIssue({ cve: 'CVE-2' }),
- createIssue({ cve: 'CVE-3' }),
- ],
- fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })],
- existing: [createIssue({ cve: 'CVE-6' })],
- base_report_out_of_date: true,
- },
- };
- state.isLoading = true;
- mutations[types.RECEIVE_DIFF_SUCCESS](state, reports);
- });
-
- it('should set the `isLoading` status to `false`', () => {
- expect(state.isLoading).toBe(false);
- });
-
- it('should set the `baseReportOutofDate` status to `false`', () => {
- expect(state.baseReportOutofDate).toBe(true);
- });
-
- it('should have the relevant `new` issues', () => {
- expect(state.newIssues).toHaveLength(3);
- });
-
- it('should have the relevant `resolved` issues', () => {
- expect(state.resolvedIssues).toHaveLength(2);
- });
-
- it('should have the relevant `all` issues', () => {
- expect(state.allIssues).toHaveLength(1);
- });
- });
-
- describe(types.RECEIVE_DIFF_ERROR, () => {
- beforeEach(() => {
- state.isLoading = true;
- mutations[types.RECEIVE_DIFF_ERROR](state);
- });
-
- it('should set the `isLoading` status to `false`', () => {
- expect(state.isLoading).toBe(false);
- });
-
- it('should set the `hasError` status to `true`', () => {
- expect(state.hasError).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
deleted file mode 100644
index 7197784c3e8..00000000000
--- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
+++ /dev/null
@@ -1,198 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import testAction from 'helpers/vuex_action_helper';
-
-import axios from '~/lib/utils/axios_utils';
-import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import * as actions from '~/vue_shared/security_reports/store/modules/secret_detection/actions';
-import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types';
-import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
-
-const diffEndpoint = 'diff-endpoint.json';
-const blobPath = 'blob-path.json';
-const reports = {
- base: 'base',
- head: 'head',
- enrichData: 'enrichData',
- diff: 'diff',
-};
-const error = 'Something went wrong';
-const vulnerabilityFeedbackPath = 'vulnerability-feedback-path';
-const rootState = { vulnerabilityFeedbackPath, blobPath };
-
-let state;
-
-describe('secret detection report actions', () => {
- beforeEach(() => {
- state = createState();
- });
-
- describe('setDiffEndpoint', () => {
- it(`should commit ${types.SET_DIFF_ENDPOINT} with the correct path`, () => {
- return testAction(
- actions.setDiffEndpoint,
- diffEndpoint,
- state,
- [
- {
- type: types.SET_DIFF_ENDPOINT,
- payload: diffEndpoint,
- },
- ],
- [],
- );
- });
- });
-
- describe('requestDiff', () => {
- it(`should commit ${types.REQUEST_DIFF}`, () => {
- return testAction(actions.requestDiff, {}, state, [{ type: types.REQUEST_DIFF }], []);
- });
- });
-
- describe('receiveDiffSuccess', () => {
- it(`should commit ${types.RECEIVE_DIFF_SUCCESS} with the correct response`, () => {
- return testAction(
- actions.receiveDiffSuccess,
- reports,
- state,
- [
- {
- type: types.RECEIVE_DIFF_SUCCESS,
- payload: reports,
- },
- ],
- [],
- );
- });
- });
-
- describe('receiveDiffError', () => {
- it(`should commit ${types.RECEIVE_DIFF_ERROR} with the correct response`, () => {
- return testAction(
- actions.receiveDiffError,
- error,
- state,
- [
- {
- type: types.RECEIVE_DIFF_ERROR,
- payload: error,
- },
- ],
- [],
- );
- });
- });
-
- describe('fetchDiff', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- state.paths.diffEndpoint = diffEndpoint;
- rootState.canReadVulnerabilityFeedback = true;
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('when diff and vulnerability feedback endpoints respond successfully', () => {
- beforeEach(() => {
- mock
- .onGet(diffEndpoint)
- .replyOnce(HTTP_STATUS_OK, reports.diff)
- .onGet(vulnerabilityFeedbackPath)
- .replyOnce(HTTP_STATUS_OK, reports.enrichData);
- });
-
- it('should dispatch the `receiveDiffSuccess` action', () => {
- const { diff, enrichData } = reports;
-
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [
- { type: 'requestDiff' },
- {
- type: 'receiveDiffSuccess',
- payload: {
- diff,
- enrichData,
- },
- },
- ],
- );
- });
- });
-
- describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => {
- beforeEach(() => {
- rootState.canReadVulnerabilityFeedback = false;
- mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff);
- });
-
- it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => {
- const { diff } = reports;
- const enrichData = [];
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [
- { type: 'requestDiff' },
- {
- type: 'receiveDiffSuccess',
- payload: {
- diff,
- enrichData,
- },
- },
- ],
- );
- });
- });
-
- describe('when the vulnerability feedback endpoint fails', () => {
- beforeEach(() => {
- mock
- .onGet(diffEndpoint)
- .replyOnce(HTTP_STATUS_OK, reports.diff)
- .onGet(vulnerabilityFeedbackPath)
- .replyOnce(HTTP_STATUS_NOT_FOUND);
- });
-
- it('should dispatch the `receiveDiffError` action', () => {
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
- );
- });
- });
-
- describe('when the diff endpoint fails', () => {
- beforeEach(() => {
- mock
- .onGet(diffEndpoint)
- .replyOnce(HTTP_STATUS_NOT_FOUND)
- .onGet(vulnerabilityFeedbackPath)
- .replyOnce(HTTP_STATUS_OK, reports.enrichData);
- });
-
- it('should dispatch the `receiveDiffError` action', () => {
- return testAction(
- actions.fetchDiff,
- {},
- { ...rootState, ...state },
- [],
- [{ type: 'requestDiff' }, { type: 'receiveDiffError' }],
- );
- });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js
deleted file mode 100644
index 42da7476a40..00000000000
--- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/mutations_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types';
-import mutations from '~/vue_shared/security_reports/store/modules/secret_detection/mutations';
-import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
-
-const createIssue = ({ ...config }) => ({ changed: false, ...config });
-
-describe('secret detection module mutations', () => {
- const path = 'path';
- let state;
-
- beforeEach(() => {
- state = createState();
- });
-
- describe(types.SET_DIFF_ENDPOINT, () => {
- it('should set the secret detection diff endpoint', () => {
- mutations[types.SET_DIFF_ENDPOINT](state, path);
-
- expect(state.paths.diffEndpoint).toBe(path);
- });
- });
-
- describe(types.REQUEST_DIFF, () => {
- it('should set the `isLoading` status to `true`', () => {
- mutations[types.REQUEST_DIFF](state);
-
- expect(state.isLoading).toBe(true);
- });
- });
-
- describe(types.RECEIVE_DIFF_SUCCESS, () => {
- beforeEach(() => {
- const reports = {
- diff: {
- added: [
- createIssue({ cve: 'CVE-1' }),
- createIssue({ cve: 'CVE-2' }),
- createIssue({ cve: 'CVE-3' }),
- ],
- fixed: [createIssue({ cve: 'CVE-4' }), createIssue({ cve: 'CVE-5' })],
- existing: [createIssue({ cve: 'CVE-6' })],
- base_report_out_of_date: true,
- },
- };
- state.isLoading = true;
- mutations[types.RECEIVE_DIFF_SUCCESS](state, reports);
- });
-
- it('should set the `isLoading` status to `false`', () => {
- expect(state.isLoading).toBe(false);
- });
-
- it('should set the `baseReportOutofDate` status to `true`', () => {
- expect(state.baseReportOutofDate).toBe(true);
- });
-
- it('should have the relevant `new` issues', () => {
- expect(state.newIssues).toHaveLength(3);
- });
-
- it('should have the relevant `resolved` issues', () => {
- expect(state.resolvedIssues).toHaveLength(2);
- });
-
- it('should have the relevant `all` issues', () => {
- expect(state.allIssues).toHaveLength(1);
- });
- });
-
- describe(types.RECEIVE_DIFF_ERROR, () => {
- beforeEach(() => {
- state.isLoading = true;
- mutations[types.RECEIVE_DIFF_ERROR](state);
- });
-
- it('should set the `isLoading` status to `false`', () => {
- expect(state.isLoading).toBe(false);
- });
-
- it('should set the `hasError` status to `true`', () => {
- expect(state.hasError).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/store/utils_spec.js b/spec/frontend/vue_shared/security_reports/store/utils_spec.js
deleted file mode 100644
index c8750cd58a0..00000000000
--- a/spec/frontend/vue_shared/security_reports/store/utils_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import { enrichVulnerabilityWithFeedback } from '~/vue_shared/security_reports/store/utils';
-import {
- FEEDBACK_TYPE_DISMISSAL,
- FEEDBACK_TYPE_ISSUE,
- FEEDBACK_TYPE_MERGE_REQUEST,
-} from '~/vue_shared/security_reports/constants';
-
-describe('security reports store utils', () => {
- const vulnerability = { uuid: 1 };
-
- describe('enrichVulnerabilityWithFeedback', () => {
- const dismissalFeedback = {
- feedback_type: FEEDBACK_TYPE_DISMISSAL,
- finding_uuid: vulnerability.uuid,
- };
- const dismissalVuln = { ...vulnerability, isDismissed: true, dismissalFeedback };
-
- const issueFeedback = {
- feedback_type: FEEDBACK_TYPE_ISSUE,
- issue_iid: 1,
- finding_uuid: vulnerability.uuid,
- };
- const issueVuln = { ...vulnerability, hasIssue: true, issue_feedback: issueFeedback };
- const mrFeedback = {
- feedback_type: FEEDBACK_TYPE_MERGE_REQUEST,
- merge_request_iid: 1,
- finding_uuid: vulnerability.uuid,
- };
- const mrVuln = {
- ...vulnerability,
- hasMergeRequest: true,
- merge_request_feedback: mrFeedback,
- };
-
- it.each`
- feedbacks | expected
- ${[dismissalFeedback]} | ${dismissalVuln}
- ${[{ ...issueFeedback, issue_iid: null }]} | ${vulnerability}
- ${[issueFeedback]} | ${issueVuln}
- ${[{ ...mrFeedback, merge_request_iid: null }]} | ${vulnerability}
- ${[mrFeedback]} | ${mrVuln}
- ${[dismissalFeedback, issueFeedback, mrFeedback]} | ${{ ...dismissalVuln, ...issueVuln, ...mrVuln }}
- `('returns expected enriched vulnerability: $expected', ({ feedbacks, expected }) => {
- const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks);
-
- expect(enrichedVulnerability).toEqual(expected);
- });
-
- it('matches correct feedback objects to vulnerability', () => {
- const feedbacks = [
- dismissalFeedback,
- issueFeedback,
- mrFeedback,
- { ...dismissalFeedback, finding_uuid: 2 },
- { ...issueFeedback, finding_uuid: 2 },
- { ...mrFeedback, finding_uuid: 2 },
- ];
- const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks);
-
- expect(enrichedVulnerability).toEqual({ ...dismissalVuln, ...issueVuln, ...mrVuln });
- });
- });
-});
diff --git a/spec/frontend/vue_shared/security_reports/utils_spec.js b/spec/frontend/vue_shared/security_reports/utils_spec.js
deleted file mode 100644
index b7129ece698..00000000000
--- a/spec/frontend/vue_shared/security_reports/utils_spec.js
+++ /dev/null
@@ -1,48 +0,0 @@
-import {
- REPORT_TYPE_SAST,
- REPORT_TYPE_SECRET_DETECTION,
- REPORT_FILE_TYPES,
-} from '~/vue_shared/security_reports/constants';
-import {
- extractSecurityReportArtifactsFromMergeRequest,
- extractSecurityReportArtifactsFromPipeline,
-} from '~/vue_shared/security_reports/utils';
-import {
- securityReportMergeRequestDownloadPathsQueryResponse,
- securityReportPipelineDownloadPathsQueryResponse,
- sastArtifacts,
- secretDetectionArtifacts,
- archiveArtifacts,
- traceArtifacts,
- metadataArtifacts,
-} from './mock_data';
-
-describe.each([
- [
- 'extractSecurityReportArtifactsFromMergeRequest',
- extractSecurityReportArtifactsFromMergeRequest,
- securityReportMergeRequestDownloadPathsQueryResponse,
- ],
- [
- 'extractSecurityReportArtifactsFromPipelines',
- extractSecurityReportArtifactsFromPipeline,
- securityReportPipelineDownloadPathsQueryResponse,
- ],
-])('%s', (funcName, extractFunc, response) => {
- it.each`
- reportTypes | expectedArtifacts
- ${[]} | ${[]}
- ${['foo']} | ${[]}
- ${[REPORT_TYPE_SAST]} | ${sastArtifacts}
- ${[REPORT_TYPE_SECRET_DETECTION]} | ${secretDetectionArtifacts}
- ${[REPORT_TYPE_SAST, REPORT_TYPE_SECRET_DETECTION]} | ${[...secretDetectionArtifacts, ...sastArtifacts]}
- ${[REPORT_FILE_TYPES.ARCHIVE]} | ${archiveArtifacts}
- ${[REPORT_FILE_TYPES.TRACE]} | ${traceArtifacts}
- ${[REPORT_FILE_TYPES.METADATA]} | ${metadataArtifacts}
- `(
- 'returns the expected artifacts given report types $reportTypes',
- ({ reportTypes, expectedArtifacts }) => {
- expect(extractFunc(reportTypes, response)).toEqual(expectedArtifacts);
- },
- );
-});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
index 2e901783e07..e4180b2d178 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -1,11 +1,10 @@
import { GlDisclosureDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective } from 'helpers/vue_mock_directive';
-import EmojiPicker from '~/emoji/components/picker.vue';
-import waitForPromises from 'helpers/wait_for_promises';
+import { stubComponent } from 'helpers/stub_component';
import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
import WorkItemNoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
@@ -15,18 +14,19 @@ Vue.use(VueApollo);
describe('Work Item Note Actions', () => {
let wrapper;
const noteId = '1';
+ const showSpy = jest.fn();
const findReplyButton = () => wrapper.findComponent(ReplyButton);
- const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]');
- const findEmojiButton = () => wrapper.find('[data-testid="note-emoji-button"]');
+ const findEditButton = () => wrapper.findByTestId('edit-work-item-note');
+ const findEmojiButton = () => wrapper.findByTestId('note-emoji-button');
const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
- const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
- const findCopyLinkButton = () => wrapper.find('[data-testid="copy-link-action"]');
- const findAssignUnassignButton = () => wrapper.find('[data-testid="assign-note-action"]');
- const findReportAbuseToAdminButton = () => wrapper.find('[data-testid="abuse-note-action"]');
- const findAuthorBadge = () => wrapper.find('[data-testid="author-badge"]');
- const findMaxAccessLevelBadge = () => wrapper.find('[data-testid="max-access-level-badge"]');
- const findContributorBadge = () => wrapper.find('[data-testid="contributor-badge"]');
+ const findDeleteNoteButton = () => wrapper.findByTestId('delete-note-action');
+ const findCopyLinkButton = () => wrapper.findByTestId('copy-link-action');
+ const findAssignUnassignButton = () => wrapper.findByTestId('assign-note-action');
+ const findReportAbuseToAdminButton = () => wrapper.findByTestId('abuse-note-action');
+ const findAuthorBadge = () => wrapper.findByTestId('author-badge');
+ const findMaxAccessLevelBadge = () => wrapper.findByTestId('max-access-level-badge');
+ const findContributorBadge = () => wrapper.findByTestId('contributor-badge');
const addEmojiMutationResolver = jest.fn().mockResolvedValue({
data: {
@@ -34,11 +34,6 @@ describe('Work Item Note Actions', () => {
},
});
- const EmojiPickerStub = {
- props: EmojiPicker.props,
- template: '<div></div>',
- };
-
const createComponent = ({
showReply = true,
showEdit = true,
@@ -51,10 +46,12 @@ describe('Work Item Note Actions', () => {
maxAccessLevelOfAuthor = '',
projectName = 'Project name',
} = {}) => {
- wrapper = shallowMount(WorkItemNoteActions, {
+ wrapper = shallowMountExtended(WorkItemNoteActions, {
propsData: {
showReply,
showEdit,
+ workItemIid: '1',
+ note: {},
noteId,
showAwardEmoji,
showAssignUnassign,
@@ -66,21 +63,27 @@ describe('Work Item Note Actions', () => {
projectName,
},
provide: {
+ fullPath: 'gitlab-org',
glFeatures: {
workItemsMvc2: true,
},
},
stubs: {
- EmojiPicker: EmojiPickerStub,
+ GlDisclosureDropdown: stubComponent(GlDisclosureDropdown, {
+ methods: { close: showSpy },
+ }),
},
apolloProvider: createMockApollo([[addAwardEmojiMutation, addEmojiMutationResolver]]),
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
});
- wrapper.vm.$refs.dropdown.close = jest.fn();
};
+ afterEach(() => {
+ showSpy.mockClear();
+ });
+
describe('reply button', () => {
it('is visible by default', () => {
createComponent();
@@ -128,22 +131,6 @@ describe('Work Item Note Actions', () => {
expect(findEmojiButton().exists()).toBe(false);
});
-
- it('commits mutation on click', async () => {
- const awardName = 'carrot';
-
- createComponent();
-
- findEmojiButton().vm.$emit('click', awardName);
-
- await waitForPromises();
-
- expect(findEmojiButton().emitted('errors')).toEqual(undefined);
- expect(addEmojiMutationResolver).toHaveBeenCalledWith({
- awardableId: noteId,
- name: awardName,
- });
- });
});
describe('delete note', () => {
@@ -173,6 +160,7 @@ describe('Work Item Note Actions', () => {
findDeleteNoteButton().vm.$emit('action');
expect(wrapper.emitted('deleteNote')).toEqual([[]]);
+ expect(showSpy).toHaveBeenCalled();
});
});
@@ -188,6 +176,7 @@ describe('Work Item Note Actions', () => {
findCopyLinkButton().vm.$emit('action');
expect(wrapper.emitted('notifyCopyDone')).toEqual([[]]);
+ expect(showSpy).toHaveBeenCalled();
});
});
@@ -214,6 +203,7 @@ describe('Work Item Note Actions', () => {
findAssignUnassignButton().vm.$emit('action');
expect(wrapper.emitted('assignUser')).toEqual([[]]);
+ expect(showSpy).toHaveBeenCalled();
});
});
@@ -240,6 +230,7 @@ describe('Work Item Note Actions', () => {
findReportAbuseToAdminButton().vm.$emit('action');
expect(wrapper.emitted('reportAbuse')).toEqual([[]]);
+ expect(showSpy).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
new file mode 100644
index 00000000000..d425f1e50dc
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
@@ -0,0 +1,147 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import mockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { __ } from '~/locale';
+import AwardsList from '~/vue_shared/components/awards_list.vue';
+import WorkItemNoteAwardsList from '~/work_items/components/notes/work_item_note_awards_list.vue';
+import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
+import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
+import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
+import {
+ mockWorkItemNotesResponseWithComments,
+ mockAwardEmojiThumbsUp,
+} from 'jest/work_items/mock_data';
+import { EMOJI_THUMBSUP, EMOJI_THUMBSDOWN } from '~/work_items/constants';
+
+Vue.use(VueApollo);
+
+describe('Work Item Note Awards List', () => {
+ let wrapper;
+ const workItem = mockWorkItemNotesResponseWithComments.data.workspace.workItems.nodes[0];
+ const firstNote = workItem.widgets.find((w) => w.type === 'NOTES').discussions.nodes[0].notes
+ .nodes[0];
+ const fullPath = 'test-project-path';
+ const workItemIid = workItem.iid;
+ const currentUserId = getIdFromGraphQLId(mockAwardEmojiThumbsUp.user.id);
+
+ const addAwardEmojiMutationSuccessHandler = jest.fn().mockResolvedValue({
+ data: {
+ awardEmojiAdd: {
+ errors: [],
+ },
+ },
+ });
+ const removeAwardEmojiMutationSuccessHandler = jest.fn().mockResolvedValue({
+ data: {
+ awardEmojiRemove: {
+ errors: [],
+ },
+ },
+ });
+
+ const findAwardsList = () => wrapper.findComponent(AwardsList);
+
+ const createComponent = ({
+ note = firstNote,
+ addAwardEmojiMutationHandler = addAwardEmojiMutationSuccessHandler,
+ removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler,
+ } = {}) => {
+ const apolloProvider = mockApollo([
+ [addAwardEmojiMutation, addAwardEmojiMutationHandler],
+ [removeAwardEmojiMutation, removeAwardEmojiMutationHandler],
+ ]);
+
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: workItemNotesByIidQuery,
+ variables: { fullPath, iid: workItemIid },
+ ...mockWorkItemNotesResponseWithComments,
+ });
+
+ wrapper = shallowMount(WorkItemNoteAwardsList, {
+ provide: {
+ fullPath,
+ },
+ propsData: {
+ workItemIid,
+ note,
+ isModal: false,
+ },
+ apolloProvider,
+ });
+ };
+
+ beforeEach(() => {
+ window.gon.current_user_id = currentUserId;
+ });
+
+ describe('when not editing', () => {
+ it.each([true, false])('passes emoji permission to awards-list', (hasAwardEmojiPermission) => {
+ const note = {
+ ...firstNote,
+ userPermissions: {
+ ...firstNote.userPermissions,
+ awardEmoji: hasAwardEmojiPermission,
+ },
+ };
+ createComponent({ note });
+
+ expect(findAwardsList().props('canAwardEmoji')).toBe(hasAwardEmojiPermission);
+ });
+
+ it('adds award if not already awarded', async () => {
+ createComponent();
+ await waitForPromises();
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
+
+ expect(addAwardEmojiMutationSuccessHandler).toHaveBeenCalledWith({
+ awardableId: firstNote.id,
+ name: EMOJI_THUMBSUP,
+ });
+ });
+
+ it('emits error if awarding emoji fails', async () => {
+ createComponent({
+ addAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no'),
+ });
+ await waitForPromises();
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[__('Failed to add emoji. Please try again')]]);
+ });
+
+ it('removes award if already awarded', async () => {
+ const removeAwardEmojiMutationHandler = removeAwardEmojiMutationSuccessHandler;
+
+ createComponent({ removeAwardEmojiMutationHandler });
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN);
+
+ await waitForPromises();
+
+ expect(removeAwardEmojiMutationHandler).toHaveBeenCalledWith({
+ awardableId: firstNote.id,
+ name: EMOJI_THUMBSDOWN,
+ });
+ });
+
+ it('restores award if remove fails', async () => {
+ createComponent({
+ removeAwardEmojiMutationHandler: jest.fn().mockRejectedValue('oh no'),
+ });
+ await waitForPromises();
+
+ findAwardsList().vm.$emit('award', EMOJI_THUMBSDOWN);
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[__('Failed to remove emoji. Please try again')]]);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js
index 8dbd2818fc5..c5d1decfb42 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -1,11 +1,13 @@
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { GlAvatarLink } from '@gitlab/ui';
import mockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { updateDraft, clearDraft } from '~/lib/utils/autosave';
import EditedAt from '~/issues/show/components/edited.vue';
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
+import WorkItemNoteAwardsList from '~/work_items/components/notes/work_item_note_awards_list.vue';
import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
@@ -76,6 +78,7 @@ describe('Work Item Note', () => {
const errorHandler = jest.fn().mockRejectedValue('Oops');
+ const findAwardsList = () => wrapper.findComponent(WorkItemNoteAwardsList);
const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
const findNoteHeader = () => wrapper.findComponent(NoteHeader);
const findNoteBody = () => wrapper.findComponent(NoteBody);
@@ -148,6 +151,13 @@ describe('Work Item Note', () => {
expect(findCommentForm().exists()).toBe(false);
expect(findNoteWrapper().exists()).toBe(true);
});
+
+ it('should show the awards list when in edit mode', async () => {
+ createComponent({ note: mockWorkItemCommentNote, workItemsMvc2: true });
+ findNoteActions().vm.$emit('startEditing');
+ await nextTick();
+ expect(findAwardsList().exists()).toBe(true);
+ });
});
describe('when submitting a form to edit a note', () => {
@@ -264,6 +274,19 @@ describe('Work Item Note', () => {
createComponent();
});
+ it('should show avatar link with popover support', () => {
+ const avatarLink = findTimelineEntryItem().findComponent(GlAvatarLink);
+ const { author } = mockWorkItemCommentNote;
+
+ expect(avatarLink.exists()).toBe(true);
+ expect(avatarLink.classes()).toContain('js-user-link');
+ expect(avatarLink.attributes()).toMatchObject({
+ href: author.webUrl,
+ 'data-user-id': '1',
+ 'data-username': `${author.username}`,
+ });
+ });
+
it('should have the note header, actions and body', () => {
expect(findTimelineEntryItem().exists()).toBe(true);
expect(findNoteHeader().exists()).toBe(true);
@@ -404,5 +427,12 @@ describe('Work Item Note', () => {
});
});
});
+
+ it('passes note props to awards list', () => {
+ createComponent({ note: mockWorkItemCommentNote, workItemsMvc2: true });
+
+ expect(findAwardsList().props('note')).toBe(mockWorkItemCommentNote);
+ expect(findAwardsList().props('workItemIid')).toBe('1');
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index 94d47bfb3be..ff1998ab2ed 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -274,14 +274,14 @@ describe('WorkItemAssignees component', () => {
});
describe('when assigning to current user', () => {
- it('does not show `Assign myself` button if current user is loading', () => {
+ it('does not show `Assign yourself` button if current user is loading', () => {
createComponent();
findTokenSelector().trigger('mouseover');
expect(findAssignSelfButton().exists()).toBe(false);
});
- it('does not show `Assign myself` button if work item has assignees', async () => {
+ it('does not show `Assign yourself` button if work item has assignees', async () => {
createComponent();
await waitForPromises();
findTokenSelector().trigger('mouseover');
@@ -289,7 +289,7 @@ describe('WorkItemAssignees component', () => {
expect(findAssignSelfButton().exists()).toBe(false);
});
- it('does now show `Assign myself` button if user is not logged in', async () => {
+ it('does now show `Assign yourself` button if user is not logged in', async () => {
createComponent({ currentUserQueryHandler: noCurrentUserQueryHandler, assignees: [] });
await waitForPromises();
findTokenSelector().trigger('mouseover');
@@ -304,7 +304,7 @@ describe('WorkItemAssignees component', () => {
return waitForPromises();
});
- it('renders `Assign myself` button', () => {
+ it('renders `Assign yourself` button', () => {
findTokenSelector().trigger('mouseover');
expect(findAssignSelfButton().exists()).toBe(true);
});
diff --git a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
new file mode 100644
index 00000000000..ba9af7b2b68
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
@@ -0,0 +1,107 @@
+import { shallowMount } from '@vue/test-utils';
+import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
+import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
+import WorkItemState from '~/work_items/components/work_item_state.vue';
+import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
+import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
+
+import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
+import { workItemResponseFactory } from '../mock_data';
+
+describe('WorkItemAttributesWrapper component', () => {
+ let wrapper;
+
+ const workItemQueryResponse = workItemResponseFactory({ canUpdate: true, canDelete: true });
+
+ const findWorkItemState = () => wrapper.findComponent(WorkItemState);
+ const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
+ const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
+ const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
+ const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestone);
+
+ const createComponent = ({ workItem = workItemQueryResponse.data.workItem } = {}) => {
+ wrapper = shallowMount(WorkItemAttributesWrapper, {
+ propsData: {
+ workItem,
+ },
+ provide: {
+ hasIssueWeightsFeature: true,
+ hasIterationsFeature: true,
+ hasOkrsFeature: true,
+ hasIssuableHealthStatusFeature: true,
+ projectNamespace: 'namespace',
+ fullPath: 'group/project',
+ },
+ stubs: {
+ WorkItemWeight: true,
+ WorkItemIteration: true,
+ WorkItemHealthStatus: true,
+ },
+ });
+ };
+
+ describe('work item state', () => {
+ it('renders the work item state', () => {
+ createComponent();
+
+ expect(findWorkItemState().exists()).toBe(true);
+ });
+ });
+
+ describe('assignees widget', () => {
+ it('renders assignees component when widget is returned from the API', () => {
+ createComponent();
+
+ expect(findWorkItemAssignees().exists()).toBe(true);
+ });
+
+ it('does not render assignees component when widget is not returned from the API', () => {
+ createComponent({
+ workItem: workItemResponseFactory({ assigneesWidgetPresent: false }).data.workItem,
+ });
+
+ expect(findWorkItemAssignees().exists()).toBe(false);
+ });
+ });
+
+ describe('labels widget', () => {
+ it.each`
+ description | labelsWidgetPresent | exists
+ ${'renders when widget is returned from API'} | ${true} | ${true}
+ ${'does not render when widget is not returned from API'} | ${false} | ${false}
+ `('$description', ({ labelsWidgetPresent, exists }) => {
+ const response = workItemResponseFactory({ labelsWidgetPresent });
+ createComponent({ workItem: response.data.workItem });
+
+ expect(findWorkItemLabels().exists()).toBe(exists);
+ });
+ });
+
+ describe('dates widget', () => {
+ describe.each`
+ description | datesWidgetPresent | exists
+ ${'when widget is returned from API'} | ${true} | ${true}
+ ${'when widget is not returned from API'} | ${false} | ${false}
+ `('$description', ({ datesWidgetPresent, exists }) => {
+ it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, () => {
+ const response = workItemResponseFactory({ datesWidgetPresent });
+ createComponent({ workItem: response.data.workItem });
+
+ expect(findWorkItemDueDate().exists()).toBe(exists);
+ });
+ });
+ });
+
+ describe('milestone widget', () => {
+ it.each`
+ description | milestoneWidgetPresent | exists
+ ${'renders when widget is returned from API'} | ${true} | ${true}
+ ${'does not render when widget is not returned from API'} | ${false} | ${false}
+ `('$description', ({ milestoneWidgetPresent, exists }) => {
+ const response = workItemResponseFactory({ milestoneWidgetPresent });
+ createComponent({ workItem: response.data.workItem });
+
+ expect(findWorkItemMilestone().exists()).toBe(exists);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_award_emoji_spec.js b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
index 82be6d990e4..f8c5f8edc4c 100644
--- a/spec/frontend/work_items/components/work_item_award_emoji_spec.js
+++ b/spec/frontend/work_items/components/work_item_award_emoji_spec.js
@@ -1,4 +1,4 @@
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
@@ -9,36 +9,67 @@ import { isLoggedIn } from '~/lib/utils/common_utils';
import AwardList from '~/vue_shared/components/awards_list.vue';
import WorkItemAwardEmoji from '~/work_items/components/work_item_award_emoji.vue';
import updateAwardEmojiMutation from '~/work_items/graphql/update_award_emoji.mutation.graphql';
-import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import { EMOJI_THUMBSUP, EMOJI_THUMBSDOWN } from '~/work_items/constants';
+import workItemAwardEmojiQuery from '~/work_items/graphql/award_emoji.query.graphql';
+import {
+ EMOJI_THUMBSUP,
+ EMOJI_THUMBSDOWN,
+ DEFAULT_PAGE_SIZE_EMOJIS,
+ I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR,
+} from '~/work_items/constants';
import {
workItemByIidResponseFactory,
mockAwardsWidget,
mockAwardEmojiThumbsUp,
getAwardEmojiResponse,
+ mockMoreThanDefaultAwardEmojisWidget,
} from '../mock_data';
jest.mock('~/lib/utils/common_utils');
+jest.mock('~/work_items/constants', () => ({
+ ...jest.requireActual('~/work_items/constants'),
+ DEFAULT_PAGE_SIZE_EMOJIS: 5,
+}));
+
Vue.use(VueApollo);
describe('WorkItemAwardEmoji component', () => {
let wrapper;
let mockApolloProvider;
- const errorMessage = 'Failed to update the award';
+ const mutationErrorMessage = 'Failed to update the award';
+
const workItemQueryResponse = workItemByIidResponseFactory();
- const workItemQueryAddAwardEmojiResponse = workItemByIidResponseFactory({
- awardEmoji: { ...mockAwardsWidget, nodes: [mockAwardEmojiThumbsUp] },
- });
- const workItemQueryRemoveAwardEmojiResponse = workItemByIidResponseFactory({
- awardEmoji: { ...mockAwardsWidget, nodes: [] },
- });
+ const mockWorkItem = workItemQueryResponse.data.workspace.workItems.nodes[0];
+
+ const awardEmojiQuerySuccessHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ const awardEmojiQueryEmptyHandler = jest.fn().mockResolvedValue(
+ workItemByIidResponseFactory({
+ awardEmoji: {
+ ...mockAwardsWidget,
+ nodes: [],
+ },
+ }),
+ );
+ const awardEmojiQueryThumbsUpHandler = jest.fn().mockResolvedValue(
+ workItemByIidResponseFactory({
+ awardEmoji: {
+ ...mockAwardsWidget,
+ nodes: [mockAwardEmojiThumbsUp],
+ },
+ }),
+ );
+ const awardEmojiQueryFailureHandler = jest
+ .fn()
+ .mockRejectedValue(new Error(I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR));
+
const awardEmojiAddSuccessHandler = jest.fn().mockResolvedValue(getAwardEmojiResponse(true));
const awardEmojiRemoveSuccessHandler = jest.fn().mockResolvedValue(getAwardEmojiResponse(false));
- const awardEmojiUpdateFailureHandler = jest.fn().mockRejectedValue(new Error(errorMessage));
- const mockWorkItem = workItemQueryResponse.data.workspace.workItems.nodes[0];
- const mockAwardEmojiDifferentUserThumbsUp = {
+ const awardEmojiUpdateFailureHandler = jest
+ .fn()
+ .mockRejectedValue(new Error(mutationErrorMessage));
+
+ const mockAwardEmojiDifferentUser = {
name: 'thumbsup',
__typename: 'AwardEmoji',
user: {
@@ -49,35 +80,37 @@ describe('WorkItemAwardEmoji component', () => {
};
const createComponent = ({
- awardMutationHandler = awardEmojiAddSuccessHandler,
- workItem = mockWorkItem,
+ awardEmojiQueryHandler = awardEmojiQuerySuccessHandler,
+ awardEmojiMutationHandler = awardEmojiAddSuccessHandler,
workItemIid = '1',
- awardEmoji = { ...mockAwardsWidget, nodes: [] },
} = {}) => {
- mockApolloProvider = createMockApollo([[updateAwardEmojiMutation, awardMutationHandler]]);
-
- mockApolloProvider.clients.defaultClient.writeQuery({
- query: workItemByIidQuery,
- variables: { fullPath: workItem.project.fullPath, iid: workItemIid },
- data: {
- ...workItemQueryResponse.data,
- workspace: {
- __typename: 'Project',
- id: 'gid://gitlab/Project/1',
- workItems: {
- nodes: [workItem],
+ mockApolloProvider = createMockApollo(
+ [
+ [workItemAwardEmojiQuery, awardEmojiQueryHandler],
+ [updateAwardEmojiMutation, awardEmojiMutationHandler],
+ ],
+ {},
+ {
+ typePolicies: {
+ WorkItemWidgetAwardEmoji: {
+ fields: {
+ // If we add any key args, the awardEmoji field becomes awardEmoji({"first":10}) and
+ // kills any possibility to handle it on the widget level without hardcoding a string.
+ awardEmoji: {
+ keyArgs: false,
+ },
+ },
},
},
},
- });
+ );
wrapper = shallowMount(WorkItemAwardEmoji, {
isLoggedIn: isLoggedIn(),
apolloProvider: mockApolloProvider,
propsData: {
- workItemId: workItem.id,
- workItemFullpath: workItem.project.fullPath,
- awardEmoji,
+ workItemId: 'gid://gitlab/WorkItem/1',
+ workItemFullpath: 'test-project-path',
workItemIid,
},
});
@@ -85,17 +118,23 @@ describe('WorkItemAwardEmoji component', () => {
const findAwardsList = () => wrapper.findComponent(AwardList);
- beforeEach(() => {
+ beforeEach(async () => {
isLoggedIn.mockReturnValue(true);
window.gon = {
current_user_id: 5,
current_user_fullname: 'Dave Smith',
};
- createComponent();
+ await createComponent();
});
- it('renders the award-list component with default props', () => {
+ it('renders the award-list component with default props', async () => {
+ createComponent({
+ awardEmojiQueryHandler: awardEmojiQueryEmptyHandler,
+ });
+
+ await waitForPromises();
+
expect(findAwardsList().exists()).toBe(true);
expect(findAwardsList().props()).toEqual({
boundary: '',
@@ -108,8 +147,6 @@ describe('WorkItemAwardEmoji component', () => {
});
it('renders awards-list component with awards present', () => {
- createComponent({ awardEmoji: mockAwardsWidget });
-
expect(findAwardsList().props('awards')).toEqual([
{
name: EMOJI_THUMBSUP,
@@ -128,13 +165,32 @@ describe('WorkItemAwardEmoji component', () => {
]);
});
- it('renders awards list given by multiple users', () => {
+ it('emits error when there is an error while fetching award emojis', async () => {
createComponent({
+ awardEmojiQueryHandler: awardEmojiQueryFailureHandler,
+ });
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[I18N_WORK_ITEM_FETCH_AWARD_EMOJI_ERROR]]);
+ });
+
+ it('renders awards list given by multiple users', async () => {
+ const mockWorkItemAwardEmojiDifferentUser = workItemByIidResponseFactory({
awardEmoji: {
...mockAwardsWidget,
- nodes: [mockAwardEmojiThumbsUp, mockAwardEmojiDifferentUserThumbsUp],
+ nodes: [mockAwardEmojiThumbsUp, mockAwardEmojiDifferentUser],
},
});
+ const awardEmojiWithDifferentUsersQueryHandler = jest
+ .fn()
+ .mockResolvedValue(mockWorkItemAwardEmojiDifferentUser);
+
+ createComponent({
+ awardEmojiQueryHandler: awardEmojiWithDifferentUsersQueryHandler,
+ });
+
+ await waitForPromises();
expect(findAwardsList().props('awards')).toEqual([
{
@@ -155,21 +211,19 @@ describe('WorkItemAwardEmoji component', () => {
});
it.each`
- expectedAssertion | awardEmojiMutationHandler | mockAwardEmojiNodes | workItem
- ${'added'} | ${awardEmojiAddSuccessHandler} | ${[]} | ${workItemQueryRemoveAwardEmojiResponse.data.workspace.workItems.nodes[0]}
- ${'removed'} | ${awardEmojiRemoveSuccessHandler} | ${[mockAwardEmojiThumbsUp]} | ${workItemQueryAddAwardEmojiResponse.data.workspace.workItems.nodes[0]}
+ expectedAssertion | awardEmojiMutationHandler | awardEmojiQueryHandler
+ ${'added'} | ${awardEmojiAddSuccessHandler} | ${awardEmojiQueryEmptyHandler}
+ ${'removed'} | ${awardEmojiRemoveSuccessHandler} | ${awardEmojiQueryThumbsUpHandler}
`(
'calls mutation when an award emoji is $expectedAssertion',
- ({ awardEmojiMutationHandler, mockAwardEmojiNodes, workItem }) => {
+ async ({ awardEmojiMutationHandler, awardEmojiQueryHandler }) => {
createComponent({
- awardMutationHandler: awardEmojiMutationHandler,
- awardEmoji: {
- ...mockAwardsWidget,
- nodes: mockAwardEmojiNodes,
- },
- workItem,
+ awardEmojiMutationHandler,
+ awardEmojiQueryHandler,
});
+ await waitForPromises();
+
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
expect(awardEmojiMutationHandler).toHaveBeenCalledWith({
@@ -183,21 +237,24 @@ describe('WorkItemAwardEmoji component', () => {
it('emits error when the update mutation fails', async () => {
createComponent({
- awardMutationHandler: awardEmojiUpdateFailureHandler,
+ awardEmojiMutationHandler: awardEmojiUpdateFailureHandler,
+ awardEmojiQueryHandler: awardEmojiQueryEmptyHandler,
});
+ await waitForPromises();
+
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
await waitForPromises();
- expect(wrapper.emitted('error')).toEqual([[errorMessage]]);
+ expect(wrapper.emitted('error')).toEqual([[mutationErrorMessage]]);
});
describe('when user is not logged in', () => {
- beforeEach(() => {
+ beforeEach(async () => {
isLoggedIn.mockReturnValue(false);
- createComponent();
+ await createComponent();
});
it('renders the component with required props and canAwardEmoji false', () => {
@@ -213,15 +270,13 @@ describe('WorkItemAwardEmoji component', () => {
};
});
- it('calls mutation succesfully and adds the award emoji with proper user details', () => {
+ it('calls mutation succesfully and adds the award emoji with proper user details', async () => {
createComponent({
- awardMutationHandler: awardEmojiAddSuccessHandler,
- awardEmoji: {
- ...mockAwardsWidget,
- nodes: [mockAwardEmojiThumbsUp],
- },
+ awardEmojiMutationHandler: awardEmojiAddSuccessHandler,
});
+ await waitForPromises();
+
findAwardsList().vm.$emit('award', EMOJI_THUMBSUP);
expect(awardEmojiAddSuccessHandler).toHaveBeenCalledWith({
@@ -232,4 +287,62 @@ describe('WorkItemAwardEmoji component', () => {
});
});
});
+
+ describe('pagination', () => {
+ describe('when there is no next page', () => {
+ const awardEmojiQuerySingleItemHandler = jest.fn().mockResolvedValue(
+ workItemByIidResponseFactory({
+ awardEmoji: {
+ ...mockAwardsWidget,
+ nodes: [mockAwardEmojiThumbsUp],
+ },
+ }),
+ );
+
+ it('fetch more award emojis should not be called', async () => {
+ createComponent({ awardEmojiQueryHandler: awardEmojiQuerySingleItemHandler });
+ await waitForPromises();
+
+ expect(awardEmojiQuerySingleItemHandler).toHaveBeenCalledWith({
+ fullPath: 'test-project-path',
+ iid: '1',
+ pageSize: DEFAULT_PAGE_SIZE_EMOJIS,
+ after: undefined,
+ });
+ expect(awardEmojiQuerySingleItemHandler).toHaveBeenCalledTimes(1);
+ });
+ });
+
+ describe('when there is next page', () => {
+ const awardEmojisQueryMoreThanDefaultHandler = jest.fn().mockResolvedValueOnce(
+ workItemByIidResponseFactory({
+ awardEmoji: mockMoreThanDefaultAwardEmojisWidget,
+ }),
+ );
+
+ it('fetch more award emojis should be called', async () => {
+ createComponent({
+ awardEmojiQueryHandler: awardEmojisQueryMoreThanDefaultHandler,
+ });
+ await waitForPromises();
+
+ expect(awardEmojisQueryMoreThanDefaultHandler).toHaveBeenCalledWith({
+ fullPath: 'test-project-path',
+ iid: '1',
+ pageSize: DEFAULT_PAGE_SIZE_EMOJIS,
+ after: 'endCursor',
+ });
+
+ await nextTick();
+
+ expect(awardEmojisQueryMoreThanDefaultHandler).toHaveBeenCalledWith({
+ fullPath: 'test-project-path',
+ iid: '1',
+ pageSize: DEFAULT_PAGE_SIZE_EMOJIS,
+ after: mockMoreThanDefaultAwardEmojisWidget.pageInfo.endCursor,
+ });
+ expect(awardEmojisQueryMoreThanDefaultHandler).toHaveBeenCalledTimes(2);
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index b910e9854f8..8b9963b2476 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -12,14 +12,12 @@ import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
-import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
import {
updateWorkItemMutationResponse,
workItemByIidResponseFactory,
- workItemDescriptionSubscriptionResponse,
workItemQueryResponse,
} from '../mock_data';
@@ -34,7 +32,6 @@ describe('WorkItemDescription', () => {
Vue.use(VueApollo);
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
- const subscriptionHandler = jest.fn().mockResolvedValue(workItemDescriptionSubscriptionResponse);
let workItemResponseHandler;
const findForm = () => wrapper.findComponent(GlForm);
@@ -63,7 +60,6 @@ describe('WorkItemDescription', () => {
apolloProvider: createMockApollo([
[workItemByIidQuery, workItemResponseHandler],
[updateWorkItemMutation, mutationHandler],
- [workItemDescriptionSubscription, subscriptionHandler],
]),
propsData: {
workItemId: id,
@@ -83,14 +79,6 @@ describe('WorkItemDescription', () => {
}
};
- it('has a subscription', async () => {
- await createComponent();
-
- expect(subscriptionHandler).toHaveBeenCalledWith({
- issuableId: workItemQueryResponse.data.workItem.id,
- });
- });
-
describe('editing description', () => {
it('passes correct autocompletion data and preview markdown sources and enables quick actions', async () => {
const {
@@ -103,7 +91,6 @@ describe('WorkItemDescription', () => {
expect(findMarkdownEditor().props()).toMatchObject({
supportsQuickActions: true,
renderMarkdownPath: markdownPreviewPath(fullPath, iid),
- quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
autocompleteDataSources: autocompleteDataSources(fullPath, iid),
});
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index d8ba8ea74f2..7ceae935d2d 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -5,10 +5,11 @@ import {
GlSkeletonLoader,
GlButton,
GlEmptyState,
+ GlIntersectionObserver,
} from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { isLoggedIn } from '~/lib/utils/common_utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -18,12 +19,8 @@ import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
-import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
-import WorkItemState from '~/work_items/components/work_item_state.vue';
+import WorkItemAttributesWrapper from '~/work_items/components/work_item_attributes_wrapper.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
-import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
-import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
-import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
@@ -31,20 +28,13 @@ import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_sel
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
import { i18n } from '~/work_items/constants';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import workItemDatesSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql';
-import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
-import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
-import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
+import workItemUpdatedSubscription from '~/work_items/graphql/work_item_updated.subscription.graphql';
import {
mockParent,
- workItemDatesSubscriptionResponse,
workItemByIidResponseFactory,
- workItemTitleSubscriptionResponse,
- workItemAssigneesSubscriptionResponse,
- workItemMilestoneSubscriptionResponse,
objectiveType,
mockWorkItemCommentNote,
} from '../mock_data';
@@ -63,16 +53,11 @@ describe('WorkItemDetail component', () => {
canDelete: true,
});
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
- const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
- const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
- const milestoneSubscriptionHandler = jest
- .fn()
- .mockResolvedValue(workItemMilestoneSubscriptionResponse);
- const assigneesSubscriptionHandler = jest
- .fn()
- .mockResolvedValue(workItemAssigneesSubscriptionResponse);
const showModalHandler = jest.fn();
const { id } = workItemQueryResponse.data.workspace.workItems.nodes[0];
+ const workItemUpdatedSubscriptionHandler = jest
+ .fn()
+ .mockResolvedValue({ data: { workItemUpdated: null } });
const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
@@ -81,42 +66,39 @@ describe('WorkItemDetail component', () => {
const findWorkItemActions = () => wrapper.findComponent(WorkItemActions);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
const findCreatedUpdated = () => wrapper.findComponent(WorkItemCreatedUpdated);
- const findWorkItemState = () => wrapper.findComponent(WorkItemState);
const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
- const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
- const findWorkItemAssignees = () => wrapper.findComponent(WorkItemAssignees);
- const findWorkItemLabels = () => wrapper.findComponent(WorkItemLabels);
- const findWorkItemMilestone = () => wrapper.findComponent(WorkItemMilestone);
- const findParent = () => wrapper.find('[data-testid="work-item-parent"]');
+ const findWorkItemAttributesWrapper = () => wrapper.findComponent(WorkItemAttributesWrapper);
+ const findParent = () => wrapper.findByTestId('work-item-parent');
const findParentButton = () => findParent().findComponent(GlButton);
- const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]');
- const findWorkItemType = () => wrapper.find('[data-testid="work-item-type"]');
+ const findCloseButton = () => wrapper.findByTestId('work-item-close');
+ const findWorkItemType = () => wrapper.findByTestId('work-item-type');
const findHierarchyTree = () => wrapper.findComponent(WorkItemTree);
const findNotesWidget = () => wrapper.findComponent(WorkItemNotes);
const findModal = () => wrapper.findComponent(WorkItemDetailModal);
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
const findWorkItemTodos = () => wrapper.findComponent(WorkItemTodos);
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const findStickyHeader = () => wrapper.findByTestId('work-item-sticky-header');
+ const findWorkItemTwoColumnViewContainer = () => wrapper.findByTestId('work-item-overview');
+ const findRightSidebar = () => wrapper.findByTestId('work-item-overview-right-sidebar');
+ const triggerPageScroll = () => findIntersectionObserver().vm.$emit('disappear');
const createComponent = ({
isModal = false,
updateInProgress = false,
workItemIid = '1',
handler = successHandler,
- subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
error = undefined,
workItemsMvc2Enabled = false,
} = {}) => {
const handlers = [
[workItemByIidQuery, handler],
- [workItemTitleSubscription, subscriptionHandler],
- [workItemDatesSubscription, datesSubscriptionHandler],
- [workItemAssigneesSubscription, assigneesSubscriptionHandler],
- [workItemMilestoneSubscription, milestoneSubscriptionHandler],
+ [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler],
confidentialityMock,
];
- wrapper = shallowMount(WorkItemDetail, {
+ wrapper = shallowMountExtended(WorkItemDetail, {
apolloProvider: createMockApollo(handlers),
isLoggedIn: isLoggedIn(),
propsData: {
@@ -163,13 +145,18 @@ describe('WorkItemDetail component', () => {
});
describe('when there is no `workItemIid` prop', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent({ workItemIid: null });
+ await waitForPromises();
});
it('skips the work item query', () => {
expect(successHandler).not.toHaveBeenCalled();
});
+
+ it('skips the work item updated subscription', () => {
+ expect(workItemUpdatedSubscriptionHandler).not.toHaveBeenCalled();
+ });
});
describe('when loading', () => {
@@ -179,7 +166,6 @@ describe('WorkItemDetail component', () => {
it('renders skeleton loader', () => {
expect(findSkeleton().exists()).toBe(true);
- expect(findWorkItemState().exists()).toBe(false);
expect(findWorkItemTitle().exists()).toBe(false);
});
});
@@ -192,7 +178,6 @@ describe('WorkItemDetail component', () => {
it('does not render skeleton', () => {
expect(findSkeleton().exists()).toBe(false);
- expect(findWorkItemState().exists()).toBe(true);
expect(findWorkItemTitle().exists()).toBe(true);
});
@@ -203,6 +188,10 @@ describe('WorkItemDetail component', () => {
it('renders todos widget if logged in', () => {
expect(findWorkItemTodos().exists()).toBe(true);
});
+
+ it('calls the work item updated subscription', () => {
+ expect(workItemUpdatedSubscriptionHandler).toHaveBeenCalledWith({ id });
+ });
});
describe('close button', () => {
@@ -488,159 +477,6 @@ describe('WorkItemDetail component', () => {
expect(findAlert().text()).toBe(updateError);
});
- describe('subscriptions', () => {
- it('calls the title subscription', async () => {
- createComponent();
- await waitForPromises();
-
- expect(titleSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id });
- });
-
- describe('assignees subscription', () => {
- describe('when the assignees widget exists', () => {
- it('calls the assignees subscription', async () => {
- createComponent();
- await waitForPromises();
-
- expect(assigneesSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id });
- });
- });
-
- describe('when the assignees widget does not exist', () => {
- it('does not call the assignees subscription', async () => {
- const response = workItemByIidResponseFactory({ assigneesWidgetPresent: false });
- const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
- await waitForPromises();
-
- expect(assigneesSubscriptionHandler).not.toHaveBeenCalled();
- });
- });
- });
-
- describe('dates subscription', () => {
- describe('when the due date widget exists', () => {
- it('calls the dates subscription', async () => {
- createComponent();
- await waitForPromises();
-
- expect(datesSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id });
- });
- });
-
- describe('when the due date widget does not exist', () => {
- it('does not call the dates subscription', async () => {
- const response = workItemByIidResponseFactory({ datesWidgetPresent: false });
- const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
- await waitForPromises();
-
- expect(datesSubscriptionHandler).not.toHaveBeenCalled();
- });
- });
- });
- });
-
- describe('assignees widget', () => {
- it('renders assignees component when widget is returned from the API', async () => {
- createComponent();
- await waitForPromises();
-
- expect(findWorkItemAssignees().exists()).toBe(true);
- });
-
- it('does not render assignees component when widget is not returned from the API', async () => {
- createComponent({
- handler: jest
- .fn()
- .mockResolvedValue(workItemByIidResponseFactory({ assigneesWidgetPresent: false })),
- });
- await waitForPromises();
-
- expect(findWorkItemAssignees().exists()).toBe(false);
- });
- });
-
- describe('labels widget', () => {
- it.each`
- description | labelsWidgetPresent | exists
- ${'renders when widget is returned from API'} | ${true} | ${true}
- ${'does not render when widget is not returned from API'} | ${false} | ${false}
- `('$description', async ({ labelsWidgetPresent, exists }) => {
- const response = workItemByIidResponseFactory({ labelsWidgetPresent });
- const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
- await waitForPromises();
-
- expect(findWorkItemLabels().exists()).toBe(exists);
- });
- });
-
- describe('dates widget', () => {
- describe.each`
- description | datesWidgetPresent | exists
- ${'when widget is returned from API'} | ${true} | ${true}
- ${'when widget is not returned from API'} | ${false} | ${false}
- `('$description', ({ datesWidgetPresent, exists }) => {
- it(`${datesWidgetPresent ? 'renders' : 'does not render'} due date component`, async () => {
- const response = workItemByIidResponseFactory({ datesWidgetPresent });
- const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
- await waitForPromises();
-
- expect(findWorkItemDueDate().exists()).toBe(exists);
- });
- });
-
- it('shows an error message when it emits an `error` event', async () => {
- createComponent();
- await waitForPromises();
- const updateError = 'Failed to update';
-
- findWorkItemDueDate().vm.$emit('error', updateError);
- await waitForPromises();
-
- expect(findAlert().text()).toBe(updateError);
- });
- });
-
- describe('milestone widget', () => {
- it.each`
- description | milestoneWidgetPresent | exists
- ${'renders when widget is returned from API'} | ${true} | ${true}
- ${'does not render when widget is not returned from API'} | ${false} | ${false}
- `('$description', async ({ milestoneWidgetPresent, exists }) => {
- const response = workItemByIidResponseFactory({ milestoneWidgetPresent });
- const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
- await waitForPromises();
-
- expect(findWorkItemMilestone().exists()).toBe(exists);
- });
-
- describe('milestone subscription', () => {
- describe('when the milestone widget exists', () => {
- it('calls the milestone subscription', async () => {
- createComponent();
- await waitForPromises();
-
- expect(milestoneSubscriptionHandler).toHaveBeenCalledWith({ issuableId: id });
- });
- });
-
- describe('when the assignees widget does not exist', () => {
- it('does not call the milestone subscription', async () => {
- const response = workItemByIidResponseFactory({ milestoneWidgetPresent: false });
- const handler = jest.fn().mockResolvedValue(response);
- createComponent({ handler });
- await waitForPromises();
-
- expect(milestoneSubscriptionHandler).not.toHaveBeenCalled();
- });
- });
- });
- });
-
it('calls the work item query', async () => {
createComponent();
await waitForPromises();
@@ -796,4 +632,76 @@ describe('WorkItemDetail component', () => {
expect(findWorkItemTodos().exists()).toBe(false);
});
});
+
+ describe('work item attributes wrapper', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('renders the work item attributes wrapper', () => {
+ expect(findWorkItemAttributesWrapper().exists()).toBe(true);
+ });
+
+ it('shows an error message when it emits an `error` event', async () => {
+ const updateError = 'Failed to update';
+
+ findWorkItemAttributesWrapper().vm.$emit('error', updateError);
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe(updateError);
+ });
+ });
+
+ describe('work item two column view', () => {
+ describe('when `workItemsMvc2Enabled` is false', () => {
+ beforeEach(async () => {
+ createComponent({ workItemsMvc2Enabled: false });
+ await waitForPromises();
+ });
+
+ it('does not have the `work-item-overview` class', () => {
+ expect(findWorkItemTwoColumnViewContainer().classes()).not.toContain('work-item-overview');
+ });
+
+ it('does not have sticky header', () => {
+ expect(findIntersectionObserver().exists()).toBe(false);
+ expect(findStickyHeader().exists()).toBe(false);
+ });
+
+ it('does not have right sidebar', () => {
+ expect(findRightSidebar().exists()).toBe(false);
+ });
+ });
+
+ describe('when `workItemsMvc2Enabled` is true', () => {
+ beforeEach(async () => {
+ createComponent({ workItemsMvc2Enabled: true });
+ await waitForPromises();
+ });
+
+ it('has the `work-item-overview` class', () => {
+ expect(findWorkItemTwoColumnViewContainer().classes()).toContain('work-item-overview');
+ });
+
+ it('does not show sticky header by default', () => {
+ expect(findStickyHeader().exists()).toBe(false);
+ });
+
+ it('has the sticky header when the page is scrolled', async () => {
+ expect(findIntersectionObserver().exists()).toBe(true);
+
+ global.pageYOffset = 100;
+ triggerPageScroll();
+
+ await nextTick();
+
+ expect(findStickyHeader().exists()).toBe(true);
+ });
+
+ it('has the right sidebar', () => {
+ expect(findRightSidebar().exists()).toBe(true);
+ });
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 6894aa236e3..4a20e654060 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -6,7 +6,6 @@ import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
-import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
@@ -16,7 +15,6 @@ import {
mockLabels,
workItemByIidResponseFactory,
updateWorkItemMutationResponse,
- workItemLabelsSubscriptionResponse,
} from '../mock_data';
Vue.use(VueApollo);
@@ -38,7 +36,6 @@ describe('WorkItemLabels component', () => {
const successUpdateWorkItemMutationHandler = jest
.fn()
.mockResolvedValue(updateWorkItemMutationResponse);
- const subscriptionHandler = jest.fn().mockResolvedValue(workItemLabelsSubscriptionResponse);
const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const createComponent = ({
@@ -53,7 +50,6 @@ describe('WorkItemLabels component', () => {
[workItemByIidQuery, workItemQueryHandler],
[labelSearchQuery, searchQueryHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
- [workItemLabelsSubscription, subscriptionHandler],
]),
provide: {
fullPath: 'test-project-path',
@@ -246,16 +242,6 @@ describe('WorkItemLabels component', () => {
expect(updateWorkItemMutationHandler).not.toHaveBeenCalled();
});
-
- it('has a subscription', async () => {
- createComponent();
-
- await waitForPromises();
-
- expect(subscriptionHandler).toHaveBeenCalledWith({
- issuableId: workItemId,
- });
- });
});
it('calls the work item query', async () => {
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
index f3aa347f389..e90775a5240 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
@@ -1,12 +1,10 @@
import { nextTick } from 'vue';
-
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/work_item_children_wrapper.vue';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
import OkrActionsSplitButton from '~/work_items/components/work_item_links/okr_actions_split_button.vue';
-
import {
FORM_TYPES,
WORK_ITEM_TYPE_ENUM_OBJECTIVE,
@@ -42,9 +40,8 @@ describe('WorkItemTree', () => {
children,
canUpdate,
},
+ stubs: { WidgetWrapper },
});
-
- wrapper.vm.$refs.wrapper.show = jest.fn();
};
it('displays Add button', () => {
diff --git a/spec/frontend/work_items/components/work_item_todos_spec.js b/spec/frontend/work_items/components/work_item_todos_spec.js
index 83b61a04298..454bd97bbee 100644
--- a/spec/frontend/work_items/components/work_item_todos_spec.js
+++ b/spec/frontend/work_items/components/work_item_todos_spec.js
@@ -1,14 +1,24 @@
import { GlButton, GlIcon } from '@gitlab/ui';
+
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
-import { ADD, TODO_DONE_ICON, TODO_ADD_ICON } from '~/work_items/constants';
-import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import {
+ TODO_DONE_ICON,
+ TODO_ADD_ICON,
+ TODO_PENDING_STATE,
+ TODO_DONE_STATE,
+} from '~/work_items/constants';
import { updateGlobalTodoCount } from '~/sidebar/utils';
-import { workItemResponseFactory, updateWorkItemMutationResponseFactory } from '../mock_data';
+import createWorkItemTodosMutation from '~/work_items/graphql/create_work_item_todos.mutation.graphql';
+import markDoneWorkItemTodosMutation from '~/work_items/graphql/mark_done_work_item_todos.mutation.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+
+import { workItemResponseFactory, getTodosMutationResponse } from '../mock_data';
jest.mock('~/sidebar/utils');
@@ -22,27 +32,58 @@ describe('WorkItemTodo component', () => {
const errorMessage = 'Failed to add item';
const workItemQueryResponse = workItemResponseFactory({ canUpdate: true });
- const successHandler = jest
+ const mockWorkItemId = workItemQueryResponse.data.workItem.id;
+ const mockWorkItemIid = workItemQueryResponse.data.workItem.iid;
+ const mockWorkItemFullpath = workItemQueryResponse.data.workItem.project.fullPath;
+
+ const createTodoSuccessHandler = jest
.fn()
- .mockResolvedValue(updateWorkItemMutationResponseFactory({ canUpdate: true }));
+ .mockResolvedValue(getTodosMutationResponse(TODO_PENDING_STATE));
+ const markDoneTodoSuccessHandler = jest
+ .fn()
+ .mockResolvedValue(getTodosMutationResponse(TODO_DONE_STATE));
const failureHandler = jest.fn().mockRejectedValue(new Error(errorMessage));
- const inputVariables = {
- id: 'gid://gitlab/WorkItem/1',
- currentUserTodosWidget: {
- action: ADD,
- },
+ const inputVariablesCreateTodos = {
+ targetId: 'gid://gitlab/WorkItem/1',
+ };
+
+ const inputVariablesMarkDoneTodos = {
+ id: 'gid://gitlab/Todo/1',
+ };
+
+ const mockCurrentUserTodos = {
+ id: 'gid://gitlab/Todo/1',
};
const createComponent = ({
- currentUserTodosMock = [updateWorkItemMutation, successHandler],
+ mutation = createWorkItemTodosMutation,
+ currentUserTodosHandler = createTodoSuccessHandler,
currentUserTodos = [],
} = {}) => {
- const handlers = [currentUserTodosMock];
+ const mockApolloProvider = createMockApollo([[mutation, currentUserTodosHandler]]);
+
+ mockApolloProvider.clients.defaultClient.cache.writeQuery({
+ query: workItemByIidQuery,
+ variables: { fullPath: mockWorkItemFullpath, iid: mockWorkItemIid },
+ data: {
+ ...workItemQueryResponse.data,
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/1',
+ workItems: {
+ nodes: [workItemQueryResponse.data.workItem],
+ },
+ },
+ },
+ });
+
wrapper = shallowMountExtended(WorkItemTodos, {
- apolloProvider: createMockApollo(handlers),
+ apolloProvider: mockApolloProvider,
propsData: {
- workItem: workItemQueryResponse.data.workItem,
+ workItemId: mockWorkItemId,
+ workItemIid: mockWorkItemIid,
+ workItemFullpath: mockWorkItemFullpath,
currentUserTodos,
},
});
@@ -58,35 +99,41 @@ describe('WorkItemTodo component', () => {
it('renders mark as done button when there is pending item', () => {
createComponent({
- currentUserTodos: [
- {
- node: {
- id: 'gid://gitlab/Todo/1',
- state: 'pending',
- },
- },
- ],
+ currentUserTodos: [mockCurrentUserTodos],
});
expect(findTodoIcon().props('name')).toEqual(TODO_DONE_ICON);
expect(findTodoIcon().classes('gl-fill-blue-500')).toBe(true);
});
- it('calls update mutation when to do button is clicked', async () => {
- createComponent();
+ it.each`
+ assertionName | mutation | currentUserTodosHandler | currentUserTodos | inputVariables
+ ${'create'} | ${createWorkItemTodosMutation} | ${createTodoSuccessHandler} | ${[]} | ${inputVariablesCreateTodos}
+ ${'mark done'} | ${markDoneWorkItemTodosMutation} | ${markDoneTodoSuccessHandler} | ${[mockCurrentUserTodos]} | ${inputVariablesMarkDoneTodos}
+ `(
+ 'calls $assertionName todos mutation when to do button is toggled',
+ async ({ mutation, currentUserTodosHandler, currentUserTodos, inputVariables }) => {
+ createComponent({
+ mutation,
+ currentUserTodosHandler,
+ currentUserTodos,
+ });
- findTodoWidget().vm.$emit('click');
+ findTodoWidget().vm.$emit('click');
- await waitForPromises();
+ await waitForPromises();
- expect(successHandler).toHaveBeenCalledWith({
- input: inputVariables,
- });
- expect(updateGlobalTodoCount).toHaveBeenCalled();
- });
+ expect(currentUserTodosHandler).toHaveBeenCalledWith({
+ input: inputVariables,
+ });
+ expect(updateGlobalTodoCount).toHaveBeenCalled();
+ },
+ );
it('emits error when the update mutation fails', async () => {
- createComponent({ currentUserTodosMock: [updateWorkItemMutation, failureHandler] });
+ createComponent({
+ currentUserTodosHandler: failureHandler,
+ });
findTodoWidget().vm.$emit('click');
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index a873462ea63..f88e69a7ffe 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -68,6 +68,38 @@ export const mockAwardEmojiThumbsDown = {
export const mockAwardsWidget = {
nodes: [mockAwardEmojiThumbsUp, mockAwardEmojiThumbsDown],
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: null,
+ __typename: 'PageInfo',
+ },
+ __typename: 'AwardEmojiConnection',
+};
+
+export const mockMoreThanDefaultAwardEmojisWidget = {
+ nodes: [
+ mockAwardEmojiThumbsUp,
+ mockAwardEmojiThumbsDown,
+ { ...mockAwardEmojiThumbsUp, name: 'one' },
+ { ...mockAwardEmojiThumbsUp, name: 'two' },
+ { ...mockAwardEmojiThumbsUp, name: 'three' },
+ { ...mockAwardEmojiThumbsUp, name: 'four' },
+ { ...mockAwardEmojiThumbsUp, name: 'five' },
+ { ...mockAwardEmojiThumbsUp, name: 'six' },
+ { ...mockAwardEmojiThumbsUp, name: 'seven' },
+ { ...mockAwardEmojiThumbsUp, name: 'eight' },
+ { ...mockAwardEmojiThumbsUp, name: 'nine' },
+ { ...mockAwardEmojiThumbsUp, name: 'ten' },
+ ],
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: 'endCursor',
+ __typename: 'PageInfo',
+ },
__typename: 'AwardEmojiConnection',
};
@@ -629,14 +661,10 @@ export const workItemResponseFactory = ({
? {
type: 'CURRENT_USER_TODOS',
currentUserTodos: {
- edges: [
+ nodes: [
{
- node: {
- id: 'gid://gitlab/Todo/1',
- state: 'pending',
- __typename: 'Todo',
- },
- __typename: 'TodoEdge',
+ id: 'gid://gitlab/Todo/1',
+ __typename: 'Todo',
},
],
__typename: 'TodoConnection',
@@ -803,154 +831,6 @@ export const deleteWorkItemMutationErrorResponse = {
},
};
-export const workItemDatesSubscriptionResponse = {
- data: {
- issuableDatesUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetStartAndDueDate',
- dueDate: '2022-12-31',
- startDate: '2022-01-01',
- },
- ],
- },
- },
-};
-
-export const workItemTitleSubscriptionResponse = {
- data: {
- issuableTitleUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- title: 'new title',
- },
- },
-};
-
-export const workItemDescriptionSubscriptionResponse = {
- data: {
- issuableDescriptionUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetDescription',
- type: 'DESCRIPTION',
- description: 'New description',
- descriptionHtml: '<p>New description</p>',
- lastEditedAt: '2022-09-21T06:18:42Z',
- lastEditedBy: {
- id: 'gid://gitlab/User/2',
- name: 'Someone else',
- webPath: '/not-you',
- },
- },
- ],
- },
- },
-};
-
-export const workItemWeightSubscriptionResponse = {
- data: {
- issuableWeightUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetWeight',
- weight: 1,
- },
- ],
- },
- },
-};
-
-export const workItemAssigneesSubscriptionResponse = {
- data: {
- issuableAssigneesUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemAssigneesWeight',
- assignees: {
- nodes: [mockAssignees[0]],
- },
- },
- ],
- },
- },
-};
-
-export const workItemLabelsSubscriptionResponse = {
- data: {
- issuableLabelsUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetLabels',
- type: 'LABELS',
- allowsScopedLabels: false,
- labels: {
- nodes: mockLabels,
- },
- },
- ],
- },
- },
-};
-
-export const workItemIterationSubscriptionResponse = {
- data: {
- issuableIterationUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetIteration',
- iteration: {
- description: 'Iteration description',
- dueDate: '2022-07-29',
- id: 'gid://gitlab/Iteration/1125',
- iid: '95',
- startDate: '2022-06-22',
- title: 'Iteration subcription title',
- },
- },
- ],
- },
- },
-};
-
-export const workItemHealthStatusSubscriptionResponse = {
- data: {
- issuableHealthStatusUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetHealthStatus',
- healthStatus: 'needsAttention',
- },
- ],
- },
- },
-};
-
-export const workItemMilestoneSubscriptionResponse = {
- data: {
- issuableMilestoneUpdated: {
- id: 'gid://gitlab/WorkItem/1',
- widgets: [
- {
- __typename: 'WorkItemWidgetMilestone',
- type: 'MILESTONE',
- milestone: {
- id: 'gid://gitlab/Milestone/1125',
- expired: false,
- title: 'Milestone title',
- },
- },
- ],
- },
- },
-};
-
export const workItemHierarchyEmptyResponse = {
data: {
workspace: {
@@ -2130,6 +2010,9 @@ export const mockWorkItemNotesResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2241,6 +2124,9 @@ export const mockWorkItemNotesByIidResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2294,6 +2180,9 @@ export const mockWorkItemNotesByIidResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2348,6 +2237,9 @@ export const mockWorkItemNotesByIidResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2460,6 +2352,9 @@ export const mockMoreWorkItemNotesResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2513,6 +2408,9 @@ export const mockMoreWorkItemNotesResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2564,6 +2462,9 @@ export const mockMoreWorkItemNotesResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2631,6 +2532,9 @@ export const createWorkItemNoteResponse = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2682,6 +2586,9 @@ export const mockWorkItemCommentNote = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [mockAwardEmojiThumbsDown],
+ },
};
export const mockWorkItemCommentNoteByContributor = {
@@ -2781,6 +2688,9 @@ export const mockWorkItemNotesResponseWithComments = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ awardEmoji: {
+ nodes: [mockAwardEmojiThumbsDown],
+ },
__typename: 'Note',
},
{
@@ -2821,6 +2731,9 @@ export const mockWorkItemNotesResponseWithComments = {
repositionNote: true,
__typename: 'NotePermissions',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2869,6 +2782,9 @@ export const mockWorkItemNotesResponseWithComments = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2945,6 +2861,9 @@ export const workItemNotesCreateSubscriptionResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -2972,6 +2891,9 @@ export const workItemNotesCreateSubscriptionResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
},
@@ -3017,6 +2939,9 @@ export const workItemNotesUpdateSubscriptionResponse = {
webUrl: 'http://127.0.0.1:3000/root',
__typename: 'UserCore',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
},
@@ -3176,6 +3101,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
},
__typename: 'SystemNoteMetadata',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -3239,6 +3167,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
},
__typename: 'SystemNoteMetadata',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -3302,6 +3233,9 @@ export const workItemNotesWithSystemNotesWithChangedDescription = {
},
__typename: 'SystemNoteMetadata',
},
+ awardEmoji: {
+ nodes: [],
+ },
__typename: 'Note',
},
],
@@ -3350,3 +3284,17 @@ export const getAwardEmojiResponse = (toggledOn) => {
},
};
};
+
+export const getTodosMutationResponse = (state) => {
+ return {
+ data: {
+ todoMutation: {
+ todo: {
+ id: 'gid://gitlab/Todo/1',
+ state,
+ },
+ errors: [],
+ },
+ },
+ };
+};
diff --git a/spec/frontend/work_items/notes/award_utils_spec.js b/spec/frontend/work_items/notes/award_utils_spec.js
new file mode 100644
index 00000000000..8ae32ce5f40
--- /dev/null
+++ b/spec/frontend/work_items/notes/award_utils_spec.js
@@ -0,0 +1,109 @@
+import { getMutation, optimisticAwardUpdate } from '~/work_items/notes/award_utils';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import mockApollo from 'helpers/mock_apollo_helper';
+import { __ } from '~/locale';
+import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
+import addAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_add_award_emoji.mutation.graphql';
+import removeAwardEmojiMutation from '~/work_items/graphql/notes/work_item_note_remove_award_emoji.mutation.graphql';
+import {
+ mockWorkItemNotesResponseWithComments,
+ mockAwardEmojiThumbsUp,
+ mockAwardEmojiThumbsDown,
+} from '../mock_data';
+
+function getWorkItem(data) {
+ return data.workspace.workItems.nodes[0];
+}
+function getFirstNote(workItem) {
+ return workItem.widgets.find((w) => w.type === 'NOTES').discussions.nodes[0].notes.nodes[0];
+}
+
+describe('Work item note award utils', () => {
+ const workItem = getWorkItem(mockWorkItemNotesResponseWithComments.data);
+ const firstNote = getFirstNote(workItem);
+ const fullPath = 'test-project-path';
+ const workItemIid = workItem.iid;
+ const currentUserId = getIdFromGraphQLId(mockAwardEmojiThumbsDown.user.id);
+
+ beforeEach(() => {
+ window.gon = { current_user_id: currentUserId };
+ });
+
+ describe('getMutation', () => {
+ it('returns remove mutation when user has already awarded award', () => {
+ const note = firstNote;
+ const { name } = mockAwardEmojiThumbsDown;
+
+ expect(getMutation({ note, name })).toEqual({
+ mutation: removeAwardEmojiMutation,
+ mutationName: 'awardEmojiRemove',
+ errorMessage: __('Failed to remove emoji. Please try again'),
+ });
+ });
+
+ it('returns remove mutation when user has not already awarded award', () => {
+ const note = {};
+ const { name } = mockAwardEmojiThumbsUp;
+
+ expect(getMutation({ note, name })).toEqual({
+ mutation: addAwardEmojiMutation,
+ mutationName: 'awardEmojiAdd',
+ errorMessage: __('Failed to add emoji. Please try again'),
+ });
+ });
+ });
+
+ describe('optimisticAwardUpdate', () => {
+ let apolloProvider;
+ beforeEach(() => {
+ apolloProvider = mockApollo();
+
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: workItemNotesByIidQuery,
+ variables: { fullPath, iid: workItemIid },
+ ...mockWorkItemNotesResponseWithComments,
+ });
+ });
+
+ it('adds new emoji to cache', () => {
+ const note = firstNote;
+ const { name } = mockAwardEmojiThumbsUp;
+
+ const updateFn = optimisticAwardUpdate({ note, name, fullPath, workItemIid });
+
+ updateFn(apolloProvider.clients.defaultClient.cache);
+
+ const updatedResult = apolloProvider.clients.defaultClient.readQuery({
+ query: workItemNotesByIidQuery,
+ variables: { fullPath, iid: workItemIid },
+ });
+
+ const updatedWorkItem = getWorkItem(updatedResult);
+ const updatedNote = getFirstNote(updatedWorkItem);
+
+ expect(updatedNote.awardEmoji.nodes).toEqual([
+ mockAwardEmojiThumbsDown,
+ mockAwardEmojiThumbsUp,
+ ]);
+ });
+
+ it('removes existing emoji from cache', () => {
+ const note = firstNote;
+ const { name } = mockAwardEmojiThumbsDown;
+
+ const updateFn = optimisticAwardUpdate({ note, name, fullPath, workItemIid });
+
+ updateFn(apolloProvider.clients.defaultClient.cache);
+
+ const updatedResult = apolloProvider.clients.defaultClient.readQuery({
+ query: workItemNotesByIidQuery,
+ variables: { fullPath, iid: workItemIid },
+ });
+
+ const updatedWorkItem = getWorkItem(updatedResult);
+ const updatedNote = getFirstNote(updatedWorkItem);
+
+ expect(updatedNote.awardEmoji.nodes).toEqual([]);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index b5d54a7c319..79ba31e7012 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -2,28 +2,14 @@ import { mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
-import {
- currentUserResponse,
- workItemAssigneesSubscriptionResponse,
- workItemDatesSubscriptionResponse,
- workItemByIidResponseFactory,
- workItemTitleSubscriptionResponse,
- workItemLabelsSubscriptionResponse,
- workItemMilestoneSubscriptionResponse,
- workItemDescriptionSubscriptionResponse,
-} from 'jest/work_items/mock_data';
+import { currentUserResponse, workItemByIidResponseFactory } from 'jest/work_items/mock_data';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
import App from '~/work_items/components/app.vue';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import workItemDatesSubscription from '~/graphql_shared/subscriptions/work_item_dates.subscription.graphql';
-import workItemTitleSubscription from '~/work_items/graphql/work_item_title.subscription.graphql';
-import workItemAssigneesSubscription from '~/work_items/graphql/work_item_assignees.subscription.graphql';
-import workItemLabelsSubscription from 'ee_else_ce/work_items/graphql/work_item_labels.subscription.graphql';
-import workItemMilestoneSubscription from '~/work_items/graphql/work_item_milestone.subscription.graphql';
-import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql';
import CreateWorkItem from '~/work_items/pages/create_work_item.vue';
import WorkItemsRoot from '~/work_items/pages/work_item_root.vue';
import { createRouter } from '~/work_items/router';
+import workItemUpdatedSubscription from '~/work_items/graphql/work_item_updated.subscription.graphql';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -34,18 +20,9 @@ describe('Work items router', () => {
const workItemQueryHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory());
const currentUserQueryHandler = jest.fn().mockResolvedValue(currentUserResponse);
- const datesSubscriptionHandler = jest.fn().mockResolvedValue(workItemDatesSubscriptionResponse);
- const titleSubscriptionHandler = jest.fn().mockResolvedValue(workItemTitleSubscriptionResponse);
- const assigneesSubscriptionHandler = jest
+ const workItemUpdatedSubscriptionHandler = jest
.fn()
- .mockResolvedValue(workItemAssigneesSubscriptionResponse);
- const labelsSubscriptionHandler = jest.fn().mockResolvedValue(workItemLabelsSubscriptionResponse);
- const milestoneSubscriptionHandler = jest
- .fn()
- .mockResolvedValue(workItemMilestoneSubscriptionResponse);
- const descriptionSubscriptionHandler = jest
- .fn()
- .mockResolvedValue(workItemDescriptionSubscriptionResponse);
+ .mockResolvedValue({ data: { workItemUpdated: null } });
const createComponent = async (routeArg) => {
const router = createRouter('/work_item');
@@ -56,12 +33,7 @@ describe('Work items router', () => {
const handlers = [
[workItemByIidQuery, workItemQueryHandler],
[currentUserQuery, currentUserQueryHandler],
- [workItemDatesSubscription, datesSubscriptionHandler],
- [workItemTitleSubscription, titleSubscriptionHandler],
- [workItemAssigneesSubscription, assigneesSubscriptionHandler],
- [workItemLabelsSubscription, labelsSubscriptionHandler],
- [workItemMilestoneSubscription, milestoneSubscriptionHandler],
- [workItemDescriptionSubscription, descriptionSubscriptionHandler],
+ [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler],
];
wrapper = mount(App, {
@@ -81,6 +53,7 @@ describe('Work items router', () => {
WorkItemIteration: true,
WorkItemHealthStatus: true,
WorkItemNotes: true,
+ WorkItemAwardEmoji: true,
},
});
};
diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js
index b8af5f10a5a..aa24b80cf08 100644
--- a/spec/frontend/work_items/utils_spec.js
+++ b/spec/frontend/work_items/utils_spec.js
@@ -1,9 +1,4 @@
-import {
- autocompleteDataSources,
- markdownPreviewPath,
- getWorkItemTodoOptimisticResponse,
-} from '~/work_items/utils';
-import { workItemResponseFactory } from './mock_data';
+import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
describe('autocompleteDataSources', () => {
beforeEach(() => {
@@ -30,17 +25,3 @@ describe('markdownPreviewPath', () => {
);
});
});
-
-describe('getWorkItemTodoOptimisticResponse', () => {
- it.each`
- scenario | pendingTodo | result
- ${'empty'} | ${false} | ${0}
- ${'present'} | ${true} | ${1}
- `('returns correct response when pending item list is $scenario', ({ pendingTodo, result }) => {
- const workItem = workItemResponseFactory({ canUpdate: true });
- expect(
- getWorkItemTodoOptimisticResponse({ workItem, pendingTodo }).workItemUpdate.workItem
- .widgets[0].currentUserTodos.edges.length,
- ).toBe(result);
- });
-});