summaryrefslogtreecommitdiff
path: root/spec/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__mocks__/@toast-ui/vue-editor/index.js11
-rw-r--r--spec/frontend/__mocks__/monaco-editor/index.js4
-rw-r--r--spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap50
-rw-r--r--spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js174
-rw-r--r--spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js51
-rw-r--r--spec/frontend/add_context_commits_modal/store/actions_spec.js239
-rw-r--r--spec/frontend/add_context_commits_modal/store/mutations_spec.js156
-rw-r--r--spec/frontend/alert_management/components/alert_details_spec.js (renamed from spec/frontend/alert_management/components/alert_management_detail_spec.js)59
-rw-r--r--spec/frontend/alert_management/components/alert_management_empty_state_spec.js1
-rw-r--r--spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js1
-rw-r--r--spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js57
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js96
-rw-r--r--spec/frontend/alert_management/components/alert_metrics_spec.js2
-rw-r--r--spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js4
-rw-r--r--spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js6
-rw-r--r--spec/frontend/alert_management/mocks/alerts.json9
-rw-r--r--spec/frontend/alert_settings/__snapshots__/alert_settings_form_spec.js.snap16
-rw-r--r--spec/frontend/alert_settings/alert_settings_form_spec.js121
-rw-r--r--spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js2
-rw-r--r--spec/frontend/analytics/components/activity_chart_spec.js39
-rw-r--r--spec/frontend/api_spec.js441
-rw-r--r--spec/frontend/awards_handler_spec.js4
-rw-r--r--spec/frontend/badges/components/badge_form_spec.js6
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js52
-rw-r--r--spec/frontend/batch_comments/components/drafts_count_spec.js4
-rw-r--r--spec/frontend/batch_comments/components/preview_item_spec.js2
-rw-r--r--spec/frontend/batch_comments/components/publish_button_spec.js2
-rw-r--r--spec/frontend/batch_comments/components/publish_dropdown_spec.js2
-rw-r--r--spec/frontend/batch_comments/mock_data.js3
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js2
-rw-r--r--spec/frontend/behaviors/copy_as_gfm_spec.js10
-rw-r--r--spec/frontend/behaviors/gl_emoji_spec.js2
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap10
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap21
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap2
-rw-r--r--spec/frontend/blob/components/blob_content_error_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_content_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_edit_content_spec.js46
-rw-r--r--spec/frontend/blob/components/blob_edit_header_spec.js44
-rw-r--r--spec/frontend/blob/components/blob_embeddable_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_header_default_actions_spec.js6
-rw-r--r--spec/frontend/blob/components/blob_header_viewer_switcher_spec.js2
-rw-r--r--spec/frontend/blob/components/mock_data.js2
-rw-r--r--spec/frontend/blob/notebook/notebook_viever_spec.js2
-rw-r--r--spec/frontend/blob/pipeline_tour_success_modal_spec.js9
-rw-r--r--spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js6
-rw-r--r--spec/frontend/blob/utils_spec.js51
-rw-r--r--spec/frontend/blob/viewer/index_spec.js4
-rw-r--r--spec/frontend/blob_edit/blob_bundle_spec.js2
-rw-r--r--spec/frontend/boards/board_card_spec.js2
-rw-r--r--spec/frontend/boards/components/board_column_spec.js5
-rw-r--r--spec/frontend/boards/components/board_form_spec.js2
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js5
-rw-r--r--spec/frontend/boards/components/board_settings_sidebar_spec.js159
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js4
-rw-r--r--spec/frontend/boards/components/sidebar/remove_issue_spec.js28
-rw-r--r--spec/frontend/boards/issue_card_spec.js2
-rw-r--r--spec/frontend/boards/issue_spec.js25
-rw-r--r--spec/frontend/boards/list_spec.js2
-rw-r--r--spec/frontend/boards/mock_data.js23
-rw-r--r--spec/frontend/boards/stores/actions_spec.js32
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js47
-rw-r--r--spec/frontend/branches/components/__snapshots__/divergence_graph_spec.js.snap2
-rw-r--r--spec/frontend/ci_variable_list/components/ci_enviroments_dropdown_spec.js8
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js26
-rw-r--r--spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js4
-rw-r--r--spec/frontend/ci_variable_list/store/actions_spec.js4
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js72
-rw-r--r--spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap16
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap4
-rw-r--r--spec/frontend/clusters/components/application_row_spec.js15
-rw-r--r--spec/frontend/clusters/components/applications_spec.js66
-rw-r--r--spec/frontend/clusters/components/fluentd_output_settings_spec.js4
-rw-r--r--spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js4
-rw-r--r--spec/frontend/clusters/components/knative_domain_editor_spec.js4
-rw-r--r--spec/frontend/clusters/forms/components/integration_form_spec.js112
-rw-r--r--spec/frontend/clusters/services/application_state_machine_spec.js24
-rw-r--r--spec/frontend/clusters/services/crossplane_provider_stack_spec.js4
-rw-r--r--spec/frontend/clusters/services/mock_data.js10
-rw-r--r--spec/frontend/clusters/stores/clusters_store_spec.js23
-rw-r--r--spec/frontend/clusters_list/components/ancestor_notice_spec.js4
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js8
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js8
-rw-r--r--spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap2
-rw-r--r--spec/frontend/collapsed_sidebar_todo_spec.js2
-rw-r--r--spec/frontend/commit/commit_pipeline_status_component_spec.js2
-rw-r--r--spec/frontend/commit/pipelines/pipelines_spec.js75
-rw-r--r--spec/frontend/confidential_merge_request/components/dropdown_spec.js4
-rw-r--r--spec/frontend/confirm_modal_spec.js2
-rw-r--r--spec/frontend/contributors/store/actions_spec.js2
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/actions_spec.js2
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js92
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js42
-rw-r--r--spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js70
-rw-r--r--spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js98
-rw-r--r--spec/frontend/deploy_freeze/store/actions_spec.js123
-rw-r--r--spec/frontend/deploy_freeze/store/mutations_spec.js72
-rw-r--r--spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap14
-rw-r--r--spec/frontend/design_management/components/delete_button_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js4
-rw-r--r--spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap331
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js51
-rw-r--r--spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap (renamed from spec/frontend/design_management_new/components/toolbar/__snapshots__/pagination_spec.js.snap)24
-rw-r--r--spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap76
-rw-r--r--spec/frontend/design_management/components/toolbar/design_navigation_spec.js (renamed from spec/frontend/design_management_new/components/toolbar/pagination_spec.js)8
-rw-r--r--spec/frontend/design_management/components/toolbar/index_spec.js4
-rw-r--r--spec/frontend/design_management/components/toolbar/pagination_button_spec.js61
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap30
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/design_dropzone_spec.js.snap158
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap162
-rw-r--r--spec/frontend/design_management/components/upload/design_dropzone_spec.js25
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js16
-rw-r--r--spec/frontend/design_management/components/upload/mock_data/all_versions.js12
-rw-r--r--spec/frontend/design_management/mock_data/all_versions.js6
-rw-r--r--spec/frontend/design_management/mock_data/apollo_mock.js106
-rw-r--r--spec/frontend/design_management/mock_data/design.js12
-rw-r--r--spec/frontend/design_management/mock_data/designs.js6
-rw-r--r--spec/frontend/design_management/mock_data/no_designs.js2
-rw-r--r--spec/frontend/design_management/mock_data/versions_list.js0
-rw-r--r--spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap125
-rw-r--r--spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap16
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js9
-rw-r--r--spec/frontend/design_management/pages/index_apollo_spec.js162
-rw-r--r--spec/frontend/design_management/pages/index_spec.js196
-rw-r--r--spec/frontend/design_management/router_spec.js16
-rw-r--r--spec/frontend/design_management/utils/cache_update_spec.js2
-rw-r--r--spec/frontend/design_management/utils/design_management_utils_spec.js7
-rw-r--r--spec/frontend/design_management/utils/error_messages_spec.js6
-rw-r--r--spec/frontend/design_management_legacy/components/__snapshots__/design_note_pin_spec.js.snap (renamed from spec/frontend/design_management_new/components/__snapshots__/design_note_pin_spec.js.snap)14
-rw-r--r--spec/frontend/design_management_legacy/components/__snapshots__/design_presentation_spec.js.snap (renamed from spec/frontend/design_management_new/components/__snapshots__/design_presentation_spec.js.snap)0
-rw-r--r--spec/frontend/design_management_legacy/components/__snapshots__/design_scaler_spec.js.snap (renamed from spec/frontend/design_management_new/components/__snapshots__/design_scaler_spec.js.snap)0
-rw-r--r--spec/frontend/design_management_legacy/components/__snapshots__/image_spec.js.snap (renamed from spec/frontend/design_management_new/components/__snapshots__/image_spec.js.snap)0
-rw-r--r--spec/frontend/design_management_legacy/components/delete_button_spec.js (renamed from spec/frontend/design_management_new/components/delete_button_spec.js)6
-rw-r--r--spec/frontend/design_management_legacy/components/design_note_pin_spec.js (renamed from spec/frontend/design_management_new/components/design_note_pin_spec.js)2
-rw-r--r--spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_note_spec.js.snap (renamed from spec/frontend/design_management_new/components/design_notes/__snapshots__/design_note_spec.js.snap)0
-rw-r--r--spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_reply_form_spec.js.snap (renamed from spec/frontend/design_management_new/components/design_notes/__snapshots__/design_reply_form_spec.js.snap)0
-rw-r--r--spec/frontend/design_management_legacy/components/design_notes/design_discussion_spec.js (renamed from spec/frontend/design_management_new/components/design_notes/design_discussion_spec.js)16
-rw-r--r--spec/frontend/design_management_legacy/components/design_notes/design_note_spec.js (renamed from spec/frontend/design_management_new/components/design_notes/design_note_spec.js)4
-rw-r--r--spec/frontend/design_management_legacy/components/design_notes/design_reply_form_spec.js (renamed from spec/frontend/design_management_new/components/design_notes/design_reply_form_spec.js)2
-rw-r--r--spec/frontend/design_management_legacy/components/design_notes/toggle_replies_widget_spec.js (renamed from spec/frontend/design_management_new/components/design_notes/toggle_replies_widget_spec.js)2
-rw-r--r--spec/frontend/design_management_legacy/components/design_overlay_spec.js (renamed from spec/frontend/design_management_new/components/design_overlay_spec.js)6
-rw-r--r--spec/frontend/design_management_legacy/components/design_presentation_spec.js (renamed from spec/frontend/design_management_new/components/design_presentation_spec.js)4
-rw-r--r--spec/frontend/design_management_legacy/components/design_scaler_spec.js (renamed from spec/frontend/design_management_new/components/design_scaler_spec.js)2
-rw-r--r--spec/frontend/design_management_legacy/components/design_sidebar_spec.js (renamed from spec/frontend/design_management_new/components/design_sidebar_spec.js)6
-rw-r--r--spec/frontend/design_management_legacy/components/image_spec.js (renamed from spec/frontend/design_management_new/components/image_spec.js)2
-rw-r--r--spec/frontend/design_management_legacy/components/list/__snapshots__/item_spec.js.snap149
-rw-r--r--spec/frontend/design_management_legacy/components/list/item_spec.js (renamed from spec/frontend/design_management_new/components/list/item_spec.js)53
-rw-r--r--spec/frontend/design_management_legacy/components/toolbar/__snapshots__/index_spec.js.snap (renamed from spec/frontend/design_management_new/components/toolbar/__snapshots__/index_spec.js.snap)2
-rw-r--r--spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_button_spec.js.snap (renamed from spec/frontend/design_management/components/toolbar/__snapshots__/pagination_button_spec.js.snap)0
-rw-r--r--spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_spec.js.snap (renamed from spec/frontend/design_management/components/toolbar/__snapshots__/pagination_spec.js.snap)0
-rw-r--r--spec/frontend/design_management_legacy/components/toolbar/index_spec.js (renamed from spec/frontend/design_management_new/components/toolbar/index_spec.js)6
-rw-r--r--spec/frontend/design_management_legacy/components/toolbar/pagination_button_spec.js (renamed from spec/frontend/design_management_new/components/toolbar/pagination_button_spec.js)4
-rw-r--r--spec/frontend/design_management_legacy/components/toolbar/pagination_spec.js (renamed from spec/frontend/design_management/components/toolbar/pagination_spec.js)4
-rw-r--r--spec/frontend/design_management_legacy/components/upload/__snapshots__/button_spec.js.snap (renamed from spec/frontend/design_management_new/components/upload/__snapshots__/button_spec.js.snap)24
-rw-r--r--spec/frontend/design_management_legacy/components/upload/__snapshots__/design_dropzone_spec.js.snap (renamed from spec/frontend/design_management_new/components/upload/__snapshots__/design_dropzone_spec.js.snap)158
-rw-r--r--spec/frontend/design_management_legacy/components/upload/__snapshots__/design_version_dropdown_spec.js.snap (renamed from spec/frontend/design_management_new/components/upload/__snapshots__/design_version_dropdown_spec.js.snap)62
-rw-r--r--spec/frontend/design_management_legacy/components/upload/button_spec.js (renamed from spec/frontend/design_management_new/components/upload/button_spec.js)2
-rw-r--r--spec/frontend/design_management_legacy/components/upload/design_dropzone_spec.js (renamed from spec/frontend/design_management_new/components/upload/design_dropzone_spec.js)25
-rw-r--r--spec/frontend/design_management_legacy/components/upload/design_version_dropdown_spec.js (renamed from spec/frontend/design_management_new/components/upload/design_version_dropdown_spec.js)22
-rw-r--r--spec/frontend/design_management_legacy/components/upload/mock_data/all_versions.js (renamed from spec/frontend/design_management_new/components/upload/mock_data/all_versions.js)0
-rw-r--r--spec/frontend/design_management_legacy/mock_data/all_versions.js (renamed from spec/frontend/design_management_new/mock_data/all_versions.js)0
-rw-r--r--spec/frontend/design_management_legacy/mock_data/design.js (renamed from spec/frontend/design_management_new/mock_data/design.js)0
-rw-r--r--spec/frontend/design_management_legacy/mock_data/designs.js (renamed from spec/frontend/design_management_new/mock_data/designs.js)0
-rw-r--r--spec/frontend/design_management_legacy/mock_data/no_designs.js (renamed from spec/frontend/design_management_new/mock_data/no_designs.js)0
-rw-r--r--spec/frontend/design_management_legacy/mock_data/notes.js (renamed from spec/frontend/design_management_new/mock_data/notes.js)0
-rw-r--r--spec/frontend/design_management_legacy/pages/__snapshots__/index_spec.js.snap (renamed from spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap)120
-rw-r--r--spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap (renamed from spec/frontend/design_management_new/pages/design/__snapshots__/index_spec.js.snap)16
-rw-r--r--spec/frontend/design_management_legacy/pages/design/index_spec.js (renamed from spec/frontend/design_management_new/pages/design/index_spec.js)27
-rw-r--r--spec/frontend/design_management_legacy/pages/index_spec.js (renamed from spec/frontend/design_management_new/pages/index_spec.js)116
-rw-r--r--spec/frontend/design_management_legacy/router_spec.js (renamed from spec/frontend/design_management_new/router_spec.js)24
-rw-r--r--spec/frontend/design_management_legacy/utils/cache_update_spec.js (renamed from spec/frontend/design_management_new/utils/cache_update_spec.js)6
-rw-r--r--spec/frontend/design_management_legacy/utils/design_management_utils_spec.js (renamed from spec/frontend/design_management_new/utils/design_management_utils_spec.js)2
-rw-r--r--spec/frontend/design_management_legacy/utils/error_messages_spec.js (renamed from spec/frontend/design_management_new/utils/error_messages_spec.js)4
-rw-r--r--spec/frontend/design_management_legacy/utils/tracking_spec.js (renamed from spec/frontend/design_management_new/utils/tracking_spec.js)2
-rw-r--r--spec/frontend/design_management_new/components/list/__snapshots__/item_spec.js.snap472
-rw-r--r--spec/frontend/design_management_new/components/toolbar/__snapshots__/pagination_button_spec.js.snap28
-rw-r--r--spec/frontend/diffs/components/app_spec.js1
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_expansion_cell_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js22
-rw-r--r--spec/frontend/diffs/components/diff_stats_spec.js104
-rw-r--r--spec/frontend/diffs/components/diff_table_cell_spec.js56
-rw-r--r--spec/frontend/diffs/components/inline_diff_view_spec.js2
-rw-r--r--spec/frontend/diffs/components/no_changes_spec.js2
-rw-r--r--spec/frontend/diffs/components/parallel_diff_view_spec.js2
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js1
-rw-r--r--spec/frontend/diffs/diff_file_spec.js60
-rw-r--r--spec/frontend/diffs/store/actions_spec.js27
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js2
-rw-r--r--spec/frontend/diffs/store/utils_spec.js55
-rw-r--r--spec/frontend/dropzone_input_spec.js2
-rw-r--r--spec/frontend/editor/editor_lite_spec.js28
-rw-r--r--spec/frontend/editor/editor_markdown_ext_spec.js2
-rw-r--r--spec/frontend/emoji/emoji_spec.js2
-rw-r--r--spec/frontend/emoji/support/unicode_support_map_spec.js2
-rw-r--r--spec/frontend/environment.js18
-rw-r--r--spec/frontend/environments/environment_actions_spec.js2
-rw-r--r--spec/frontend/environments/environment_external_url_spec.js4
-rw-r--r--spec/frontend/environments/environment_stop_spec.js4
-rw-r--r--spec/frontend/environments/environments_app_spec.js2
-rw-r--r--spec/frontend/environments/folder/environments_folder_view_spec.js6
-rw-r--r--spec/frontend/error_tracking/components/error_details_spec.js4
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_actions_spec.js6
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js28
-rw-r--r--spec/frontend/error_tracking/components/stacktrace_entry_spec.js2
-rw-r--r--spec/frontend/error_tracking/store/actions_spec.js2
-rw-r--r--spec/frontend/error_tracking/store/details/actions_spec.js2
-rw-r--r--spec/frontend/error_tracking/store/list/actions_spec.js4
-rw-r--r--spec/frontend/error_tracking_settings/components/project_dropdown_spec.js14
-rw-r--r--spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js8
-rw-r--r--spec/frontend/filtered_search/filtered_search_manager_spec.js46
-rw-r--r--spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js4
-rw-r--r--spec/frontend/filtered_search/services/recent_searches_service_spec.js2
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js2
-rw-r--r--spec/frontend/fixtures/api_merge_requests.rb24
-rw-r--r--spec/frontend/fixtures/api_projects.rb35
-rw-r--r--spec/frontend/fixtures/freeze_period.rb40
-rw-r--r--spec/frontend/fixtures/merge_requests.rb1
-rw-r--r--spec/frontend/fixtures/metrics_dashboard.rb2
-rw-r--r--spec/frontend/fixtures/projects_json.rb47
-rw-r--r--spec/frontend/fixtures/test_report.rb1
-rw-r--r--spec/frontend/flash_spec.js129
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js4
-rw-r--r--spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap30
-rw-r--r--spec/frontend/grafana_integration/components/grafana_integration_spec.js11
-rw-r--r--spec/frontend/groups/components/app_spec.js2
-rw-r--r--spec/frontend/header_spec.js2
-rw-r--r--spec/frontend/helpers/backoff_helper.js33
-rw-r--r--spec/frontend/helpers/dom_events_helper.js3
-rw-r--r--spec/frontend/helpers/dom_shims/index.js2
-rw-r--r--spec/frontend/helpers/dom_shims/mutation_observer.js7
-rw-r--r--spec/frontend/helpers/dom_shims/range.js13
-rw-r--r--spec/frontend/helpers/filtered_search_spec_helper.js2
-rw-r--r--spec/frontend/helpers/init_vue_mr_page_helper.js1
-rw-r--r--spec/frontend/helpers/monitor_helper_spec.js58
-rw-r--r--spec/frontend/ide/components/activity_bar_spec.js8
-rw-r--r--spec/frontend/ide/components/commit_sidebar/empty_state_spec.js8
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js9
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_collapsed_spec.js5
-rw-r--r--spec/frontend/ide/components/commit_sidebar/list_spec.js9
-rw-r--r--spec/frontend/ide/components/commit_sidebar/radio_group_spec.js8
-rw-r--r--spec/frontend/ide/components/commit_sidebar/success_message_spec.js8
-rw-r--r--spec/frontend/ide/components/file_row_extra_spec.js3
-rw-r--r--spec/frontend/ide/components/file_templates/bar_spec.js3
-rw-r--r--spec/frontend/ide/components/ide_review_spec.js4
-rw-r--r--spec/frontend/ide/components/ide_side_bar_spec.js8
-rw-r--r--spec/frontend/ide/components/ide_spec.js6
-rw-r--r--spec/frontend/ide/components/ide_tree_list_spec.js11
-rw-r--r--spec/frontend/ide/components/ide_tree_spec.js9
-rw-r--r--spec/frontend/ide/components/jobs/detail_spec.js2
-rw-r--r--spec/frontend/ide/components/new_dropdown/index_spec.js8
-rw-r--r--spec/frontend/ide/components/new_dropdown/modal_spec.js2
-rw-r--r--spec/frontend/ide/components/panes/collapsible_sidebar_spec.js2
-rw-r--r--spec/frontend/ide/components/pipelines/list_spec.js2
-rw-r--r--spec/frontend/ide/components/preview/navigator_spec.js2
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js4
-rw-r--r--spec/frontend/ide/helpers.js20
-rw-r--r--spec/frontend/ide/ide_router_spec.js2
-rw-r--r--spec/frontend/ide/lib/decorations/controller_spec.js5
-rw-r--r--spec/frontend/ide/lib/diff/controller_spec.js6
-rw-r--r--spec/frontend/ide/lib/editor_spec.js7
-rw-r--r--spec/frontend/ide/lib/languages/vue_spec.js2
-rw-r--r--spec/frontend/ide/stores/actions/merge_request_spec.js9
-rw-r--r--spec/frontend/ide/stores/actions/project_spec.js4
-rw-r--r--spec/frontend/ide/stores/actions/tree_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/file_templates/getters_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/router/actions_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/terminal/messages_spec.js2
-rw-r--r--spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js2
-rw-r--r--spec/frontend/ide/sync_router_and_store_spec.js2
-rw-r--r--spec/frontend/ide/utils_spec.js2
-rw-r--r--spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js2
-rw-r--r--spec/frontend/image_diff/helpers/utils_helper_spec.js2
-rw-r--r--spec/frontend/image_diff/image_diff_spec.js2
-rw-r--r--spec/frontend/image_diff/replaced_image_diff_spec.js2
-rw-r--r--spec/frontend/import_projects/components/import_projects_table_spec.js108
-rw-r--r--spec/frontend/import_projects/components/imported_project_table_row_spec.js65
-rw-r--r--spec/frontend/import_projects/components/page_query_param_sync_spec.js87
-rw-r--r--spec/frontend/import_projects/components/provider_repo_table_row_spec.js120
-rw-r--r--spec/frontend/import_projects/store/actions_spec.js221
-rw-r--r--spec/frontend/import_projects/store/getters_spec.js140
-rw-r--r--spec/frontend/import_projects/store/mutations_spec.js299
-rw-r--r--spec/frontend/import_projects/utils_spec.js32
-rw-r--r--spec/frontend/incidents/components/incidents_list_spec.js362
-rw-r--r--spec/frontend/incidents/mocks/incidents.json39
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap26
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap8
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap54
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_service_spec.js4
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js4
-rw-r--r--spec/frontend/incidents_settings/components/pagerduty_form_spec.js2
-rw-r--r--spec/frontend/integrations/edit/components/dynamic_field_spec.js2
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js2
-rw-r--r--spec/frontend/integrations/edit/components/trigger_fields_spec.js2
-rw-r--r--spec/frontend/integrations/edit/store/actions_spec.js3
-rw-r--r--spec/frontend/issuable_form_spec.js56
-rw-r--r--spec/frontend/issuable_suggestions/components/item_spec.js2
-rw-r--r--spec/frontend/issuables_list/components/__snapshots__/issuables_list_app_spec.js.snap1
-rw-r--r--spec/frontend/issuables_list/components/issuable_spec.js52
-rw-r--r--spec/frontend/issuables_list/components/issuables_list_app_spec.js21
-rw-r--r--spec/frontend/issuables_list/issuable_list_test_data.js2
-rw-r--r--spec/frontend/issue_show/components/app_spec.js9
-rw-r--r--spec/frontend/issue_show/components/issuable_header_warnings_spec.js79
-rw-r--r--spec/frontend/jira_import/components/jira_import_app_spec.js200
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js201
-rw-r--r--spec/frontend/jira_import/mock_data.js16
-rw-r--r--spec/frontend/jobs/components/empty_state_spec.js128
-rw-r--r--spec/frontend/jobs/components/job_app_spec.js85
-rw-r--r--spec/frontend/jobs/components/job_log_controllers_spec.js167
-rw-r--r--spec/frontend/jobs/components/log/mock_data.js2
-rw-r--r--spec/frontend/jobs/components/sidebar_spec.js6
-rw-r--r--spec/frontend/jobs/components/stuck_block_spec.js68
-rw-r--r--spec/frontend/labels_select_spec.js31
-rw-r--r--spec/frontend/lazy_loader_spec.js18
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js2
-rw-r--r--spec/frontend/lib/utils/csrf_token_spec.js2
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js27
-rw-r--r--spec/frontend/lib/utils/poll_spec.js31
-rw-r--r--spec/frontend/lib/utils/poll_until_complete_spec.js2
-rw-r--r--spec/frontend/lib/utils/sticky_spec.js2
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js112
-rw-r--r--spec/frontend/locale/index_spec.js3
-rw-r--r--spec/frontend/logs/components/environment_logs_spec.js12
-rw-r--r--spec/frontend/logs/components/log_advanced_filters_spec.js2
-rw-r--r--spec/frontend/logs/components/log_simple_filters_spec.js4
-rw-r--r--spec/frontend/logs/mock_data.js10
-rw-r--r--spec/frontend/logs/stores/actions_spec.js2
-rw-r--r--spec/frontend/logs/stores/mutations_spec.js3
-rw-r--r--spec/frontend/maintenance_mode_settings/components/app_spec.js6
-rw-r--r--spec/frontend/merge_request_spec.js2
-rw-r--r--spec/frontend/merge_request_tabs_spec.js2
-rw-r--r--spec/frontend/milestones/project_milestone_combobox_spec.js4
-rw-r--r--spec/frontend/monitoring/alert_widget_spec.js4
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap65
-rw-r--r--spec/frontend/monitoring/components/alert_widget_form_spec.js112
-rw-r--r--spec/frontend/monitoring/components/charts/gauge_spec.js215
-rw-r--r--spec/frontend/monitoring/components/charts/heatmap_spec.js18
-rw-r--r--spec/frontend/monitoring/components/charts/options_spec.js244
-rw-r--r--spec/frontend/monitoring/components/charts/single_stat_spec.js60
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js27
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js440
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js372
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_builder_spec.js234
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js111
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js522
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboards_dropdown_spec.js120
-rw-r--r--spec/frontend/monitoring/components/embeds/metric_embed_spec.js4
-rw-r--r--spec/frontend/monitoring/components/graph_group_spec.js2
-rw-r--r--spec/frontend/monitoring/components/group_empty_state_spec.js2
-rw-r--r--spec/frontend/monitoring/components/refresh_button_spec.js30
-rw-r--r--spec/frontend/monitoring/components/variables/dropdown_field_spec.js6
-rw-r--r--spec/frontend/monitoring/csv_export_spec.js126
-rw-r--r--spec/frontend/monitoring/fixture_data.js24
-rw-r--r--spec/frontend/monitoring/graph_data.js92
-rw-r--r--spec/frontend/monitoring/mock_data.js107
-rw-r--r--spec/frontend/monitoring/pages/panel_new_page_spec.js98
-rw-r--r--spec/frontend/monitoring/requests/index_spec.js149
-rw-r--r--spec/frontend/monitoring/router_spec.js66
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js125
-rw-r--r--spec/frontend/monitoring/store/getters_spec.js119
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js149
-rw-r--r--spec/frontend/monitoring/utils_spec.js2
-rw-r--r--spec/frontend/notebook/cells/output/html_sanitize_fixtures.js114
-rw-r--r--spec/frontend/notebook/cells/output/html_sanitize_tests.js68
-rw-r--r--spec/frontend/notebook/cells/output/html_spec.js17
-rw-r--r--spec/frontend/notebook/cells/output/index_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js14
-rw-r--r--spec/frontend/notes/components/discussion_filter_spec.js5
-rw-r--r--spec/frontend/notes/components/discussion_navigator_spec.js (renamed from spec/frontend/notes/components/discussion_keyboard_navigator_spec.js)38
-rw-r--r--spec/frontend/notes/components/discussion_notes_spec.js47
-rw-r--r--spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js4
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js2
-rw-r--r--spec/frontend/notes/components/note_awards_list_spec.js2
-rw-r--r--spec/frontend/notes/components/noteable_discussion_spec.js21
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js38
-rw-r--r--spec/frontend/notes/mixins/discussion_navigation_spec.js31
-rw-r--r--spec/frontend/notes/stores/actions_spec.js85
-rw-r--r--spec/frontend/notes/stores/mutation_spec.js32
-rw-r--r--spec/frontend/onboarding_issues/index_spec.js2
-rw-r--r--spec/frontend/operation_settings/components/metrics_settings_spec.js11
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap46
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap49
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap34
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/history_element_spec.js.snap38
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap69
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap69
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap49
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap172
-rw-r--r--spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap50
-rw-r--r--spec/frontend/packages/details/components/additional_metadata_spec.js119
-rw-r--r--spec/frontend/packages/details/components/app_spec.js281
-rw-r--r--spec/frontend/packages/details/components/code_instruction_spec.js110
-rw-r--r--spec/frontend/packages/details/components/composer_installation_spec.js95
-rw-r--r--spec/frontend/packages/details/components/conan_installation_spec.js68
-rw-r--r--spec/frontend/packages/details/components/dependency_row_spec.js62
-rw-r--r--spec/frontend/packages/details/components/history_element_spec.js57
-rw-r--r--spec/frontend/packages/details/components/installations_commands_spec.js57
-rw-r--r--spec/frontend/packages/details/components/maven_installation_spec.js91
-rw-r--r--spec/frontend/packages/details/components/npm_installation_spec.js99
-rw-r--r--spec/frontend/packages/details/components/nuget_installation_spec.js75
-rw-r--r--spec/frontend/packages/details/components/package_history_spec.js106
-rw-r--r--spec/frontend/packages/details/components/package_title_spec.js168
-rw-r--r--spec/frontend/packages/details/components/pypi_installation_spec.js60
-rw-r--r--spec/frontend/packages/details/mock_data.js47
-rw-r--r--spec/frontend/packages/details/store/actions_spec.js76
-rw-r--r--spec/frontend/packages/details/store/getters_spec.js237
-rw-r--r--spec/frontend/packages/details/store/mutations_spec.js31
-rw-r--r--spec/frontend/packages/details/utils_spec.js24
-rw-r--r--spec/frontend/packages/list/coming_soon/helpers_spec.js36
-rw-r--r--spec/frontend/packages/list/coming_soon/mock_data.js90
-rw-r--r--spec/frontend/packages/list/coming_soon/packages_coming_soon_spec.js138
-rw-r--r--spec/frontend/packages/list/components/__snapshots__/packages_filter_spec.js.snap14
-rw-r--r--spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap457
-rw-r--r--spec/frontend/packages/list/components/packages_filter_spec.js50
-rw-r--r--spec/frontend/packages/list/components/packages_list_app_spec.js148
-rw-r--r--spec/frontend/packages/list/components/packages_list_spec.js219
-rw-r--r--spec/frontend/packages/list/components/packages_sort_spec.js92
-rw-r--r--spec/frontend/packages/list/stores/actions_spec.js240
-rw-r--r--spec/frontend/packages/list/stores/getters_spec.js36
-rw-r--r--spec/frontend/packages/list/stores/mutations_spec.js95
-rw-r--r--spec/frontend/packages/list/utils_spec.js39
-rw-r--r--spec/frontend/packages/mock_data.js170
-rw-r--r--spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap101
-rw-r--r--spec/frontend/packages/shared/components/__snapshots__/publish_method_spec.js.snap39
-rw-r--r--spec/frontend/packages/shared/components/package_list_row_spec.js106
-rw-r--r--spec/frontend/packages/shared/components/package_tags_spec.js115
-rw-r--r--spec/frontend/packages/shared/components/packages_list_loader_spec.js42
-rw-r--r--spec/frontend/packages/shared/components/publish_method_spec.js50
-rw-r--r--spec/frontend/packages/shared/utils_spec.js66
-rw-r--r--spec/frontend/pager_spec.js2
-rw-r--r--spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js4
-rw-r--r--spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap26
-rw-r--r--spec/frontend/pages/admin/users/components/delete_user_modal_spec.js4
-rw-r--r--spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js50
-rw-r--r--spec/frontend/pages/labels/components/promote_label_modal_spec.js2
-rw-r--r--spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js4
-rw-r--r--spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js2
-rw-r--r--spec/frontend/pages/profiles/show/emoji_menu_spec.js2
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js8
-rw-r--r--spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap16
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js8
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js2
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js84
-rw-r--r--spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js48
-rw-r--r--spec/frontend/pdf/page_spec.js2
-rw-r--r--spec/frontend/performance_bar/components/detailed_metric_spec.js2
-rw-r--r--spec/frontend/persistent_user_callout_spec.js2
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js108
-rw-r--r--spec/frontend/pipeline_new/mock_data.js21
-rw-r--r--spec/frontend/pipelines/components/dag/dag_spec.js119
-rw-r--r--spec/frontend/pipelines/components/dag/drawing_utils_spec.js4
-rw-r--r--spec/frontend/pipelines/components/dag/mock_data.js436
-rw-r--r--spec/frontend/pipelines/components/dag/parsing_utils_spec.js69
-rw-r--r--spec/frontend/pipelines/components/pipelines_filtered_search_spec.js4
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js3
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js20
-rw-r--r--spec/frontend/pipelines/header_component_spec.js6
-rw-r--r--spec/frontend/pipelines/pipeline_details_mediator_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js4
-rw-r--r--spec/frontend/pipelines/pipelines_artifacts_spec.js2
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js10
-rw-r--r--spec/frontend/pipelines/stage_spec.js2
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js124
-rw-r--r--spec/frontend/pipelines/test_reports/stores/mutations_spec.js33
-rw-r--r--spec/frontend/pipelines/test_reports/test_reports_spec.js28
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js20
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js2
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js2
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js2
-rw-r--r--spec/frontend/project_find_file_spec.js6
-rw-r--r--spec/frontend/projects/commits/components/author_select_spec.js6
-rw-r--r--spec/frontend/projects/commits/store/actions_spec.js4
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap83
-rw-r--r--spec/frontend/projects/components/__snapshots__/remove_modal_spec.js.snap126
-rw-r--r--spec/frontend/projects/components/project_delete_button_spec.js47
-rw-r--r--spec/frontend/projects/components/remove_modal_spec.js62
-rw-r--r--spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap113
-rw-r--r--spec/frontend/projects/components/shared/delete_button_spec.js83
-rw-r--r--spec/frontend/projects/experiment_new_project_creation/components/legacy_container_spec.js2
-rw-r--r--spec/frontend/projects/experiment_new_project_creation/components/welcome_spec.js2
-rw-r--r--spec/frontend/projects/project_new_spec.js2
-rw-r--r--spec/frontend/projects/settings/access_dropdown_spec.js140
-rw-r--r--spec/frontend/prometheus_alerts/components/reset_key_spec.js2
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js50
-rw-r--r--spec/frontend/registry/explorer/components/details_page/details_row_spec.js43
-rw-r--r--spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js4
-rw-r--r--spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js4
-rw-r--r--spec/frontend/registry/explorer/pages/details_spec.js2
-rw-r--r--spec/frontend/registry/explorer/pages/list_spec.js4
-rw-r--r--spec/frontend/registry/explorer/stores/actions_spec.js8
-rw-r--r--spec/frontend/registry/settings/components/settings_form_spec.js2
-rw-r--r--spec/frontend/registry/settings/store/actions_spec.js2
-rw-r--r--spec/frontend/registry/shared/components/details_row_spec.js71
-rw-r--r--spec/frontend/related_merge_requests/store/actions_spec.js2
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js (renamed from spec/frontend/releases/components/app_edit_spec.js)103
-rw-r--r--spec/frontend/releases/components/app_index_spec.js2
-rw-r--r--spec/frontend/releases/components/app_new_spec.js26
-rw-r--r--spec/frontend/releases/components/app_show_spec.js2
-rw-r--r--spec/frontend/releases/components/asset_links_form_spec.js143
-rw-r--r--spec/frontend/releases/components/release_block_assets_spec.js4
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js2
-rw-r--r--spec/frontend/releases/components/release_block_metadata_spec.js2
-rw-r--r--spec/frontend/releases/components/tag_field_exsting_spec.js78
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js144
-rw-r--r--spec/frontend/releases/components/tag_field_spec.js59
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js615
-rw-r--r--spec/frontend/releases/stores/modules/detail/getters_spec.js85
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js48
-rw-r--r--spec/frontend/releases/util_spec.js103
-rw-r--r--spec/frontend/reports/accessibility_report/mock_data.js3
-rw-r--r--spec/frontend/reports/accessibility_report/store/actions_spec.js6
-rw-r--r--spec/frontend/reports/codequality_report/store/actions_spec.js6
-rw-r--r--spec/frontend/reports/components/grouped_test_reports_app_spec.js29
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap12
-rw-r--r--spec/frontend/repository/components/breadcrumbs_spec.js6
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js8
-rw-r--r--spec/frontend/repository/components/preview/index_spec.js6
-rw-r--r--spec/frontend/repository/components/table/index_spec.js2
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js76
-rw-r--r--spec/frontend/repository/components/web_ide_link_spec.js2
-rw-r--r--spec/frontend/repository/utils/dom_spec.js2
-rw-r--r--spec/frontend/search_autocomplete_spec.js26
-rw-r--r--spec/frontend/self_monitor/components/self_monitor_form_spec.js2
-rw-r--r--spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap20
-rw-r--r--spec/frontend/serverless/components/empty_state_spec.js25
-rw-r--r--spec/frontend/serverless/components/function_details_spec.js10
-rw-r--r--spec/frontend/serverless/components/functions_spec.js57
-rw-r--r--spec/frontend/serverless/components/missing_prometheus_spec.js20
-rw-r--r--spec/frontend/serverless/survey_banner_spec.js2
-rw-r--r--spec/frontend/serverless/utils.js4
-rw-r--r--spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap8
-rw-r--r--spec/frontend/sidebar/confidential/__snapshots__/edit_form_spec.js.snap50
-rw-r--r--spec/frontend/sidebar/confidential/edit_form_buttons_spec.js108
-rw-r--r--spec/frontend/sidebar/confidential/edit_form_spec.js11
-rw-r--r--spec/frontend/sidebar/confidential_issue_sidebar_spec.js98
-rw-r--r--spec/frontend/sidebar/lock/__snapshots__/edit_form_spec.js.snap79
-rw-r--r--spec/frontend/sidebar/lock/constants.js2
-rw-r--r--spec/frontend/sidebar/lock/edit_form_buttons_spec.js171
-rw-r--r--spec/frontend/sidebar/lock/edit_form_spec.js67
-rw-r--r--spec/frontend/sidebar/lock/issuable_lock_form_spec.js133
-rw-r--r--spec/frontend/sidebar/lock/lock_issue_sidebar_spec.js99
-rw-r--r--spec/frontend/sidebar/todo_spec.js2
-rw-r--r--spec/frontend/snippet/collapsible_input_spec.js2
-rw-r--r--spec/frontend/snippet/snippet_bundle_spec.js2
-rw-r--r--spec/frontend/snippet/snippet_edit_spec.js3
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap32
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap2
-rw-r--r--spec/frontend/snippets/components/edit_spec.js539
-rw-r--r--spec/frontend/snippets/components/show_spec.js59
-rw-r--r--spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js301
-rw-r--r--spec/frontend/snippets/components/snippet_blob_edit_spec.js227
-rw-r--r--spec/frontend/snippets/components/snippet_blob_view_spec.js82
-rw-r--r--spec/frontend/snippets/components/snippet_description_edit_spec.js2
-rw-r--r--spec/frontend/snippets/components/snippet_description_view_spec.js2
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js112
-rw-r--r--spec/frontend/snippets/components/snippet_title_spec.js4
-rw-r--r--spec/frontend/snippets/components/snippet_visibility_edit_spec.js4
-rw-r--r--spec/frontend/snippets/test_utils.js76
-rw-r--r--spec/frontend/snippets/utils/blob_spec.js63
-rw-r--r--spec/frontend/snippets_spec.js2
-rw-r--r--spec/frontend/static_site_editor/components/app_spec.js34
-rw-r--r--spec/frontend/static_site_editor/components/edit_area_spec.js41
-rw-r--r--spec/frontend/static_site_editor/components/saved_changes_message_spec.js56
-rw-r--r--spec/frontend/static_site_editor/pages/home_spec.js2
-rw-r--r--spec/frontend/static_site_editor/pages/success_spec.js63
-rw-r--r--spec/frontend/static_site_editor/services/formatter_spec.js26
-rw-r--r--spec/frontend/static_site_editor/services/submit_content_changes_spec.js2
-rw-r--r--spec/frontend/static_site_editor/services/templater_spec.js104
-rw-r--r--spec/frontend/test_setup.js2
-rw-r--r--spec/frontend/vue_alerts_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js12
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js209
-rw-r--r--spec/frontend/vue_mr_widget/components/pipeline_tour_mock_data.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js66
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/mock_data.js3
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js43
-rw-r--r--spec/frontend/vue_mr_widget/stores/get_state_key_spec.js52
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap5
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap40
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap14
-rw-r--r--spec/frontend/vue_shared/components/clone_dropdown_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dismissible_container_spec.js58
-rw-r--r--spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js91
-rw-r--r--spec/frontend/vue_shared/components/file_finder/item_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/file_icon_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_bar_root_spec.js129
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js56
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js170
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js152
-rw-r--r--spec/frontend/vue_shared/components/form/form_footer_actions_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/form/title_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/icon_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/identicon_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js73
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_assignees_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js94
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js69
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js51
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js83
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js27
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js23
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js44
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js131
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js45
-rw-r--r--spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/split_button_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/table_pagination_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/time_ago_tooltip_spec.js22
-rw-r--r--spec/frontend/vue_shared/components/toggle_button_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js6
-rw-r--r--spec/frontend/vue_shared/directives/autofocusonshow_spec.js11
-rw-r--r--spec/frontend/whats_new/components/app_spec.js57
-rw-r--r--spec/frontend/whats_new/components/trigger_spec.js43
-rw-r--r--spec/frontend/whats_new/store/actions_spec.js17
-rw-r--r--spec/frontend/whats_new/store/mutations_spec.js25
644 files changed, 20717 insertions, 7336 deletions
diff --git a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js
index 726ed0fa030..9fee8e18d26 100644
--- a/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js
+++ b/spec/frontend/__mocks__/@toast-ui/vue-editor/index.js
@@ -17,6 +17,17 @@ export const Editor = {
type: String,
},
},
+ created() {
+ const mockEditorApi = {
+ eventManager: {
+ addEventType: jest.fn(),
+ listen: jest.fn(),
+ removeEventHandler: jest.fn(),
+ },
+ };
+
+ this.$emit('load', mockEditorApi);
+ },
render(h) {
return h('div');
},
diff --git a/spec/frontend/__mocks__/monaco-editor/index.js b/spec/frontend/__mocks__/monaco-editor/index.js
index b9602d69b74..18b7df32f9b 100644
--- a/spec/frontend/__mocks__/monaco-editor/index.js
+++ b/spec/frontend/__mocks__/monaco-editor/index.js
@@ -8,11 +8,11 @@ import 'monaco-editor/esm/vs/language/css/monaco.contribution';
import 'monaco-editor/esm/vs/language/json/monaco.contribution';
import 'monaco-editor/esm/vs/language/html/monaco.contribution';
import 'monaco-editor/esm/vs/basic-languages/monaco.contribution';
-import 'monaco-yaml/esm/monaco.contribution';
+import 'monaco-yaml/lib/esm/monaco.contribution';
// This language starts trying to spin up web workers which obviously breaks in Jest environment
jest.mock('monaco-editor/esm/vs/language/typescript/tsMode');
-jest.mock('monaco-yaml/esm/yamlMode');
+jest.mock('monaco-yaml/lib/esm/yamlMode');
export * from 'monaco-editor/esm/vs/editor/editor.api';
export default global.monaco;
diff --git a/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
new file mode 100644
index 00000000000..5fad0d07f97
--- /dev/null
+++ b/spec/frontend/add_context_commits_modal/components/__snapshots__/add_context_commits_modal_spec.js.snap
@@ -0,0 +1,50 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AddContextCommitsModal renders modal with 2 tabs 1`] = `
+<gl-modal-stub
+ body-class="add-review-item pt-0"
+ cancel-variant="light"
+ modalclass=""
+ modalid="add-review-item"
+ ok-disabled="true"
+ ok-title="Save changes"
+ scrollable="true"
+ size="md"
+ title="Add or remove previously merged commits"
+ titletag="h4"
+>
+ <gl-tabs-stub
+ contentclass="pt-0"
+ theme="indigo"
+ value="0"
+ >
+ <gl-tab-stub>
+
+ <div
+ class="mt-2"
+ >
+ <gl-search-box-by-type-stub
+ clearbuttontitle="Clear"
+ placeholder="Search by commit title or SHA"
+ value=""
+ />
+
+ <review-tab-container-stub
+ commits=""
+ emptylisttext="Your search didn't match any commits. Try a different query."
+ loadingfailedtext="Unable to load commits. Try again later."
+ />
+ </div>
+ </gl-tab-stub>
+
+ <gl-tab-stub>
+
+ <review-tab-container-stub
+ commits=""
+ emptylisttext="Commits you select appear here. Go to the first tab and select commits to add to this merge request."
+ loadingfailedtext="Unable to load commits. Try again later."
+ />
+ </gl-tab-stub>
+ </gl-tabs-stub>
+</gl-modal-stub>
+`;
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
new file mode 100644
index 00000000000..6904e34db5d
--- /dev/null
+++ b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
@@ -0,0 +1,174 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { GlModal, GlSearchBoxByType } from '@gitlab/ui';
+import AddReviewItemsModal from '~/add_context_commits_modal/components/add_context_commits_modal_wrapper.vue';
+import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
+
+import defaultState from '~/add_context_commits_modal/store/state';
+import mutations from '~/add_context_commits_modal/store/mutations';
+import * as actions from '~/add_context_commits_modal/store/actions';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('AddContextCommitsModal', () => {
+ let wrapper;
+ let store;
+ const createContextCommits = jest.fn();
+ const removeContextCommits = jest.fn();
+ const resetModalState = jest.fn();
+ const searchCommits = jest.fn();
+ const { commit } = getDiffWithCommit();
+
+ const createWrapper = (props = {}) => {
+ store = new Vuex.Store({
+ mutations,
+ state: {
+ ...defaultState(),
+ },
+ actions: {
+ ...actions,
+ searchCommits,
+ createContextCommits,
+ removeContextCommits,
+ resetModalState,
+ },
+ });
+
+ wrapper = shallowMount(AddReviewItemsModal, {
+ localVue,
+ store,
+ propsData: {
+ contextCommitsPath: '',
+ targetBranch: 'master',
+ mergeRequestIid: 1,
+ projectId: 1,
+ ...props,
+ },
+ });
+ return wrapper;
+ };
+
+ const findModal = () => wrapper.find(GlModal);
+ const findSearch = () => wrapper.find(GlSearchBoxByType);
+
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders modal with 2 tabs', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('an ok button labeled "Save changes"', () => {
+ expect(findModal().attributes('ok-title')).toEqual('Save changes');
+ });
+
+ describe('when in first tab, renders a modal with', () => {
+ it('renders the search box component', () => {
+ expect(findSearch().exists()).toBe(true);
+ });
+
+ it('when user starts entering text in search box, it calls action "searchCommits" after waiting for 500s', () => {
+ const searchText = 'abcd';
+ findSearch().vm.$emit('input', searchText);
+ expect(searchCommits).not.toBeCalled();
+ jest.advanceTimersByTime(500);
+ expect(searchCommits).toHaveBeenCalledWith(expect.anything(), searchText, undefined);
+ });
+
+ it('disabled ok button when no row is selected', () => {
+ expect(findModal().attributes('ok-disabled')).toBe('true');
+ });
+
+ it('enabled ok button when atleast one row is selected', () => {
+ wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findModal().attributes('ok-disabled')).toBeFalsy();
+ });
+ });
+ });
+
+ describe('when in second tab, renders a modal with', () => {
+ beforeEach(() => {
+ wrapper.vm.$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', () => {
+ wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findModal().attributes('ok-disabled')).toBeFalsy();
+ });
+ });
+
+ it('a disabled ok button in first tab, when row is selected in second tab', () => {
+ createWrapper({ selectedContextCommits: [commit] });
+ expect(wrapper.find(GlModal).attributes('ok-disabled')).toBe('true');
+ });
+ });
+
+ describe('has an ok button when clicked calls action', () => {
+ it('"createContextCommits" when only new commits to be added ', () => {
+ wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
+ findModal().vm.$emit('ok');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(createContextCommits).toHaveBeenCalledWith(
+ expect.anything(),
+ { commits: [{ ...commit, isSelected: true }], forceReload: true },
+ undefined,
+ );
+ });
+ });
+ it('"removeContextCommits" when only added commits are to be removed ', () => {
+ wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
+ findModal().vm.$emit('ok');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), true, undefined);
+ });
+ });
+ it('"createContextCommits" and "removeContextCommits" when new commits are to be added and old commits are to be removed', () => {
+ wrapper.vm.$store.state.selectedCommits = [{ ...commit, isSelected: true }];
+ wrapper.vm.$store.state.toRemoveCommits = [commit.short_id];
+ findModal().vm.$emit('ok');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(createContextCommits).toHaveBeenCalledWith(
+ expect.anything(),
+ { commits: [{ ...commit, isSelected: true }] },
+ undefined,
+ );
+ expect(removeContextCommits).toHaveBeenCalledWith(expect.anything(), undefined, undefined);
+ });
+ });
+ });
+
+ describe('has a cancel button when clicked', () => {
+ it('does not call "createContextCommits" or "removeContextCommits"', () => {
+ findModal().vm.$emit('cancel');
+ expect(createContextCommits).not.toHaveBeenCalled();
+ expect(removeContextCommits).not.toHaveBeenCalled();
+ });
+ it('"resetModalState" to reset all the modal state', () => {
+ findModal().vm.$emit('cancel');
+ expect(resetModalState).toHaveBeenCalledWith(expect.anything(), undefined, undefined);
+ });
+ });
+
+ describe('when model is closed by clicking the "X" button or by pressing "ESC" key', () => {
+ it('does not call "createContextCommits" or "removeContextCommits"', () => {
+ findModal().vm.$emit('close');
+ expect(createContextCommits).not.toHaveBeenCalled();
+ expect(removeContextCommits).not.toHaveBeenCalled();
+ });
+ it('"resetModalState" to reset all the modal state', () => {
+ findModal().vm.$emit('close');
+ expect(resetModalState).toHaveBeenCalledWith(expect.anything(), undefined, undefined);
+ });
+ });
+});
diff --git a/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js b/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
new file mode 100644
index 00000000000..4e65713a680
--- /dev/null
+++ b/spec/frontend/add_context_commits_modal/components/review_tab_container_spec.js
@@ -0,0 +1,51 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import ReviewTabContainer from '~/add_context_commits_modal/components/review_tab_container.vue';
+import CommitItem from '~/diffs/components/commit_item.vue';
+import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
+
+describe('ReviewTabContainer', () => {
+ let wrapper;
+ const { commit } = getDiffWithCommit();
+
+ const createWrapper = (props = {}) => {
+ wrapper = shallowMount(ReviewTabContainer, {
+ propsData: {
+ tab: 'commits',
+ isLoading: false,
+ loadingError: false,
+ loadingFailedText: 'Failed to load commits',
+ commits: [],
+ selectedRow: [],
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('shows loading icon when commits are being loaded', () => {
+ createWrapper({ isLoading: true });
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('shows loading error text when API call fails', () => {
+ createWrapper({ loadingError: true });
+ expect(wrapper.text()).toContain('Failed to load commits');
+ });
+
+ it('shows "No commits present here" when commits are not present', () => {
+ expect(wrapper.text()).toContain('No commits present here');
+ });
+
+ it('renders all passed commits as list', () => {
+ createWrapper({ commits: [commit] });
+ expect(wrapper.findAll(CommitItem).length).toBe(1);
+ });
+});
diff --git a/spec/frontend/add_context_commits_modal/store/actions_spec.js b/spec/frontend/add_context_commits_modal/store/actions_spec.js
new file mode 100644
index 00000000000..24948dd6073
--- /dev/null
+++ b/spec/frontend/add_context_commits_modal/store/actions_spec.js
@@ -0,0 +1,239 @@
+import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import {
+ setBaseConfig,
+ setTabIndex,
+ setCommits,
+ createContextCommits,
+ fetchContextCommits,
+ setContextCommits,
+ removeContextCommits,
+ setSelectedCommits,
+ setSearchText,
+ setToRemoveCommits,
+ resetModalState,
+} from '~/add_context_commits_modal/store/actions';
+import * as types from '~/add_context_commits_modal/store/mutation_types';
+import testAction from '../../helpers/vuex_action_helper';
+
+describe('AddContextCommitsModalStoreActions', () => {
+ const contextCommitEndpoint =
+ '/api/v4/projects/gitlab-org%2fgitlab/merge_requests/1/context_commits';
+ const mergeRequestIid = 1;
+ const projectId = 1;
+ const projectPath = 'gitlab-org/gitlab';
+ const contextCommitsPath = `${TEST_HOST}/gitlab-org/gitlab/-/merge_requests/1/context_commits.json`;
+ const dummyCommit = {
+ id: 1,
+ title: 'dummy commit',
+ short_id: 'abcdef',
+ committed_date: '2020-06-12',
+ };
+ gon.api_version = 'v4';
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('setBaseConfig', () => {
+ it('commits SET_BASE_CONFIG', done => {
+ const options = { contextCommitsPath, mergeRequestIid, projectId };
+ testAction(
+ setBaseConfig,
+ options,
+ {
+ contextCommitsPath: '',
+ mergeRequestIid,
+ projectId,
+ },
+ [
+ {
+ type: types.SET_BASE_CONFIG,
+ payload: options,
+ },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setTabIndex', () => {
+ it('commits SET_TABINDEX', done => {
+ testAction(
+ setTabIndex,
+ { tabIndex: 1 },
+ { tabIndex: 0 },
+ [{ type: types.SET_TABINDEX, payload: { tabIndex: 1 } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setCommits', () => {
+ it('commits SET_COMMITS', done => {
+ testAction(
+ setCommits,
+ { commits: [], silentAddition: false },
+ { isLoadingCommits: false, commits: [] },
+ [{ type: types.SET_COMMITS, payload: [] }],
+ [],
+ done,
+ );
+ });
+
+ it('commits SET_COMMITS_SILENT', done => {
+ testAction(
+ setCommits,
+ { commits: [], silentAddition: true },
+ { isLoadingCommits: true, commits: [] },
+ [{ type: types.SET_COMMITS_SILENT, payload: [] }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('createContextCommits', () => {
+ it('calls API to create context commits', done => {
+ mock.onPost(contextCommitEndpoint).reply(200, {});
+
+ testAction(createContextCommits, { commits: [] }, {}, [], [], done);
+
+ createContextCommits(
+ { state: { projectId, mergeRequestIid }, commit: () => null },
+ { commits: [] },
+ )
+ .then(() => {
+ done();
+ })
+ .catch(done.fail);
+ });
+ });
+
+ describe('fetchContextCommits', () => {
+ beforeEach(() => {
+ mock
+ .onGet(
+ `/api/${gon.api_version}/projects/gitlab-org%2Fgitlab/merge_requests/1/context_commits`,
+ )
+ .reply(200, [dummyCommit]);
+ });
+ it('commits FETCH_CONTEXT_COMMITS', done => {
+ const contextCommit = { ...dummyCommit, isSelected: true };
+ testAction(
+ fetchContextCommits,
+ null,
+ {
+ mergeRequestIid,
+ projectId: projectPath,
+ isLoadingContextCommits: false,
+ contextCommitsLoadingError: false,
+ commits: [],
+ },
+ [{ type: types.FETCH_CONTEXT_COMMITS }],
+ [
+ { type: 'setContextCommits', payload: [contextCommit] },
+ { type: 'setCommits', payload: { commits: [contextCommit], silentAddition: true } },
+ { type: 'setSelectedCommits', payload: [contextCommit] },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('setContextCommits', () => {
+ it('commits SET_CONTEXT_COMMITS', done => {
+ testAction(
+ setContextCommits,
+ { data: [] },
+ { contextCommits: [], isLoadingContextCommits: false },
+ [{ type: types.SET_CONTEXT_COMMITS, payload: { data: [] } }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('removeContextCommits', () => {
+ beforeEach(() => {
+ mock
+ .onDelete('/api/v4/projects/gitlab-org%2Fgitlab/merge_requests/1/context_commits')
+ .reply(204);
+ });
+ it('calls API to remove context commits', done => {
+ testAction(
+ removeContextCommits,
+ { forceReload: false },
+ { mergeRequestIid, projectId, toRemoveCommits: [] },
+ [],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setSelectedCommits', () => {
+ it('commits SET_SELECTED_COMMITS', done => {
+ testAction(
+ setSelectedCommits,
+ [dummyCommit],
+ { selectedCommits: [] },
+ [{ type: types.SET_SELECTED_COMMITS, payload: [dummyCommit] }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setSearchText', () => {
+ it('commits SET_SEARCH_TEXT', done => {
+ const searchText = 'Dummy Text';
+ testAction(
+ setSearchText,
+ searchText,
+ { searchText: '' },
+ [{ type: types.SET_SEARCH_TEXT, payload: searchText }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setToRemoveCommits', () => {
+ it('commits SET_TO_REMOVE_COMMITS', done => {
+ const commitId = 'abcde';
+
+ testAction(
+ setToRemoveCommits,
+ [commitId],
+ { toRemoveCommits: [] },
+ [{ type: types.SET_TO_REMOVE_COMMITS, payload: [commitId] }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('resetModalState', () => {
+ it('commits RESET_MODAL_STATE', done => {
+ const commitId = 'abcde';
+
+ testAction(
+ resetModalState,
+ null,
+ { toRemoveCommits: [commitId] },
+ [{ type: types.RESET_MODAL_STATE }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/add_context_commits_modal/store/mutations_spec.js b/spec/frontend/add_context_commits_modal/store/mutations_spec.js
new file mode 100644
index 00000000000..22f82570ab1
--- /dev/null
+++ b/spec/frontend/add_context_commits_modal/store/mutations_spec.js
@@ -0,0 +1,156 @@
+import { TEST_HOST } from 'helpers/test_constants';
+import mutations from '~/add_context_commits_modal/store/mutations';
+import * as types from '~/add_context_commits_modal/store/mutation_types';
+import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
+
+describe('AddContextCommitsModalStoreMutations', () => {
+ const { commit } = getDiffWithCommit();
+ describe('SET_BASE_CONFIG', () => {
+ it('should set contextCommitsPath, mergeRequestIid and projectId', () => {
+ const state = {};
+ const contextCommitsPath = `${TEST_HOST}/gitlab-org/gitlab/-/merge_requests/1/context_commits.json`;
+ const mergeRequestIid = 1;
+ const projectId = 1;
+
+ mutations[types.SET_BASE_CONFIG](state, { contextCommitsPath, mergeRequestIid, projectId });
+
+ expect(state.contextCommitsPath).toEqual(contextCommitsPath);
+ expect(state.mergeRequestIid).toEqual(mergeRequestIid);
+ expect(state.projectId).toEqual(projectId);
+ });
+ });
+
+ describe('SET_TABINDEX', () => {
+ it('sets tabIndex to specific index', () => {
+ const state = { tabIndex: 0 };
+
+ mutations[types.SET_TABINDEX](state, 1);
+
+ expect(state.tabIndex).toBe(1);
+ });
+ });
+
+ describe('FETCH_COMMITS', () => {
+ it('sets isLoadingCommits to true', () => {
+ const state = { isLoadingCommits: false };
+
+ mutations[types.FETCH_COMMITS](state);
+
+ expect(state.isLoadingCommits).toBe(true);
+ });
+ });
+
+ describe('SET_COMMITS', () => {
+ it('sets commits to passed data and stop loading', () => {
+ const state = { commits: [], isLoadingCommits: true };
+
+ mutations[types.SET_COMMITS](state, [commit]);
+
+ expect(state.commits).toStrictEqual([commit]);
+ expect(state.isLoadingCommits).toBe(false);
+ });
+ });
+
+ describe('SET_COMMITS_SILENT', () => {
+ it('sets commits to passed data and loading continues', () => {
+ const state = { commits: [], isLoadingCommits: true };
+
+ mutations[types.SET_COMMITS_SILENT](state, [commit]);
+
+ expect(state.commits).toStrictEqual([commit]);
+ expect(state.isLoadingCommits).toBe(true);
+ });
+ });
+
+ describe('FETCH_COMMITS_ERROR', () => {
+ it('sets commitsLoadingError to true', () => {
+ const state = { commitsLoadingError: false };
+
+ mutations[types.FETCH_COMMITS_ERROR](state);
+
+ expect(state.commitsLoadingError).toBe(true);
+ });
+ });
+
+ describe('FETCH_CONTEXT_COMMITS', () => {
+ it('sets isLoadingContextCommits to true', () => {
+ const state = { isLoadingContextCommits: false };
+
+ mutations[types.FETCH_CONTEXT_COMMITS](state);
+
+ expect(state.isLoadingContextCommits).toBe(true);
+ });
+ });
+
+ describe('SET_CONTEXT_COMMITS', () => {
+ it('sets contextCommit to passed data and stop loading', () => {
+ const state = { contextCommits: [], isLoadingContextCommits: true };
+
+ mutations[types.SET_CONTEXT_COMMITS](state, [commit]);
+
+ expect(state.contextCommits).toStrictEqual([commit]);
+ expect(state.isLoadingContextCommits).toBe(false);
+ });
+ });
+
+ describe('FETCH_CONTEXT_COMMITS_ERROR', () => {
+ it('sets contextCommitsLoadingError to true', () => {
+ const state = { contextCommitsLoadingError: false };
+
+ mutations[types.FETCH_CONTEXT_COMMITS_ERROR](state);
+
+ expect(state.contextCommitsLoadingError).toBe(true);
+ });
+ });
+
+ describe('SET_SELECTED_COMMITS', () => {
+ it('sets selectedCommits to specified value', () => {
+ const state = { selectedCommits: [] };
+
+ mutations[types.SET_SELECTED_COMMITS](state, [commit]);
+
+ expect(state.selectedCommits).toStrictEqual([commit]);
+ });
+ });
+
+ describe('SET_SEARCH_TEXT', () => {
+ it('sets searchText to specified value', () => {
+ const searchText = 'Test';
+ const state = { searchText: '' };
+
+ mutations[types.SET_SEARCH_TEXT](state, searchText);
+
+ expect(state.searchText).toBe(searchText);
+ });
+ });
+
+ describe('SET_TO_REMOVE_COMMITS', () => {
+ it('sets searchText to specified value', () => {
+ const state = { toRemoveCommits: [] };
+
+ mutations[types.SET_TO_REMOVE_COMMITS](state, [commit.short_id]);
+
+ expect(state.toRemoveCommits).toStrictEqual([commit.short_id]);
+ });
+ });
+
+ describe('RESET_MODAL_STATE', () => {
+ it('sets searchText to specified value', () => {
+ const state = {
+ commits: [commit],
+ contextCommits: [commit],
+ selectedCommits: [commit],
+ toRemoveCommits: [commit.short_id],
+ searchText: 'Test',
+ };
+
+ mutations[types.RESET_MODAL_STATE](state);
+
+ expect(state.commits).toStrictEqual([]);
+ expect(state.contextCommits).toStrictEqual([]);
+ expect(state.selectedCommits).toStrictEqual([]);
+ expect(state.toRemoveCommits).toStrictEqual([]);
+ expect(state.searchText).toBe('');
+ });
+ });
+});
diff --git a/spec/frontend/alert_management/components/alert_management_detail_spec.js b/spec/frontend/alert_management/components/alert_details_spec.js
index daa730d3b9f..2c4ed100a56 100644
--- a/spec/frontend/alert_management/components/alert_management_detail_spec.js
+++ b/spec/frontend/alert_management/components/alert_details_spec.js
@@ -20,6 +20,7 @@ describe('AlertDetails', () => {
const projectPath = 'root/alerts';
const projectIssuesPath = 'root/alerts/-/issues';
const projectId = '1';
+ const $router = { replace: jest.fn() };
const findDetailsTable = () => wrapper.find(GlTable);
@@ -44,6 +45,8 @@ describe('AlertDetails', () => {
sidebarStatus: {},
},
},
+ $router,
+ $route: { params: {} },
},
stubs,
});
@@ -60,9 +63,9 @@ describe('AlertDetails', () => {
mock.restore();
});
- const findCreateIssueBtn = () => wrapper.find('[data-testid="createIssueBtn"]');
- const findViewIssueBtn = () => wrapper.find('[data-testid="viewIssueBtn"]');
- const findIssueCreationAlert = () => wrapper.find('[data-testid="issueCreationError"]');
+ const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
+ const findViewIncidentBtn = () => wrapper.find('[data-testid="viewIncidentBtn"]');
+ const findIncidentCreationAlert = () => wrapper.find('[data-testid="incidentCreationError"]');
describe('Alert details', () => {
describe('when alert is null', () => {
@@ -81,11 +84,11 @@ describe('AlertDetails', () => {
});
it('renders a tab with overview information', () => {
- expect(wrapper.find('[data-testid="overviewTab"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="overview"]').exists()).toBe(true);
});
it('renders a tab with full alert information', () => {
- expect(wrapper.find('[data-testid="fullDetailsTab"]').exists()).toBe(true);
+ expect(wrapper.find('[data-testid="fullDetails"]').exists()).toBe(true);
});
it('renders severity', () => {
@@ -115,6 +118,8 @@ describe('AlertDetails', () => {
${'monitoringTool'} | ${undefined} | ${false}
${'service'} | ${'Prometheus'} | ${true}
${'service'} | ${undefined} | ${false}
+ ${'runbook'} | ${undefined} | ${false}
+ ${'runbook'} | ${'run.com'} | ${true}
`(`$desc`, ({ field, data, isShown }) => {
beforeEach(() => {
mountComponent({ data: { alert: { ...mockAlert, [field]: data } } });
@@ -130,18 +135,20 @@ describe('AlertDetails', () => {
});
});
- describe('Create issue from alert', () => {
- it('should display "View issue" button that links the issue page when issue exists', () => {
+ describe('Create incident from alert', () => {
+ it('should display "View incident" button that links the incident page when incident exists', () => {
const issueIid = '3';
mountComponent({
data: { alert: { ...mockAlert, issueIid }, sidebarStatus: false },
});
- expect(findViewIssueBtn().exists()).toBe(true);
- expect(findViewIssueBtn().attributes('href')).toBe(joinPaths(projectIssuesPath, issueIid));
- expect(findCreateIssueBtn().exists()).toBe(false);
+ expect(findViewIncidentBtn().exists()).toBe(true);
+ expect(findViewIncidentBtn().attributes('href')).toBe(
+ joinPaths(projectIssuesPath, issueIid),
+ );
+ expect(findCreateIncidentBtn().exists()).toBe(false);
});
- it('should display "Create issue" button when issue doesn\'t exist yet', () => {
+ it('should display "Create incident" button when incident doesn\'t exist yet', () => {
const issueIid = null;
mountComponent({
mountMethod: mount,
@@ -149,8 +156,8 @@ describe('AlertDetails', () => {
});
return wrapper.vm.$nextTick().then(() => {
- expect(findViewIssueBtn().exists()).toBe(false);
- expect(findCreateIssueBtn().exists()).toBe(true);
+ expect(findViewIncidentBtn().exists()).toBe(false);
+ expect(findCreateIncidentBtn().exists()).toBe(true);
});
});
@@ -160,7 +167,7 @@ describe('AlertDetails', () => {
.spyOn(wrapper.vm.$apollo, 'mutate')
.mockResolvedValue({ data: { createAlertIssue: { issue: { iid: issueIid } } } });
- findCreateIssueBtn().trigger('click');
+ findCreateIncidentBtn().trigger('click');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: createIssueMutation,
variables: {
@@ -170,7 +177,7 @@ describe('AlertDetails', () => {
});
});
- it('shows error alert when issue creation fails ', () => {
+ it('shows error alert when incident creation fails ', () => {
const errorMsg = 'Something went wrong';
mountComponent({
mountMethod: mount,
@@ -178,10 +185,10 @@ describe('AlertDetails', () => {
});
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockRejectedValue(errorMsg);
- findCreateIssueBtn().trigger('click');
+ findCreateIncidentBtn().trigger('click');
setImmediate(() => {
- expect(findIssueCreationAlert().text()).toBe(errorMsg);
+ expect(findIncidentCreationAlert().text()).toBe(errorMsg);
});
});
});
@@ -191,7 +198,7 @@ describe('AlertDetails', () => {
mountComponent({ data: { alert: mockAlert } });
});
it('should display a table of raw alert details data', () => {
- wrapper.find('[data-testid="fullDetailsTab"]').trigger('click');
+ wrapper.find('[data-testid="fullDetails"]').trigger('click');
expect(findDetailsTable().exists()).toBe(true);
});
});
@@ -252,6 +259,22 @@ describe('AlertDetails', () => {
);
});
});
+
+ describe('tab navigation', () => {
+ beforeEach(() => {
+ mountComponent({ data: { alert: mockAlert } });
+ });
+
+ it.each`
+ index | tabId
+ ${0} | ${'overview'}
+ ${1} | ${'fullDetails'}
+ ${2} | ${'metrics'}
+ `('will navigate to the correct tab via $tabId', ({ index, tabId }) => {
+ wrapper.setData({ currentTabIndex: index });
+ expect($router.replace).toHaveBeenCalledWith({ name: 'tab', params: { tabId } });
+ });
+ });
});
describe('Snowplow tracking', () => {
diff --git a/spec/frontend/alert_management/components/alert_management_empty_state_spec.js b/spec/frontend/alert_management/components/alert_management_empty_state_spec.js
index 0d1214211d3..6712282503d 100644
--- a/spec/frontend/alert_management/components/alert_management_empty_state_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_empty_state_spec.js
@@ -15,6 +15,7 @@ describe('AlertManagementEmptyState', () => {
wrapper = shallowMount(AlertManagementEmptyState, {
propsData: {
enableAlertManagementPath: '/link',
+ alertsHelpUrl: '/link',
emptyAlertSvgPath: 'illustration/path',
...props,
},
diff --git a/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js b/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js
index 4644406c037..c36107c28ce 100644
--- a/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_list_wrapper_spec.js
@@ -19,6 +19,7 @@ describe('AlertManagementList', () => {
propsData: {
projectPath: 'gitlab-org/gitlab',
enableAlertManagementPath: '/link',
+ alertsHelpUrl: '/link',
populatingAlertsHelpUrl: '/help/help-page.md#populating-alert-data',
emptyAlertSvgPath: 'illustration/path',
...props,
diff --git a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js b/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js
index fe08cf2c10a..2814b5ce357 100644
--- a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import SidebarTodo from '~/alert_management/components/sidebar/sidebar_todo.vue';
-import AlertMarkTodo from '~/alert_management/graphql/mutations/alert_todo_create.graphql';
+import AlertMarkTodo from '~/alert_management/graphql/mutations/alert_todo_create.mutation.graphql';
import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
@@ -34,6 +34,8 @@ describe('Alert Details Sidebar To Do', () => {
wrapper.destroy();
});
+ const findToDoButton = () => wrapper.find('[data-testid="alert-todo-button"]');
+
describe('updating the alert to do', () => {
const mockUpdatedMutationResult = {
data: {
@@ -44,25 +46,27 @@ describe('Alert Details Sidebar To Do', () => {
},
};
- beforeEach(() => {
- mountComponent({
- data: { alert: mockAlert },
- sidebarCollapsed: false,
- loading: false,
+ describe('adding a todo', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: { alert: mockAlert },
+ sidebarCollapsed: false,
+ loading: false,
+ });
});
- });
- it('renders a button for adding a To Do', () => {
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find('[data-testid="alert-todo-button"]').text()).toBe('Add a To Do');
+ it('renders a button for adding a To-Do', async () => {
+ await wrapper.vm.$nextTick();
+
+ expect(findToDoButton().text()).toBe('Add a To-Do');
});
- });
- it('calls `$apollo.mutate` with `AlertMarkTodo` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
+ it('calls `$apollo.mutate` with `AlertMarkTodo` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
+
+ findToDoButton().trigger('click');
+ await wrapper.vm.$nextTick();
- return wrapper.vm.$nextTick().then(() => {
- wrapper.find('button').trigger('click');
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: AlertMarkTodo,
variables: {
@@ -72,5 +76,28 @@ describe('Alert Details Sidebar To Do', () => {
});
});
});
+ describe('removing a todo', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: { alert: { ...mockAlert, todos: { nodes: [{ id: '1234' }] } } },
+ sidebarCollapsed: false,
+ loading: false,
+ });
+ });
+
+ it('renders a Mark As Done button when todo is present', async () => {
+ await wrapper.vm.$nextTick();
+
+ expect(findToDoButton().text()).toBe('Mark as done');
+ });
+
+ it('calls `$apollo.mutate` with `AlertMarkTodoDone` mutation and variables containing `id`', async () => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationResult);
+
+ findToDoButton().trigger('click');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ });
+ });
});
});
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 f316126432e..5dd0d9dc1ba 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -3,8 +3,8 @@ import {
GlTable,
GlAlert,
GlLoadingIcon,
- GlDropdown,
- GlDropdownItem,
+ GlDeprecatedDropdown,
+ GlDeprecatedDropdownItem,
GlIcon,
GlTabs,
GlTab,
@@ -12,6 +12,7 @@ import {
GlPagination,
GlSearchBoxByType,
} from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
import { visitUrl } from '~/lib/utils/url_utility';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import AlertManagementTable from '~/alert_management/components/alert_management_table.vue';
@@ -32,18 +33,19 @@ describe('AlertManagementTable', () => {
const findAlerts = () => wrapper.findAll('table tbody tr');
const findAlert = () => wrapper.find(GlAlert);
const findLoader = () => wrapper.find(GlLoadingIcon);
- const findStatusDropdown = () => wrapper.find(GlDropdown);
+ const findStatusDropdown = () => wrapper.find(GlDeprecatedDropdown);
const findStatusFilterTabs = () => wrapper.findAll(GlTab);
const findStatusTabs = () => wrapper.find(GlTabs);
const findStatusFilterBadge = () => wrapper.findAll(GlBadge);
const findDateFields = () => wrapper.findAll(TimeAgo);
- const findFirstStatusOption = () => findStatusDropdown().find(GlDropdownItem);
+ const findFirstStatusOption = () => findStatusDropdown().find(GlDeprecatedDropdownItem);
const findAssignees = () => wrapper.findAll('[data-testid="assigneesField"]');
const findSeverityFields = () => wrapper.findAll('[data-testid="severityField"]');
const findSeverityColumnHeader = () => wrapper.findAll('th').at(0);
const findPagination = () => wrapper.find(GlPagination);
const findSearch = () => wrapper.find(GlSearchBoxByType);
const findIssueFields = () => wrapper.findAll('[data-testid="issueField"]');
+ const findAlertError = () => wrapper.find('[data-testid="alert-error"]');
const alertsCount = {
open: 14,
triggered: 10,
@@ -51,6 +53,11 @@ describe('AlertManagementTable', () => {
resolved: 1,
all: 16,
};
+ const selectFirstStatusOption = () => {
+ findFirstStatusOption().vm.$emit('click');
+
+ return waitForPromises();
+ };
function mountComponent({
props = {
@@ -138,7 +145,7 @@ describe('AlertManagementTable', () => {
it('error state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { errors: ['error'] }, alertsCount: null, errored: true },
+ data: { alerts: { errors: ['error'] }, alertsCount: null, hasError: true },
loading: false,
});
expect(findAlertsTable().exists()).toBe(true);
@@ -155,7 +162,7 @@ describe('AlertManagementTable', () => {
it('empty state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: [], pageInfo: {} }, alertsCount: { all: 0 }, errored: false },
+ data: { alerts: { list: [], pageInfo: {} }, alertsCount: { all: 0 }, hasError: false },
loading: false,
});
expect(findAlertsTable().exists()).toBe(true);
@@ -172,7 +179,7 @@ describe('AlertManagementTable', () => {
it('has data state', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
+ data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
expect(findLoader().exists()).toBe(false);
@@ -188,7 +195,7 @@ describe('AlertManagementTable', () => {
it('displays status dropdown', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
+ data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
expect(findStatusDropdown().exists()).toBe(true);
@@ -197,7 +204,7 @@ describe('AlertManagementTable', () => {
it('does not display a dropdown status header', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
+ data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
expect(findStatusDropdown().contains('.dropdown-title')).toBe(false);
@@ -206,7 +213,7 @@ describe('AlertManagementTable', () => {
it('shows correct severity icons', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
+ data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
@@ -223,7 +230,7 @@ describe('AlertManagementTable', () => {
it('renders severity text', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
+ data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
@@ -237,7 +244,7 @@ describe('AlertManagementTable', () => {
it('renders Unassigned when no assignee(s) present', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
+ data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
@@ -251,7 +258,7 @@ describe('AlertManagementTable', () => {
it('renders username(s) when assignee(s) present', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
+ data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
@@ -265,7 +272,7 @@ describe('AlertManagementTable', () => {
it('navigates to the detail page when alert row is clicked', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
+ data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
@@ -279,7 +286,7 @@ describe('AlertManagementTable', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
+ data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
});
@@ -323,7 +330,7 @@ describe('AlertManagementTable', () => {
],
},
alertsCount,
- errored: false,
+ hasError: false,
},
loading: false,
});
@@ -343,7 +350,7 @@ describe('AlertManagementTable', () => {
},
],
alertsCount,
- errored: false,
+ hasError: false,
},
loading: false,
});
@@ -358,7 +365,7 @@ describe('AlertManagementTable', () => {
it('should highlight the row when alert is new', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: [newAlert] }, alertsCount, errored: false },
+ data: { alerts: { list: [newAlert] }, alertsCount, hasError: false },
loading: false,
});
@@ -372,7 +379,7 @@ describe('AlertManagementTable', () => {
it('should not highlight the row when alert is not new', () => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: [oldAlert] }, alertsCount, errored: false },
+ data: { alerts: { list: [oldAlert] }, alertsCount, hasError: false },
loading: false,
});
@@ -392,7 +399,7 @@ describe('AlertManagementTable', () => {
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
data: {
alerts: { list: mockAlerts },
- errored: false,
+ hasError: false,
sort: 'STARTED_AT_DESC',
alertsCount,
},
@@ -429,7 +436,7 @@ describe('AlertManagementTable', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
+ data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
});
@@ -448,19 +455,36 @@ describe('AlertManagementTable', () => {
});
});
- it('shows an error when request fails', () => {
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
- findFirstStatusOption().vm.$emit('click');
- wrapper.setData({
- errored: true,
+ describe('when a request fails', () => {
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockReturnValue(Promise.reject(new Error()));
});
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('[data-testid="alert-error"]').exists()).toBe(true);
+ it('shows an error', async () => {
+ await selectFirstStatusOption();
+
+ expect(findAlertError().text()).toContain(
+ 'There was an error while updating the status of the alert.',
+ );
+ });
+
+ it('shows an error when triggered a second time', async () => {
+ await selectFirstStatusOption();
+
+ wrapper.find(GlAlert).vm.$emit('dismiss');
+
+ await wrapper.vm.$nextTick();
+
+ // Assert that the error has been dismissed in the setup
+ expect(findAlertError().exists()).toBe(false);
+
+ await selectFirstStatusOption();
+
+ expect(findAlertError().exists()).toBe(true);
});
});
- it('shows an error when response includes HTML errors', () => {
+ it('shows an error when response includes HTML errors', async () => {
const mockUpdatedMutationErrorResult = {
data: {
updateAlertStatus: {
@@ -474,13 +498,11 @@ describe('AlertManagementTable', () => {
};
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue(mockUpdatedMutationErrorResult);
- findFirstStatusOption().vm.$emit('click');
- wrapper.setData({ errored: true });
- return wrapper.vm.$nextTick(() => {
- expect(wrapper.contains('[data-testid="alert-error"]')).toBe(true);
- expect(wrapper.contains('[data-testid="htmlError"]')).toBe(true);
- });
+ await selectFirstStatusOption();
+
+ expect(findAlertError().exists()).toBe(true);
+ expect(findAlertError().contains('[data-testid="htmlError"]')).toBe(true);
});
});
@@ -510,7 +532,7 @@ describe('AlertManagementTable', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: mockAlerts, pageInfo: {} }, alertsCount, errored: false },
+ data: { alerts: { list: mockAlerts, pageInfo: {} }, alertsCount, hasError: false },
loading: false,
});
});
@@ -570,7 +592,7 @@ describe('AlertManagementTable', () => {
beforeEach(() => {
mountComponent({
props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alerts: { list: mockAlerts }, alertsCount, errored: false },
+ data: { alerts: { list: mockAlerts }, alertsCount, hasError: false },
loading: false,
});
});
diff --git a/spec/frontend/alert_management/components/alert_metrics_spec.js b/spec/frontend/alert_management/components/alert_metrics_spec.js
index c188363ddc2..e0a069fa1a8 100644
--- a/spec/frontend/alert_management/components/alert_metrics_spec.js
+++ b/spec/frontend/alert_management/components/alert_metrics_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import AlertMetrics from '~/alert_management/components/alert_metrics.vue';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
+import AlertMetrics from '~/alert_management/components/alert_metrics.vue';
jest.mock('~/monitoring/stores', () => ({
monitoringDashboard: {},
diff --git a/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js b/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js
index db086782424..a14596b6722 100644
--- a/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js
+++ b/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdownItem } from '@gitlab/ui';
import SidebarAssignee from '~/alert_management/components/sidebar/sidebar_assignee.vue';
import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue';
import AlertSetAssignees from '~/alert_management/graphql/mutations/alert_set_assignees.mutation.graphql';
@@ -103,7 +103,7 @@ describe('Alert Details Sidebar Assignees', () => {
it('renders a unassigned option', () => {
wrapper.setData({ isDropdownSearching: false });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDropdownItem).text()).toBe('Unassigned');
+ expect(wrapper.find(GlDeprecatedDropdownItem).text()).toBe('Unassigned');
});
});
diff --git a/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js b/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js
index c2eaf540e9c..5bd0d3b3c17 100644
--- a/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js
+++ b/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem, GlLoadingIcon } from '@gitlab/ui';
import { trackAlertStatusUpdateOptions } from '~/alert_management/constants';
import AlertSidebarStatus from '~/alert_management/components/sidebar/sidebar_status.vue';
import updateAlertStatus from '~/alert_management/graphql/mutations/update_alert_status.mutation.graphql';
@@ -10,8 +10,8 @@ const mockAlert = mockAlerts[0];
describe('Alert Details Sidebar Status', () => {
let wrapper;
- const findStatusDropdown = () => wrapper.find(GlDropdown);
- const findStatusDropdownItem = () => wrapper.find(GlDropdownItem);
+ const findStatusDropdown = () => wrapper.find(GlDeprecatedDropdown);
+ const findStatusDropdownItem = () => wrapper.find(GlDeprecatedDropdownItem);
const findStatusLoadingIcon = () => wrapper.find(GlLoadingIcon);
function mountComponent({ data, sidebarCollapsed = true, loading = false, stubs = {} } = {}) {
diff --git a/spec/frontend/alert_management/mocks/alerts.json b/spec/frontend/alert_management/mocks/alerts.json
index f63019d1e5c..fec101a52b4 100644
--- a/spec/frontend/alert_management/mocks/alerts.json
+++ b/spec/frontend/alert_management/mocks/alerts.json
@@ -9,7 +9,8 @@
"endedAt": "2020-04-17T23:18:14.996Z",
"status": "TRIGGERED",
"assignees": { "nodes": [] },
- "notes": { "nodes": [] }
+ "notes": { "nodes": [] },
+ "todos": { "nodes": [] }
},
{
"iid": "1527543",
@@ -37,7 +38,8 @@
"systemNoteIconName": "user"
}
]
- }
+ },
+ "todos": { "nodes": [] }
},
{
"iid": "1527544",
@@ -63,6 +65,7 @@
}
}
]
- }
+ },
+ "todos": { "nodes": [] }
}
]
diff --git a/spec/frontend/alert_settings/__snapshots__/alert_settings_form_spec.js.snap b/spec/frontend/alert_settings/__snapshots__/alert_settings_form_spec.js.snap
index 1f5c3a80fbb..16e92bf505a 100644
--- a/spec/frontend/alert_settings/__snapshots__/alert_settings_form_spec.js.snap
+++ b/spec/frontend/alert_settings/__snapshots__/alert_settings_form_spec.js.snap
@@ -13,20 +13,20 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
</div>
<gl-form-stub>
<gl-form-group-stub label=\\"Integrations\\" label-for=\\"integrations\\" label-class=\\"label-bold\\">
- <gl-form-select-stub options=\\"[object Object],[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"generic\\"></gl-form-select-stub> <span class=\\"gl-text-gray-400\\"><gl-sprintf-stub message=\\"Learn more about our %{linkStart}upcoming integrations%{linkEnd}\\"></gl-sprintf-stub></span>
+ <gl-form-select-stub options=\\"[object Object],[object Object],[object Object]\\" data-testid=\\"alert-settings-select\\" value=\\"generic\\"></gl-form-select-stub> <span class=\\"gl-text-gray-200\\"><gl-sprintf-stub message=\\"Learn more about our %{linkStart}upcoming integrations%{linkEnd}\\"></gl-sprintf-stub></span>
</gl-form-group-stub>
<gl-form-group-stub label=\\"Active\\" label-for=\\"activated\\" label-class=\\"label-bold\\">
<toggle-button-stub id=\\"activated\\"></toggle-button-stub>
</gl-form-group-stub>
<!---->
<gl-form-group-stub label=\\"Webhook URL\\" label-for=\\"url\\" label-class=\\"label-bold\\">
- <gl-form-input-group-stub value=\\"/alerts/notify.json\\" predefinedoptions=\\"[object Object]\\" id=\\"url\\" readonly=\\"\\"></gl-form-input-group-stub> <span class=\\"gl-text-gray-400\\">
+ <gl-form-input-group-stub value=\\"/alerts/notify.json\\" predefinedoptions=\\"[object Object]\\" id=\\"url\\" readonly=\\"\\"></gl-form-input-group-stub> <span class=\\"gl-text-gray-200\\">
</span>
</gl-form-group-stub>
<gl-form-group-stub label=\\"Authorization key\\" label-for=\\"authorization-key\\" label-class=\\"label-bold\\">
<gl-form-input-group-stub value=\\"abcedfg123\\" predefinedoptions=\\"[object Object]\\" id=\\"authorization-key\\" readonly=\\"\\" class=\\"gl-mb-2\\"></gl-form-input-group-stub>
- <gl-button-stub category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub>
+ <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\" class=\\"gl-mt-3\\" role=\\"button\\" tabindex=\\"0\\">Reset key</gl-button-stub>
<gl-modal-stub modalid=\\"authKeyModal\\" titletag=\\"h4\\" modalclass=\\"\\" size=\\"md\\" title=\\"Reset key\\" ok-title=\\"Reset key\\" ok-variant=\\"danger\\">
Resetting the authorization key for this project will require updating the authorization key in every alert source it is enabled in.
</gl-modal-stub>
@@ -34,14 +34,16 @@ exports[`AlertsSettingsForm with default values renders the initial template 1`]
<gl-form-group-stub label=\\"Alert test payload\\" label-for=\\"alert-json\\" label-class=\\"label-bold\\">
<gl-form-textarea-stub noresize=\\"true\\" id=\\"alert-json\\" disabled=\\"true\\" state=\\"true\\" placeholder=\\"Enter test alert JSON....\\" rows=\\"6\\" max-rows=\\"10\\"></gl-form-textarea-stub>
</gl-form-group-stub>
- <gl-button-stub category=\\"tertiary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">Test alert payload</gl-button-stub>
+ <div class=\\"gl-display-flex gl-justify-content-end\\">
+ <gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">Test alert payload</gl-button-stub>
+ </div>
<div class=\\"footer-block row-content-block gl-display-flex gl-justify-content-space-between\\">
- <gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">
- Save changes
- </gl-button-stub>
<gl-button-stub category=\\"primary\\" variant=\\"default\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">
Cancel
</gl-button-stub>
+ <gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" disabled=\\"true\\">
+ Save changes
+ </gl-button-stub>
</div>
</gl-form-stub>
</div>"
diff --git a/spec/frontend/alert_settings/alert_settings_form_spec.js b/spec/frontend/alert_settings/alert_settings_form_spec.js
index 5a04d768645..87a631bda56 100644
--- a/spec/frontend/alert_settings/alert_settings_form_spec.js
+++ b/spec/frontend/alert_settings/alert_settings_form_spec.js
@@ -11,41 +11,36 @@ const KEY = 'abcedfg123';
const INVALID_URL = 'http://invalid';
const ACTIVATED = false;
-const defaultProps = {
- generic: {
- initialAuthorizationKey: KEY,
- formPath: INVALID_URL,
- url: GENERIC_URL,
- alertsSetupUrl: INVALID_URL,
- alertsUsageUrl: INVALID_URL,
- activated: ACTIVATED,
- },
- prometheus: {
- prometheusAuthorizationKey: KEY,
- prometheusFormPath: INVALID_URL,
- prometheusUrl: PROMETHEUS_URL,
- activated: ACTIVATED,
- },
- opsgenie: {
- opsgenieMvcIsAvailable: true,
- formPath: INVALID_URL,
- activated: ACTIVATED,
- opsgenieMvcTargetUrl: GENERIC_URL,
- },
-};
-
describe('AlertsSettingsForm', () => {
let wrapper;
let mockAxios;
- const createComponent = (props = defaultProps, { methods } = {}, data) => {
+ const createComponent = ({ methods } = {}, data) => {
wrapper = shallowMount(AlertsSettingsForm, {
data() {
return { ...data };
},
- propsData: {
- ...defaultProps,
- ...props,
+ provide: {
+ generic: {
+ authorizationKey: KEY,
+ formPath: INVALID_URL,
+ url: GENERIC_URL,
+ alertsSetupUrl: INVALID_URL,
+ alertsUsageUrl: INVALID_URL,
+ activated: ACTIVATED,
+ },
+ prometheus: {
+ authorizationKey: KEY,
+ prometheusFormPath: INVALID_URL,
+ prometheusUrl: PROMETHEUS_URL,
+ activated: ACTIVATED,
+ },
+ opsgenie: {
+ opsgenieMvcIsAvailable: true,
+ formPath: INVALID_URL,
+ activated: ACTIVATED,
+ opsgenieMvcTargetUrl: GENERIC_URL,
+ },
},
methods,
});
@@ -83,32 +78,33 @@ describe('AlertsSettingsForm', () => {
describe('reset key', () => {
it('triggers resetKey method', () => {
- const resetGenericKey = jest.fn();
- const methods = { resetGenericKey };
- createComponent(defaultProps, { methods });
+ const resetKey = jest.fn();
+ const methods = { resetKey };
+ createComponent({ methods });
wrapper.find(GlModal).vm.$emit('ok');
- expect(resetGenericKey).toHaveBeenCalled();
+ expect(resetKey).toHaveBeenCalled();
});
it('updates the authorization key on success', () => {
- const formPath = 'some/path';
- mockAxios.onPut(formPath, { service: { token: '' } }).replyOnce(200, { token: 'newToken' });
- createComponent({ generic: { ...defaultProps.generic, formPath } });
+ createComponent(
+ {},
+ {
+ authKey: 'newToken',
+ },
+ );
- return wrapper.vm.resetGenericKey().then(() => {
- expect(findAuthorizationKey().attributes('value')).toBe('newToken');
- });
+ expect(findAuthorizationKey().attributes('value')).toBe('newToken');
});
it('shows a alert message on error', () => {
const formPath = 'some/path';
mockAxios.onPut(formPath).replyOnce(404);
- createComponent({ generic: { ...defaultProps.generic, formPath } });
+ createComponent();
- return wrapper.vm.resetGenericKey().then(() => {
+ return wrapper.vm.resetKey().then(() => {
expect(wrapper.find(GlAlert).exists()).toBe(true);
});
});
@@ -118,22 +114,18 @@ describe('AlertsSettingsForm', () => {
it('triggers toggleActivated method', () => {
const toggleService = jest.fn();
const methods = { toggleService };
- createComponent(defaultProps, { methods });
+ createComponent({ methods });
wrapper.find(ToggleButton).vm.$emit('change', true);
-
expect(toggleService).toHaveBeenCalled();
});
describe('error is encountered', () => {
- beforeEach(() => {
+ it('restores previous value', () => {
const formPath = 'some/path';
mockAxios.onPut(formPath).replyOnce(500);
- });
-
- it('restores previous value', () => {
- createComponent({ generic: { ...defaultProps.generic, initialActivated: false } });
- return wrapper.vm.resetGenericKey().then(() => {
+ createComponent();
+ return wrapper.vm.resetKey().then(() => {
expect(wrapper.find(ToggleButton).props('value')).toBe(false);
});
});
@@ -143,7 +135,6 @@ describe('AlertsSettingsForm', () => {
describe('prometheus is active', () => {
beforeEach(() => {
createComponent(
- { prometheus: { ...defaultProps.prometheus, prometheusIsActivated: true } },
{},
{
selectedEndpoint: 'prometheus',
@@ -164,10 +155,9 @@ describe('AlertsSettingsForm', () => {
});
});
- describe('opsgenie is active', () => {
+ describe('Opsgenie is active', () => {
beforeEach(() => {
createComponent(
- { opsgenie: { ...defaultProps.opsgenie, opsgenieMvcActivated: true } },
{},
{
selectedEndpoint: 'opsgenie',
@@ -175,15 +165,14 @@ describe('AlertsSettingsForm', () => {
);
});
- it('shows a input for the opsgenie target URL', () => {
+ it('shows a input for the Opsgenie target URL', () => {
expect(findApiUrl().exists()).toBe(true);
- expect(findSelect().attributes('value')).toBe('opsgenie');
});
});
describe('trigger test alert', () => {
beforeEach(() => {
- createComponent({ generic: { ...defaultProps.generic, initialActivated: true } }, {}, true);
+ createComponent({});
});
it('should enable the JSON input', () => {
@@ -191,30 +180,19 @@ describe('AlertsSettingsForm', () => {
expect(findJsonInput().props('value')).toBe(null);
});
- it('should validate JSON input', () => {
- createComponent({ generic: { ...defaultProps.generic } }, true, {
+ it('should validate JSON input', async () => {
+ createComponent(true, {
testAlertJson: '{ "value": "test" }',
});
findJsonInput().vm.$emit('change');
- return wrapper.vm.$nextTick().then(() => {
- expect(findJsonInput().attributes('state')).toBe('true');
- });
- });
-
- describe('alert service is toggled', () => {
- it('should show a info alert if successful', () => {
- const formPath = 'some/path';
- const toggleService = true;
- mockAxios.onPut(formPath).replyOnce(200);
- createComponent({ generic: { ...defaultProps.generic, formPath } });
+ await wrapper.vm.$nextTick();
- return wrapper.vm.toggleActivated(toggleService).then(() => {
- expect(wrapper.find(GlAlert).attributes('variant')).toBe('info');
- });
- });
+ expect(findJsonInput().attributes('state')).toBe('true');
+ });
+ describe('alert service is toggled', () => {
it('should show a error alert if failed', () => {
const formPath = 'some/path';
const toggleService = true;
@@ -222,9 +200,10 @@ describe('AlertsSettingsForm', () => {
errors: 'Error message to display',
});
- createComponent({ generic: { ...defaultProps.generic, formPath } });
+ createComponent();
return wrapper.vm.toggleActivated(toggleService).then(() => {
+ expect(wrapper.vm.active).toBe(false);
expect(wrapper.find(GlAlert).attributes('variant')).toBe('danger');
});
});
diff --git a/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js b/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js
index 610f9d6b9bd..5574c83eb76 100644
--- a/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js
+++ b/spec/frontend/alerts_service_settings/components/alerts_service_form_spec.js
@@ -4,7 +4,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
import AlertsServiceForm from '~/alerts_service_settings/components/alerts_service_form.vue';
import ToggleButton from '~/vue_shared/components/toggle_button.vue';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/analytics/components/activity_chart_spec.js b/spec/frontend/analytics/components/activity_chart_spec.js
new file mode 100644
index 00000000000..1f0f9a6c5d7
--- /dev/null
+++ b/spec/frontend/analytics/components/activity_chart_spec.js
@@ -0,0 +1,39 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import ActivityChart from '~/analytics/product_analytics/components/activity_chart.vue';
+
+describe('Activity Chart Bundle', () => {
+ let wrapper;
+ function mountComponent({ provide }) {
+ wrapper = shallowMount(ActivityChart, {
+ provide: {
+ formattedData: {},
+ ...provide,
+ },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findChart = () => wrapper.find(GlColumnChart);
+ const findNoData = () => wrapper.find('[data-testid="noActivityChartData"]');
+
+ describe('Activity Chart', () => {
+ it('renders an warning message with no data', () => {
+ mountComponent({ provide: { formattedData: {} } });
+ expect(findNoData().exists()).toBe(true);
+ });
+
+ it('renders a chart with data', () => {
+ mountComponent({
+ provide: { formattedData: { keys: ['key1', 'key2'], values: [5038, 2241] } },
+ });
+
+ expect(findNoData().exists()).toBe(false);
+ expect(findChart().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index c94637e04af..4f4de62c229 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Api from '~/api';
+import httpStatus from '~/lib/utils/http_status';
describe('Api', () => {
const dummyApiVersion = 'v3000';
@@ -57,7 +58,7 @@ describe('Api', () => {
it('fetch all group packages', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/packages`;
jest.spyOn(axios, 'get');
- mock.onGet(expectedUrl).replyOnce(200, apiResponse);
+ mock.onGet(expectedUrl).replyOnce(httpStatus.OK, apiResponse);
return Api.groupPackages(groupId).then(({ data }) => {
expect(data).toEqual(apiResponse);
@@ -70,7 +71,7 @@ describe('Api', () => {
it('fetch all project packages', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/packages`;
jest.spyOn(axios, 'get');
- mock.onGet(expectedUrl).replyOnce(200, apiResponse);
+ mock.onGet(expectedUrl).replyOnce(httpStatus.OK, apiResponse);
return Api.projectPackages(projectId).then(({ data }) => {
expect(data).toEqual(apiResponse);
@@ -92,7 +93,7 @@ describe('Api', () => {
const expectedUrl = `foo`;
jest.spyOn(Api, 'buildProjectPackageUrl').mockReturnValue(expectedUrl);
jest.spyOn(axios, 'get');
- mock.onGet(expectedUrl).replyOnce(200, apiResponse);
+ mock.onGet(expectedUrl).replyOnce(httpStatus.OK, apiResponse);
return Api.projectPackage(projectId, packageId).then(({ data }) => {
expect(data).toEqual(apiResponse);
@@ -107,7 +108,7 @@ describe('Api', () => {
jest.spyOn(Api, 'buildProjectPackageUrl').mockReturnValue(expectedUrl);
jest.spyOn(axios, 'delete');
- mock.onDelete(expectedUrl).replyOnce(200, true);
+ mock.onDelete(expectedUrl).replyOnce(httpStatus.OK, true);
return Api.deleteProjectPackage(projectId, packageId).then(({ data }) => {
expect(data).toEqual(true);
@@ -121,7 +122,7 @@ describe('Api', () => {
it('fetches a group', done => {
const groupId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`;
- mock.onGet(expectedUrl).reply(200, {
+ mock.onGet(expectedUrl).reply(httpStatus.OK, {
name: 'test',
});
@@ -137,7 +138,7 @@ describe('Api', () => {
const groupId = '54321';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/members`;
const expectedData = [{ id: 7 }];
- mock.onGet(expectedUrl).reply(200, expectedData);
+ mock.onGet(expectedUrl).reply(httpStatus.OK, expectedData);
Api.groupMembers(groupId)
.then(({ data }) => {
@@ -148,12 +149,42 @@ describe('Api', () => {
});
});
+ describe('groupMilestones', () => {
+ it('fetches group milestones', done => {
+ const groupId = '16';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/milestones`;
+ const expectedData = [
+ {
+ id: 12,
+ iid: 3,
+ group_id: 16,
+ title: '10.0',
+ description: 'Version',
+ due_date: '2013-11-29',
+ start_date: '2013-11-10',
+ state: 'active',
+ updated_at: '2013-10-02T09:24:18Z',
+ created_at: '2013-10-02T09:24:18Z',
+ web_url: 'https://gitlab.com/groups/gitlab-org/-/milestones/42',
+ },
+ ];
+ mock.onGet(expectedUrl).reply(httpStatus.OK, expectedData);
+
+ Api.groupMilestones(groupId)
+ .then(({ data }) => {
+ expect(data).toEqual(expectedData);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
describe('groups', () => {
it('fetches groups', done => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`;
- mock.onGet(expectedUrl).reply(200, [
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [
{
name: 'test',
},
@@ -171,7 +202,7 @@ describe('Api', () => {
it('fetches namespaces', done => {
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`;
- mock.onGet(expectedUrl).reply(200, [
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [
{
name: 'test',
},
@@ -191,7 +222,7 @@ describe('Api', () => {
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
window.gon.current_user_id = 1;
- mock.onGet(expectedUrl).reply(200, [
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [
{
name: 'test',
},
@@ -208,7 +239,7 @@ describe('Api', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
- mock.onGet(expectedUrl).reply(200, [
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [
{
name: 'test',
},
@@ -226,7 +257,7 @@ describe('Api', () => {
it('update a project with the given payload', done => {
const projectPath = 'foo';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}`;
- mock.onPut(expectedUrl).reply(200, { foo: 'bar' });
+ mock.onPut(expectedUrl).reply(httpStatus.OK, { foo: 'bar' });
Api.updateProject(projectPath, { foo: 'bar' })
.then(({ data }) => {
@@ -243,7 +274,7 @@ describe('Api', () => {
const options = { unused: 'option' };
const projectPath = 'gitlab-org%2Fgitlab-ce';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/users`;
- mock.onGet(expectedUrl).reply(200, [
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [
{
name: 'test',
},
@@ -265,7 +296,7 @@ describe('Api', () => {
it('fetches all merge requests for a project', done => {
const mockData = [{ source_branch: 'foo' }, { source_branch: 'bar' }];
- mock.onGet(expectedUrl).reply(200, mockData);
+ mock.onGet(expectedUrl).reply(httpStatus.OK, mockData);
Api.projectMergeRequests(projectPath)
.then(({ data }) => {
expect(data.length).toEqual(2);
@@ -281,7 +312,7 @@ describe('Api', () => {
source_branch: 'bar',
};
const mockData = [{ source_branch: 'bar' }];
- mock.onGet(expectedUrl, { params }).reply(200, mockData);
+ mock.onGet(expectedUrl, { params }).reply(httpStatus.OK, mockData);
Api.projectMergeRequests(projectPath, params)
.then(({ data }) => {
@@ -298,7 +329,7 @@ describe('Api', () => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}`;
- mock.onGet(expectedUrl).reply(200, {
+ mock.onGet(expectedUrl).reply(httpStatus.OK, {
title: 'test',
});
@@ -316,7 +347,7 @@ describe('Api', () => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/changes`;
- mock.onGet(expectedUrl).reply(200, {
+ mock.onGet(expectedUrl).reply(httpStatus.OK, {
title: 'test',
});
@@ -334,7 +365,7 @@ describe('Api', () => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/versions`;
- mock.onGet(expectedUrl).reply(200, [
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [
{
id: 123,
},
@@ -356,7 +387,7 @@ describe('Api', () => {
const params = { scope: 'active' };
const mockData = [{ id: 4 }];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/runners`;
- mock.onGet(expectedUrl, { params }).reply(200, mockData);
+ mock.onGet(expectedUrl, { params }).reply(httpStatus.OK, mockData);
Api.projectRunners(projectPath, { params })
.then(({ data }) => {
@@ -380,7 +411,7 @@ describe('Api', () => {
expect(config.data).toBe(JSON.stringify(expectedData));
return [
- 200,
+ httpStatus.OK,
{
name: 'test',
},
@@ -404,7 +435,7 @@ describe('Api', () => {
expect(config.data).toBe(JSON.stringify(expectedData));
return [
- 200,
+ httpStatus.OK,
{
name: 'test',
},
@@ -423,7 +454,7 @@ describe('Api', () => {
const groupId = '123456';
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
- mock.onGet(expectedUrl).reply(200, [
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [
{
name: 'test',
},
@@ -445,7 +476,7 @@ describe('Api', () => {
)}/repository/commits/${sha}`;
it('fetches a single commit', () => {
- mock.onGet(expectedUrl).reply(200, { id: sha });
+ mock.onGet(expectedUrl).reply(httpStatus.OK, { id: sha });
return Api.commit(projectId, sha).then(({ data: commit }) => {
expect(commit.id).toBe(sha);
@@ -453,7 +484,7 @@ describe('Api', () => {
});
it('fetches a single commit without stats', () => {
- mock.onGet(expectedUrl, { params: { stats: false } }).reply(200, { id: sha });
+ mock.onGet(expectedUrl, { params: { stats: false } }).reply(httpStatus.OK, { id: sha });
return Api.commit(projectId, sha, { stats: false }).then(({ data: commit }) => {
expect(commit.id).toBe(sha);
@@ -470,7 +501,7 @@ describe('Api', () => {
const expectedUrl = `${dummyUrlRoot}/${namespace}/${project}/templates/${templateType}/${encodeURIComponent(
templateKey,
)}`;
- mock.onGet(expectedUrl).reply(200, 'test');
+ mock.onGet(expectedUrl).reply(httpStatus.OK, 'test');
Api.issueTemplate(namespace, project, templateKey, templateType, (error, response) => {
expect(response).toBe('test');
@@ -483,7 +514,7 @@ describe('Api', () => {
it('fetches a list of templates', done => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/gitlab-org%2Fgitlab-ce/templates/licenses`;
- mock.onGet(expectedUrl).reply(200, 'test');
+ mock.onGet(expectedUrl).reply(httpStatus.OK, 'test');
Api.projectTemplates('gitlab-org/gitlab-ce', 'licenses', {}, response => {
expect(response).toBe('test');
@@ -497,7 +528,7 @@ describe('Api', () => {
const data = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/gitlab-org%2Fgitlab-ce/templates/licenses/test%20license`;
- mock.onGet(expectedUrl).reply(200, 'test');
+ mock.onGet(expectedUrl).reply(httpStatus.OK, 'test');
Api.projectTemplate('gitlab-org/gitlab-ce', 'licenses', 'test license', data, response => {
expect(response).toBe('test');
@@ -511,7 +542,7 @@ describe('Api', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`;
- mock.onGet(expectedUrl).reply(200, [
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [
{
name: 'test',
},
@@ -531,7 +562,7 @@ describe('Api', () => {
it('fetches single user', done => {
const userId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}`;
- mock.onGet(expectedUrl).reply(200, {
+ mock.onGet(expectedUrl).reply(httpStatus.OK, {
name: 'testuser',
});
@@ -547,7 +578,7 @@ describe('Api', () => {
describe('user counts', () => {
it('fetches single user counts', done => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/user_counts`;
- mock.onGet(expectedUrl).reply(200, {
+ mock.onGet(expectedUrl).reply(httpStatus.OK, {
merge_requests: 4,
});
@@ -564,7 +595,7 @@ describe('Api', () => {
it('fetches single user status', done => {
const userId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}/status`;
- mock.onGet(expectedUrl).reply(200, {
+ mock.onGet(expectedUrl).reply(httpStatus.OK, {
message: 'testmessage',
});
@@ -583,7 +614,7 @@ describe('Api', () => {
const options = { unused: 'option' };
const userId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}/projects`;
- mock.onGet(expectedUrl).reply(200, [
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [
{
name: 'test',
},
@@ -602,7 +633,7 @@ describe('Api', () => {
const projectId = 'example/foobar';
const commitSha = 'abc123def';
const expectedUrl = `${dummyUrlRoot}/${projectId}/commit/${commitSha}/pipelines`;
- mock.onGet(expectedUrl).reply(200, [
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [
{
name: 'test',
},
@@ -629,7 +660,7 @@ describe('Api', () => {
jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl).replyOnce(200, {
+ mock.onPost(expectedUrl).replyOnce(httpStatus.OK, {
name: branch,
});
@@ -652,7 +683,7 @@ describe('Api', () => {
jest.spyOn(axios, 'get');
- mock.onGet(expectedUrl).replyOnce(200, ['fork']);
+ mock.onGet(expectedUrl).replyOnce(httpStatus.OK, ['fork']);
Api.projectForks(dummyProjectPath, { visibility: 'private' })
.then(({ data }) => {
@@ -666,62 +697,239 @@ describe('Api', () => {
});
});
- describe('createReleaseLink', () => {
+ describe('createContextCommits', () => {
+ it('creates a new context commit', done => {
+ const projectPath = 'abc';
+ const mergeRequestId = '123456';
+ const commitsData = ['abcdefg'];
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/context_commits`;
+ const expectedData = {
+ commits: commitsData,
+ };
+
+ jest.spyOn(axios, 'post');
+
+ mock.onPost(expectedUrl).replyOnce(200, [
+ {
+ id: 'abcdefghijklmnop',
+ short_id: 'abcdefg',
+ title: 'Dummy commit',
+ },
+ ]);
+
+ Api.createContextCommits(projectPath, mergeRequestId, expectedData)
+ .then(({ data }) => {
+ expect(data[0].title).toBe('Dummy commit');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('allContextCommits', () => {
+ it('gets all context commits', done => {
+ const projectPath = 'abc';
+ const mergeRequestId = '123456';
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/context_commits`;
+
+ jest.spyOn(axios, 'get');
+
+ mock
+ .onGet(expectedUrl)
+ .replyOnce(200, [{ id: 'abcdef', short_id: 'abcdefghi', title: 'Dummy commit title' }]);
+
+ Api.allContextCommits(projectPath, mergeRequestId)
+ .then(({ data }) => {
+ expect(data[0].title).toBe('Dummy commit title');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('removeContextCommits', () => {
+ it('removes context commits', done => {
+ const projectPath = 'abc';
+ const mergeRequestId = '123456';
+ const commitsData = ['abcdefg'];
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/context_commits`;
+ const expectedData = {
+ commits: commitsData,
+ };
+
+ jest.spyOn(axios, 'delete');
+
+ mock.onDelete(expectedUrl).replyOnce(204);
+
+ Api.removeContextCommits(projectPath, mergeRequestId, expectedData)
+ .then(() => {
+ expect(axios.delete).toHaveBeenCalledWith(expectedUrl, { data: expectedData });
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('release-related methods', () => {
const dummyProjectPath = 'gitlab-org/gitlab';
- const dummyReleaseTag = 'v1.3';
- const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent(
+ const dummyTagName = 'v1.3';
+ const baseReleaseUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent(
dummyProjectPath,
- )}/releases/${dummyReleaseTag}/assets/links`;
- const expectedLink = {
- url: 'https://example.com',
- name: 'An example link',
- };
+ )}/releases`;
- describe('when the Release is successfully created', () => {
- it('resolves the Promise', () => {
- mock.onPost(expectedUrl, expectedLink).replyOnce(201);
+ describe('releases', () => {
+ const expectedUrl = baseReleaseUrl;
- return Api.createReleaseLink(dummyProjectPath, dummyReleaseTag, expectedLink).then(() => {
- expect(mock.history.post).toHaveLength(1);
+ describe('when releases are successfully returned', () => {
+ it('resolves the Promise', () => {
+ mock.onGet(expectedUrl).replyOnce(httpStatus.OK);
+
+ return Api.releases(dummyProjectPath).then(() => {
+ expect(mock.history.get).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('when an error occurs while fetching releases', () => {
+ it('rejects the Promise', () => {
+ mock.onGet(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+
+ return Api.releases(dummyProjectPath).catch(() => {
+ expect(mock.history.get).toHaveLength(1);
+ });
});
});
});
- describe('when an error occurs while creating the Release', () => {
- it('rejects the Promise', () => {
- mock.onPost(expectedUrl, expectedLink).replyOnce(500);
+ describe('release', () => {
+ const expectedUrl = `${baseReleaseUrl}/${encodeURIComponent(dummyTagName)}`;
- return Api.createReleaseLink(dummyProjectPath, dummyReleaseTag, expectedLink).catch(() => {
- expect(mock.history.post).toHaveLength(1);
+ describe('when the release is successfully returned', () => {
+ it('resolves the Promise', () => {
+ mock.onGet(expectedUrl).replyOnce(httpStatus.OK);
+
+ return Api.release(dummyProjectPath, dummyTagName).then(() => {
+ expect(mock.history.get).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('when an error occurs while fetching the release', () => {
+ it('rejects the Promise', () => {
+ mock.onGet(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+
+ return Api.release(dummyProjectPath, dummyTagName).catch(() => {
+ expect(mock.history.get).toHaveLength(1);
+ });
});
});
});
- });
- describe('deleteReleaseLink', () => {
- const dummyProjectPath = 'gitlab-org/gitlab';
- const dummyReleaseTag = 'v1.3';
- const dummyLinkId = '4';
- const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${encodeURIComponent(
- dummyProjectPath,
- )}/releases/${dummyReleaseTag}/assets/links/${dummyLinkId}`;
+ describe('createRelease', () => {
+ const expectedUrl = baseReleaseUrl;
- describe('when the Release is successfully deleted', () => {
- it('resolves the Promise', () => {
- mock.onDelete(expectedUrl).replyOnce(200);
+ const release = {
+ name: 'Version 1.0',
+ };
+
+ describe('when the release is successfully created', () => {
+ it('resolves the Promise', () => {
+ mock.onPost(expectedUrl, release).replyOnce(httpStatus.CREATED);
+
+ return Api.createRelease(dummyProjectPath, release).then(() => {
+ expect(mock.history.post).toHaveLength(1);
+ });
+ });
+ });
- return Api.deleteReleaseLink(dummyProjectPath, dummyReleaseTag, dummyLinkId).then(() => {
- expect(mock.history.delete).toHaveLength(1);
+ describe('when an error occurs while creating the release', () => {
+ it('rejects the Promise', () => {
+ mock.onPost(expectedUrl, release).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+
+ return Api.createRelease(dummyProjectPath, release).catch(() => {
+ expect(mock.history.post).toHaveLength(1);
+ });
});
});
});
- describe('when an error occurs while deleting the Release', () => {
- it('rejects the Promise', () => {
- mock.onDelete(expectedUrl).replyOnce(500);
+ describe('updateRelease', () => {
+ const expectedUrl = `${baseReleaseUrl}/${encodeURIComponent(dummyTagName)}`;
+
+ const release = {
+ name: 'Version 1.0',
+ };
+
+ describe('when the release is successfully updated', () => {
+ it('resolves the Promise', () => {
+ mock.onPut(expectedUrl, release).replyOnce(httpStatus.OK);
+
+ return Api.updateRelease(dummyProjectPath, dummyTagName, release).then(() => {
+ expect(mock.history.put).toHaveLength(1);
+ });
+ });
+ });
- return Api.deleteReleaseLink(dummyProjectPath, dummyReleaseTag, dummyLinkId).catch(() => {
- expect(mock.history.delete).toHaveLength(1);
+ describe('when an error occurs while updating the release', () => {
+ it('rejects the Promise', () => {
+ mock.onPut(expectedUrl, release).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+
+ return Api.updateRelease(dummyProjectPath, dummyTagName, release).catch(() => {
+ expect(mock.history.put).toHaveLength(1);
+ });
+ });
+ });
+ });
+
+ describe('createReleaseLink', () => {
+ const expectedUrl = `${baseReleaseUrl}/${dummyTagName}/assets/links`;
+ const expectedLink = {
+ url: 'https://example.com',
+ name: 'An example link',
+ };
+
+ describe('when the Release is successfully created', () => {
+ it('resolves the Promise', () => {
+ mock.onPost(expectedUrl, expectedLink).replyOnce(httpStatus.CREATED);
+
+ return Api.createReleaseLink(dummyProjectPath, dummyTagName, expectedLink).then(() => {
+ expect(mock.history.post).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('when an error occurs while creating the Release', () => {
+ it('rejects the Promise', () => {
+ mock.onPost(expectedUrl, expectedLink).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+
+ return Api.createReleaseLink(dummyProjectPath, dummyTagName, expectedLink).catch(() => {
+ expect(mock.history.post).toHaveLength(1);
+ });
+ });
+ });
+ });
+
+ describe('deleteReleaseLink', () => {
+ const dummyLinkId = '4';
+ const expectedUrl = `${baseReleaseUrl}/${dummyTagName}/assets/links/${dummyLinkId}`;
+
+ describe('when the Release is successfully deleted', () => {
+ it('resolves the Promise', () => {
+ mock.onDelete(expectedUrl).replyOnce(httpStatus.OK);
+
+ return Api.deleteReleaseLink(dummyProjectPath, dummyTagName, dummyLinkId).then(() => {
+ expect(mock.history.delete).toHaveLength(1);
+ });
+ });
+ });
+
+ describe('when an error occurs while deleting the Release', () => {
+ it('rejects the Promise', () => {
+ mock.onDelete(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+
+ return Api.deleteReleaseLink(dummyProjectPath, dummyTagName, dummyLinkId).catch(() => {
+ expect(mock.history.delete).toHaveLength(1);
+ });
});
});
});
@@ -736,7 +944,7 @@ describe('Api', () => {
describe('when the raw file is successfully fetched', () => {
it('resolves the Promise', () => {
- mock.onGet(expectedUrl).replyOnce(200);
+ mock.onGet(expectedUrl).replyOnce(httpStatus.OK);
return Api.getRawFile(dummyProjectPath, dummyFilePath).then(() => {
expect(mock.history.get).toHaveLength(1);
@@ -746,7 +954,7 @@ describe('Api', () => {
describe('when an error occurs while getting a raw file', () => {
it('rejects the Promise', () => {
- mock.onPost(expectedUrl).replyOnce(500);
+ mock.onPost(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
return Api.getRawFile(dummyProjectPath, dummyFilePath).catch(() => {
expect(mock.history.get).toHaveLength(1);
@@ -768,7 +976,7 @@ describe('Api', () => {
describe('when the merge request is successfully created', () => {
it('resolves the Promise', () => {
- mock.onPost(expectedUrl, options).replyOnce(201);
+ mock.onPost(expectedUrl, options).replyOnce(httpStatus.CREATED);
return Api.createProjectMergeRequest(dummyProjectPath, options).then(() => {
expect(mock.history.post).toHaveLength(1);
@@ -778,7 +986,7 @@ describe('Api', () => {
describe('when an error occurs while getting a raw file', () => {
it('rejects the Promise', () => {
- mock.onPost(expectedUrl).replyOnce(500);
+ mock.onPost(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
return Api.createProjectMergeRequest(dummyProjectPath).catch(() => {
expect(mock.history.post).toHaveLength(1);
@@ -793,7 +1001,7 @@ describe('Api', () => {
const issue = 1;
const expectedArray = [1, 2, 3];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/issues/${issue}`;
- mock.onPut(expectedUrl).reply(200, { assigneeIds: expectedArray });
+ mock.onPut(expectedUrl).reply(httpStatus.OK, { assigneeIds: expectedArray });
Api.updateIssue(projectId, issue, { assigneeIds: expectedArray })
.then(({ data }) => {
@@ -810,7 +1018,7 @@ describe('Api', () => {
const mergeRequest = 1;
const expectedArray = [1, 2, 3];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/merge_requests/${mergeRequest}`;
- mock.onPut(expectedUrl).reply(200, { assigneeIds: expectedArray });
+ mock.onPut(expectedUrl).reply(httpStatus.OK, { assigneeIds: expectedArray });
Api.updateMergeRequest(projectId, mergeRequest, { assigneeIds: expectedArray })
.then(({ data }) => {
@@ -827,7 +1035,7 @@ describe('Api', () => {
const options = { unused: 'option' };
const projectId = 8;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/repository/tags`;
- mock.onGet(expectedUrl).reply(200, [
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [
{
name: 'test',
},
@@ -842,4 +1050,83 @@ describe('Api', () => {
.catch(done.fail);
});
});
+
+ describe('freezePeriods', () => {
+ it('fetches freezePeriods', () => {
+ const projectId = 8;
+ const freezePeriod = {
+ id: 3,
+ freeze_start: '5 4 * * *',
+ freeze_end: '5 9 * 8 *',
+ cron_timezone: 'America/New_York',
+ created_at: '2020-07-10T05:10:35.122Z',
+ updated_at: '2020-07-10T05:10:35.122Z',
+ };
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/freeze_periods`;
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [freezePeriod]);
+
+ return Api.freezePeriods(projectId).then(({ data }) => {
+ expect(data[0]).toStrictEqual(freezePeriod);
+ });
+ });
+ });
+
+ describe('createFreezePeriod', () => {
+ const projectId = 8;
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/freeze_periods`;
+ const options = {
+ freeze_start: '* * * * *',
+ freeze_end: '* * * * *',
+ cron_timezone: 'America/Juneau',
+ };
+
+ const expectedResult = {
+ id: 10,
+ freeze_start: '* * * * *',
+ freeze_end: '* * * * *',
+ cron_timezone: 'America/Juneau',
+ created_at: '2020-07-11T07:04:50.153Z',
+ updated_at: '2020-07-11T07:04:50.153Z',
+ };
+
+ describe('when the freeze period is successfully created', () => {
+ it('resolves the Promise', () => {
+ mock.onPost(expectedUrl, options).replyOnce(httpStatus.CREATED, expectedResult);
+
+ return Api.createFreezePeriod(projectId, options).then(({ data }) => {
+ expect(data).toStrictEqual(expectedResult);
+ });
+ });
+ });
+ });
+
+ describe('createPipeline', () => {
+ it('creates new pipeline', () => {
+ const redirectUrl = 'ci-project/-/pipelines/95';
+ const projectId = 8;
+ const postData = {
+ ref: 'tag-1',
+ variables: [
+ { key: 'test_file', value: 'test_file_val', variable_type: 'file' },
+ { key: 'test_var', value: 'test_var_val', variable_type: 'env_var' },
+ ],
+ };
+ const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/pipeline`;
+
+ jest.spyOn(axios, 'post');
+
+ mock.onPost(expectedUrl).replyOnce(httpStatus.OK, {
+ web_url: redirectUrl,
+ });
+
+ return Api.createPipeline(projectId, postData).then(({ data }) => {
+ expect(data.web_url).toBe(redirectUrl);
+ expect(axios.post).toHaveBeenCalledWith(expectedUrl, postData, {
+ headers: {
+ 'Content-Type': 'application/json',
+ },
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/awards_handler_spec.js b/spec/frontend/awards_handler_spec.js
index 6cfbc6024af..1a1738ecf4a 100644
--- a/spec/frontend/awards_handler_spec.js
+++ b/spec/frontend/awards_handler_spec.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import Cookies from 'js-cookie';
import MockAdapter from 'axios-mock-adapter';
+import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import axios from '~/lib/utils/axios_utils';
import loadAwardsHandler from '~/awards_handler';
import { setTestTimeout } from './helpers/timeout';
import { EMOJI_VERSION } from '~/emoji';
-import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
window.gl = window.gl || {};
window.gon = window.gon || {};
@@ -162,7 +162,7 @@ describe('AwardsHandler', () => {
describe('::getAwardUrl', () => {
it('returns the url for request', () => {
- expect(awardsHandler.getAwardUrl()).toBe('http://test.host/snippets/1/toggle_award_emoji');
+ expect(awardsHandler.getAwardUrl()).toBe('http://test.host/-/snippets/1/toggle_award_emoji');
});
});
diff --git a/spec/frontend/badges/components/badge_form_spec.js b/spec/frontend/badges/components/badge_form_spec.js
index d61bd29ca9d..1edc9adbfb2 100644
--- a/spec/frontend/badges/components/badge_form_spec.js
+++ b/spec/frontend/badges/components/badge_form_spec.js
@@ -1,11 +1,11 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { DUMMY_IMAGE_URL, TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import store from '~/badges/store';
import createEmptyBadge from '~/badges/empty_badge';
import BadgeForm from '~/badges/components/badge_form.vue';
-import { DUMMY_IMAGE_URL, TEST_HOST } from 'helpers/test_constants';
// avoid preview background process
BadgeForm.methods.debouncedPreview = () => {};
@@ -182,11 +182,11 @@ describe('BadgeForm component', () => {
const buttons = vm.$el.querySelectorAll('.row-content-block button');
expect(buttons.length).toBe(2);
- const buttonSaveElement = buttons[0];
+ const buttonSaveElement = buttons[1];
expect(buttonSaveElement).toBeVisible();
expect(buttonSaveElement).toHaveText('Save changes');
- const buttonCancelElement = buttons[1];
+ const buttonCancelElement = buttons[0];
expect(buttonCancelElement).toBeVisible();
expect(buttonCancelElement).toHaveText('Cancel');
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index eea7f25dbc1..99980c98f8b 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -1,4 +1,5 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { getByRole } from '@testing-library/dom';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import { createStore } from '~/batch_comments/stores';
import NoteableNote from '~/notes/components/noteable_note.vue';
@@ -8,21 +9,34 @@ import { createDraft } from '../mock_data';
const localVue = createLocalVue();
describe('Batch comments draft note component', () => {
+ let store;
let wrapper;
let draft;
+ const LINE_RANGE = {};
+ const draftWithLineRange = {
+ position: {
+ line_range: LINE_RANGE,
+ },
+ };
- beforeEach(() => {
- const store = createStore();
-
- draft = createDraft();
+ const getList = () => getByRole(wrapper.element, 'list');
+ const createComponent = (propsData = { draft }, features = {}) => {
wrapper = shallowMount(localVue.extend(DraftNote), {
store,
- propsData: { draft },
+ propsData,
localVue,
+ provide: {
+ glFeatures: { multilineComments: true, ...features },
+ },
});
jest.spyOn(wrapper.vm.$store, 'dispatch').mockImplementation();
+ };
+
+ beforeEach(() => {
+ store = createStore();
+ draft = createDraft();
});
afterEach(() => {
@@ -30,6 +44,7 @@ describe('Batch comments draft note component', () => {
});
it('renders template', () => {
+ createComponent();
expect(wrapper.find('.draft-pending-label').exists()).toBe(true);
const note = wrapper.find(NoteableNote);
@@ -40,6 +55,7 @@ describe('Batch comments draft note component', () => {
describe('add comment now', () => {
it('dispatches publishSingleDraft when clicking', () => {
+ createComponent();
const publishNowButton = wrapper.find({ ref: 'publishNowButton' });
publishNowButton.vm.$emit('click');
@@ -50,6 +66,7 @@ describe('Batch comments draft note component', () => {
});
it('sets as loading when draft is publishing', done => {
+ createComponent();
wrapper.vm.$store.state.batchComments.currentlyPublishingDrafts.push(1);
wrapper.vm.$nextTick(() => {
@@ -64,6 +81,7 @@ describe('Batch comments draft note component', () => {
describe('update', () => {
it('dispatches updateDraft', done => {
+ createComponent();
const note = wrapper.find(NoteableNote);
note.vm.$emit('handleEdit');
@@ -91,6 +109,7 @@ describe('Batch comments draft note component', () => {
describe('deleteDraft', () => {
it('dispatches deleteDraft', () => {
+ createComponent();
jest.spyOn(window, 'confirm').mockImplementation(() => true);
const note = wrapper.find(NoteableNote);
@@ -103,6 +122,7 @@ describe('Batch comments draft note component', () => {
describe('quick actions', () => {
it('renders referenced commands', done => {
+ createComponent();
wrapper.setProps({
draft: {
...draft,
@@ -122,4 +142,26 @@ describe('Batch comments draft note component', () => {
});
});
});
+
+ describe('multiline comments', () => {
+ describe.each`
+ desc | props | features | event | expectedCalls
+ ${'with `draft.position`'} | ${draftWithLineRange} | ${{}} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]}
+ ${'with `draft.position`'} | ${draftWithLineRange} | ${{}} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]}
+ ${'with `draft.position`'} | ${draftWithLineRange} | ${{ multilineComments: false }} | ${'mouseenter'} | ${[]}
+ ${'with `draft.position`'} | ${draftWithLineRange} | ${{ multilineComments: false }} | ${'mouseleave'} | ${[]}
+ ${'without `draft.position`'} | ${{}} | ${{}} | ${'mouseenter'} | ${[]}
+ ${'without `draft.position`'} | ${{}} | ${{}} | ${'mouseleave'} | ${[]}
+ `('$desc and features $features', ({ props, event, features, expectedCalls }) => {
+ beforeEach(() => {
+ createComponent({ draft: { ...draft, ...props } }, features);
+ jest.spyOn(store, 'dispatch');
+ });
+
+ it(`calls store ${expectedCalls.length} times on ${event}`, () => {
+ getList().dispatchEvent(new MouseEvent(event, { bubbles: true }));
+ expect(store.dispatch.mock.calls).toEqual(expectedCalls);
+ });
+ });
+ });
});
diff --git a/spec/frontend/batch_comments/components/drafts_count_spec.js b/spec/frontend/batch_comments/components/drafts_count_spec.js
index 9d9fffce7e7..83d2f9eb639 100644
--- a/spec/frontend/batch_comments/components/drafts_count_spec.js
+++ b/spec/frontend/batch_comments/components/drafts_count_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import DraftsCount from '~/batch_comments/components/drafts_count.vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import DraftsCount from '~/batch_comments/components/drafts_count.vue';
import { createStore } from '~/batch_comments/stores';
describe('Batch comments drafts count component', () => {
@@ -24,7 +24,7 @@ describe('Batch comments drafts count component', () => {
});
it('renders count', () => {
- expect(vm.$el.querySelector('.drafts-count-number').textContent).toBe('1');
+ expect(vm.$el.textContent).toContain('1');
});
it('renders screen reader text', done => {
diff --git a/spec/frontend/batch_comments/components/preview_item_spec.js b/spec/frontend/batch_comments/components/preview_item_spec.js
index 7d951fd7799..2b63ece28ba 100644
--- a/spec/frontend/batch_comments/components/preview_item_spec.js
+++ b/spec/frontend/batch_comments/components/preview_item_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import PreviewItem from '~/batch_comments/components/preview_item.vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import PreviewItem from '~/batch_comments/components/preview_item.vue';
import { createStore } from '~/batch_comments/stores';
import diffsModule from '~/diffs/store/modules';
import notesModule from '~/notes/stores/modules';
diff --git a/spec/frontend/batch_comments/components/publish_button_spec.js b/spec/frontend/batch_comments/components/publish_button_spec.js
index 97f3a1c8939..4362f62c7f8 100644
--- a/spec/frontend/batch_comments/components/publish_button_spec.js
+++ b/spec/frontend/batch_comments/components/publish_button_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import PublishButton from '~/batch_comments/components/publish_button.vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import PublishButton from '~/batch_comments/components/publish_button.vue';
import { createStore } from '~/batch_comments/stores';
describe('Batch comments publish button component', () => {
diff --git a/spec/frontend/batch_comments/components/publish_dropdown_spec.js b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
index b50ae340691..fb3c532174d 100644
--- a/spec/frontend/batch_comments/components/publish_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
import { createStore } from '~/mr_notes/stores';
import '~/behaviors/markdown/render_gfm';
import { createDraft } from '../mock_data';
diff --git a/spec/frontend/batch_comments/mock_data.js b/spec/frontend/batch_comments/mock_data.js
index c50fea94fe3..5601e489066 100644
--- a/spec/frontend/batch_comments/mock_data.js
+++ b/spec/frontend/batch_comments/mock_data.js
@@ -1,5 +1,6 @@
import { TEST_HOST } from 'spec/test_constants';
+// eslint-disable-next-line import/prefer-default-export
export const createDraft = () => ({
author: {
id: 1,
@@ -23,5 +24,3 @@ export const createDraft = () => ({
isDraft: true,
position: null,
});
-
-export default () => {};
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 4bac6d4e3dc..a6942115649 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,8 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'jest/helpers/test_constants';
import * as actions from '~/batch_comments/stores/modules/batch_comments/actions';
import axios from '~/lib/utils/axios_utils';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('Batch comments store actions', () => {
let res = {};
diff --git a/spec/frontend/behaviors/copy_as_gfm_spec.js b/spec/frontend/behaviors/copy_as_gfm_spec.js
index 33af9bc135e..46d4451c941 100644
--- a/spec/frontend/behaviors/copy_as_gfm_spec.js
+++ b/spec/frontend/behaviors/copy_as_gfm_spec.js
@@ -123,4 +123,14 @@ describe('CopyAsGFM', () => {
});
});
});
+
+ describe('CopyAsGFM.quoted', () => {
+ const sampleGFM = '* List 1\n* List 2\n\n`Some code`';
+
+ it('adds quote char `> ` to each line', done => {
+ const expectedQuotedGFM = '> * List 1\n> * List 2\n> \n> `Some code`';
+ expect(CopyAsGFM.quoted(sampleGFM)).toEqual(expectedQuotedGFM);
+ done();
+ });
+ });
});
diff --git a/spec/frontend/behaviors/gl_emoji_spec.js b/spec/frontend/behaviors/gl_emoji_spec.js
index 7ea0bafc328..ef6b1673b7c 100644
--- a/spec/frontend/behaviors/gl_emoji_spec.js
+++ b/spec/frontend/behaviors/gl_emoji_spec.js
@@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'jest/helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { initEmojiMap, EMOJI_VERSION } from '~/emoji';
import installGlEmojiElement from '~/behaviors/gl_emoji';
import * as EmojiUnicodeSupport from '~/emoji/support';
-import waitForPromises from 'jest/helpers/wait_for_promises';
jest.mock('~/emoji/support');
diff --git a/spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap
index 0409b118222..72761c18b3d 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_edit_content_spec.js.snap
@@ -4,11 +4,15 @@ exports[`Blob Header Editing rendering matches the snapshot 1`] = `
<div
class="file-content code"
>
- <pre
+ <div
data-editor-loading=""
id="editor"
>
- Lorem ipsum dolor sit amet, consectetur adipiscing elit.
- </pre>
+ <pre
+ class="editor-loading-content"
+ >
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit.
+ </pre>
+ </div>
</div>
`;
diff --git a/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap
index 1e639f91797..a5690844053 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_edit_header_spec.js.snap
@@ -4,13 +4,18 @@ exports[`Blob Header Editing rendering matches the snapshot 1`] = `
<div
class="js-file-title file-title-flex-parent"
>
- <gl-form-input-stub
- class="form-control js-snippet-file-name"
- id="snippet_file_name"
- name="snippet_file_name"
- placeholder="Give your file a name to add code highlighting, e.g. example.rb for Ruby"
- type="text"
- value="foo.md"
- />
+ <div
+ class="gl-display-flex gl-align-items-center gl-w-full"
+ >
+ <gl-form-input-stub
+ class="form-control js-snippet-file-name"
+ name="snippet_file_name"
+ placeholder="Give your file a name to add code highlighting, e.g. example.rb for Ruby"
+ type="text"
+ value="foo.md"
+ />
+
+ <!---->
+ </div>
</div>
`;
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
index 7d868625956..b54efb93bc9 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap
@@ -9,7 +9,7 @@ exports[`Blob Header Default Actions rendering matches the snapshot 1`] = `
/>
<div
- class="file-actions d-none d-sm-flex"
+ class="gl-display-none gl-display-sm-flex"
>
<viewer-switcher-stub
value="simple"
diff --git a/spec/frontend/blob/components/blob_content_error_spec.js b/spec/frontend/blob/components/blob_content_error_spec.js
index 508b1ed7e68..0c6d269ad05 100644
--- a/spec/frontend/blob/components/blob_content_error_spec.js
+++ b/spec/frontend/blob/components/blob_content_error_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import BlobContentError from '~/blob/components/blob_content_error.vue';
import { GlSprintf } from '@gitlab/ui';
+import BlobContentError from '~/blob/components/blob_content_error.vue';
import { BLOB_RENDER_ERRORS } from '~/blob/components/constants';
diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js
index 244ed41869d..9232a709194 100644
--- a/spec/frontend/blob/components/blob_content_spec.js
+++ b/spec/frontend/blob/components/blob_content_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
import BlobContent from '~/blob/components/blob_content.vue';
import BlobContentError from '~/blob/components/blob_content_error.vue';
import {
@@ -13,7 +14,6 @@ import {
RichBlobContentMock,
SimpleBlobContentMock,
} from './mock_data';
-import { GlLoadingIcon } from '@gitlab/ui';
import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
describe('Blob Content component', () => {
diff --git a/spec/frontend/blob/components/blob_edit_content_spec.js b/spec/frontend/blob/components/blob_edit_content_spec.js
index 971ef72521d..3cc210e972c 100644
--- a/spec/frontend/blob/components/blob_edit_content_spec.js
+++ b/spec/frontend/blob/components/blob_edit_content_spec.js
@@ -1,28 +1,31 @@
import { shallowMount } from '@vue/test-utils';
-import BlobEditContent from '~/blob/components/blob_edit_content.vue';
-import { initEditorLite } from '~/blob/utils';
import { nextTick } from 'vue';
+import BlobEditContent from '~/blob/components/blob_edit_content.vue';
+import * as utils from '~/blob/utils';
+import Editor from '~/editor/editor_lite';
-jest.mock('~/blob/utils', () => ({
- initEditorLite: jest.fn(),
-}));
+jest.mock('~/editor/editor_lite');
describe('Blob Header Editing', () => {
let wrapper;
const value = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
const fileName = 'lorem.txt';
+ const fileGlobalId = 'snippet_777';
function createComponent(props = {}) {
wrapper = shallowMount(BlobEditContent, {
propsData: {
value,
fileName,
+ fileGlobalId,
...props,
},
});
}
beforeEach(() => {
+ jest.spyOn(utils, 'initEditorLite');
+
createComponent();
});
@@ -30,6 +33,15 @@ describe('Blob Header Editing', () => {
wrapper.destroy();
});
+ const triggerChangeContent = val => {
+ jest.spyOn(Editor.prototype, 'getValue').mockReturnValue(val);
+ const [cb] = Editor.prototype.onChangeContent.mock.calls[0];
+
+ cb();
+
+ jest.runOnlyPendingTimers();
+ };
+
describe('rendering', () => {
it('matches the snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
@@ -51,18 +63,15 @@ describe('Blob Header Editing', () => {
it('initialises Editor Lite', () => {
const el = wrapper.find({ ref: 'editor' }).element;
- expect(initEditorLite).toHaveBeenCalledWith({
+ expect(utils.initEditorLite).toHaveBeenCalledWith({
el,
blobPath: fileName,
+ blobGlobalId: fileGlobalId,
blobContent: value,
});
});
it('reacts to the changes in fileName', () => {
- wrapper.vm.editor = {
- updateModelLanguage: jest.fn(),
- };
-
const newFileName = 'ipsum.txt';
wrapper.setProps({
@@ -70,21 +79,20 @@ describe('Blob Header Editing', () => {
});
return nextTick().then(() => {
- expect(wrapper.vm.editor.updateModelLanguage).toHaveBeenCalledWith(newFileName);
+ expect(Editor.prototype.updateModelLanguage).toHaveBeenCalledWith(newFileName);
});
});
+ it('registers callback with editor onChangeContent', () => {
+ expect(Editor.prototype.onChangeContent).toHaveBeenCalledWith(expect.any(Function));
+ });
+
it('emits input event when the blob content is changed', () => {
- const editorEl = wrapper.find({ ref: 'editor' });
- wrapper.vm.editor = {
- getValue: jest.fn().mockReturnValue(value),
- };
+ expect(wrapper.emitted().input).toBeUndefined();
- editorEl.trigger('keyup');
+ triggerChangeContent(value);
- return nextTick().then(() => {
- expect(wrapper.emitted().input[0]).toEqual([value]);
- });
+ expect(wrapper.emitted().input).toEqual([[value]]);
});
});
});
diff --git a/spec/frontend/blob/components/blob_edit_header_spec.js b/spec/frontend/blob/components/blob_edit_header_spec.js
index db7d7d7d48d..c71595a79cf 100644
--- a/spec/frontend/blob/components/blob_edit_header_spec.js
+++ b/spec/frontend/blob/components/blob_edit_header_spec.js
@@ -1,18 +1,21 @@
import { shallowMount } from '@vue/test-utils';
+import { GlFormInput, GlButton } from '@gitlab/ui';
import BlobEditHeader from '~/blob/components/blob_edit_header.vue';
-import { GlFormInput } from '@gitlab/ui';
describe('Blob Header Editing', () => {
let wrapper;
const value = 'foo.md';
- function createComponent() {
+ const createComponent = (props = {}) => {
wrapper = shallowMount(BlobEditHeader, {
propsData: {
value,
+ ...props,
},
});
- }
+ };
+ const findDeleteButton = () =>
+ wrapper.findAll(GlButton).wrappers.find(x => x.text() === 'Delete file');
beforeEach(() => {
createComponent();
@@ -30,6 +33,10 @@ describe('Blob Header Editing', () => {
it('contains a form input field', () => {
expect(wrapper.contains(GlFormInput)).toBe(true);
});
+
+ it('does not show delete button', () => {
+ expect(findDeleteButton()).toBeUndefined();
+ });
});
describe('functionality', () => {
@@ -47,4 +54,35 @@ describe('Blob Header Editing', () => {
});
});
});
+
+ describe.each`
+ props | expectedDisabled
+ ${{ showDelete: true }} | ${false}
+ ${{ showDelete: true, canDelete: false }} | ${true}
+ `('with $props', ({ props, expectedDisabled }) => {
+ beforeEach(() => {
+ createComponent(props);
+ });
+
+ it(`shows delete button (disabled=${expectedDisabled})`, () => {
+ const deleteButton = findDeleteButton();
+
+ expect(deleteButton.exists()).toBe(true);
+ expect(deleteButton.props('disabled')).toBe(expectedDisabled);
+ });
+ });
+
+ describe('with delete button', () => {
+ beforeEach(() => {
+ createComponent({ showDelete: true, canDelete: true });
+ });
+
+ it('emits delete when clicked', () => {
+ expect(wrapper.emitted().delete).toBeUndefined();
+
+ findDeleteButton().vm.$emit('click');
+
+ expect(wrapper.emitted().delete).toEqual([[]]);
+ });
+ });
});
diff --git a/spec/frontend/blob/components/blob_embeddable_spec.js b/spec/frontend/blob/components/blob_embeddable_spec.js
index b2fe71f1401..1f6790013ca 100644
--- a/spec/frontend/blob/components/blob_embeddable_spec.js
+++ b/spec/frontend/blob/components/blob_embeddable_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import { GlFormInputGroup } from '@gitlab/ui';
+import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
describe('Blob Embeddable', () => {
let wrapper;
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index 529e7cc85f5..590e36b16af 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { GlButtonGroup, GlButton } from '@gitlab/ui';
import BlobHeaderActions from '~/blob/components/blob_header_default_actions.vue';
import {
BTN_COPY_CONTENTS_TITLE,
@@ -6,7 +7,6 @@ import {
BTN_RAW_TITLE,
RICH_BLOB_VIEWER,
} from '~/blob/components/constants';
-import { GlButtonGroup, GlDeprecatedButton } from '@gitlab/ui';
import { Blob } from './mock_data';
describe('Blob Header Default Actions', () => {
@@ -26,7 +26,7 @@ describe('Blob Header Default Actions', () => {
beforeEach(() => {
createComponent();
btnGroup = wrapper.find(GlButtonGroup);
- buttons = wrapper.findAll(GlDeprecatedButton);
+ buttons = wrapper.findAll(GlButton);
});
afterEach(() => {
@@ -61,7 +61,7 @@ describe('Blob Header Default Actions', () => {
createComponent({
activeViewer: RICH_BLOB_VIEWER,
});
- buttons = wrapper.findAll(GlDeprecatedButton);
+ buttons = wrapper.findAll(GlButton);
expect(buttons.at(0).attributes('disabled')).toBeTruthy();
});
diff --git a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
index f1a7ac8b21a..cf1101bc22c 100644
--- a/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
+++ b/spec/frontend/blob/components/blob_header_viewer_switcher_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { GlButtonGroup, GlButton } from '@gitlab/ui';
import BlobHeaderViewerSwitcher from '~/blob/components/blob_header_viewer_switcher.vue';
import {
RICH_BLOB_VIEWER,
@@ -6,7 +7,6 @@ import {
SIMPLE_BLOB_VIEWER,
SIMPLE_BLOB_VIEWER_TITLE,
} from '~/blob/components/constants';
-import { GlButtonGroup, GlButton } from '@gitlab/ui';
describe('Blob Header Viewer Switcher', () => {
let wrapper;
diff --git a/spec/frontend/blob/components/mock_data.js b/spec/frontend/blob/components/mock_data.js
index 58aa1dc6dc9..8cfcec2693c 100644
--- a/spec/frontend/blob/components/mock_data.js
+++ b/spec/frontend/blob/components/mock_data.js
@@ -47,10 +47,12 @@ export const BinaryBlob = {
};
export const RichBlobContentMock = {
+ path: 'foo.md',
richData: '<h1>Rich</h1>',
};
export const SimpleBlobContentMock = {
+ path: 'foo.js',
plainData: 'Plain',
};
diff --git a/spec/frontend/blob/notebook/notebook_viever_spec.js b/spec/frontend/blob/notebook/notebook_viever_spec.js
index 535d2bd544a..f6a926a5ecb 100644
--- a/spec/frontend/blob/notebook/notebook_viever_spec.js
+++ b/spec/frontend/blob/notebook/notebook_viever_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import component from '~/blob/notebook/notebook_viewer.vue';
import NotebookLab from '~/notebook/index.vue';
-import waitForPromises from 'helpers/wait_for_promises';
describe('iPython notebook renderer', () => {
let wrapper;
diff --git a/spec/frontend/blob/pipeline_tour_success_modal_spec.js b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
index 6d4e5e46cb8..9998cd7f91c 100644
--- a/spec/frontend/blob/pipeline_tour_success_modal_spec.js
+++ b/spec/frontend/blob/pipeline_tour_success_modal_spec.js
@@ -1,8 +1,8 @@
-import pipelineTourSuccess from '~/blob/pipeline_tour_success_modal.vue';
import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
-import { GlSprintf, GlModal } from '@gitlab/ui';
+import { GlSprintf, GlModal, GlLink } from '@gitlab/ui';
import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
+import pipelineTourSuccess from '~/blob/pipeline_tour_success_modal.vue';
import modalProps from './pipeline_tour_success_mock_data';
describe('PipelineTourSuccessModal', () => {
@@ -18,6 +18,7 @@ describe('PipelineTourSuccessModal', () => {
propsData: modalProps,
stubs: {
GlModal,
+ GlSprintf,
},
});
@@ -37,6 +38,10 @@ describe('PipelineTourSuccessModal', () => {
expect(sprintf.exists()).toBe(true);
});
+ it('renders the link for codeQualityLink', () => {
+ expect(wrapper.find(GlLink).attributes('href')).toBe(wrapper.vm.$options.codeQualityLink);
+ });
+
it('calls to remove cookie', () => {
wrapper.vm.disableModalFromRenderingAgain();
diff --git a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
index 3c03e6f04ab..4714d34dbec 100644
--- a/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
+++ b/spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
-import Popover from '~/blob/suggest_gitlab_ci_yml/components/popover.vue';
import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
+import { GlButton } from '@gitlab/ui';
+import Popover from '~/blob/suggest_gitlab_ci_yml/components/popover.vue';
import * as utils from '~/lib/utils/common_utils';
-import { GlDeprecatedButton } from '@gitlab/ui';
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
@@ -96,7 +96,7 @@ describe('Suggest gitlab-ci.yml Popover', () => {
const expectedAction = 'click_button';
const expectedProperty = 'owner';
const expectedValue = '10';
- const dismissButton = wrapper.find(GlDeprecatedButton);
+ const dismissButton = wrapper.find(GlButton);
trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
triggerEvent(dismissButton.element);
diff --git a/spec/frontend/blob/utils_spec.js b/spec/frontend/blob/utils_spec.js
index 119ed2dfe7a..ab9e325e963 100644
--- a/spec/frontend/blob/utils_spec.js
+++ b/spec/frontend/blob/utils_spec.js
@@ -1,53 +1,44 @@
import Editor from '~/editor/editor_lite';
import * as utils from '~/blob/utils';
-const mockCreateMonacoInstance = jest.fn();
-jest.mock('~/editor/editor_lite', () => {
- return jest.fn().mockImplementation(() => {
- return { createInstance: mockCreateMonacoInstance };
- });
-});
+jest.mock('~/editor/editor_lite');
describe('Blob utilities', () => {
- beforeEach(() => {
- Editor.mockClear();
- });
-
describe('initEditorLite', () => {
let editorEl;
const blobPath = 'foo.txt';
const blobContent = 'Foo bar';
+ const blobGlobalId = 'snippet_777';
beforeEach(() => {
- setFixtures('<div id="editor"></div>');
- editorEl = document.getElementById('editor');
+ editorEl = document.createElement('div');
});
describe('Monaco editor', () => {
it('initializes the Editor Lite', () => {
utils.initEditorLite({ el: editorEl });
- expect(Editor).toHaveBeenCalled();
+ expect(Editor).toHaveBeenCalledWith({
+ scrollbar: {
+ alwaysConsumeMouseWheel: false,
+ },
+ });
});
- it('creates the instance with the passed parameters', () => {
- utils.initEditorLite({ el: editorEl });
- expect(mockCreateMonacoInstance.mock.calls[0]).toEqual([
- {
+ it.each([[{}], [{ blobPath, blobContent, blobGlobalId }]])(
+ 'creates the instance with the passed parameters %s',
+ extraParams => {
+ const params = {
el: editorEl,
- blobPath: undefined,
- blobContent: undefined,
- },
- ]);
+ ...extraParams,
+ };
- utils.initEditorLite({ el: editorEl, blobPath, blobContent });
- expect(mockCreateMonacoInstance.mock.calls[1]).toEqual([
- {
- el: editorEl,
- blobPath,
- blobContent,
- },
- ]);
- });
+ expect(Editor.prototype.createInstance).not.toHaveBeenCalled();
+
+ utils.initEditorLite(params);
+
+ expect(Editor.prototype.createInstance).toHaveBeenCalledWith(params);
+ },
+ );
});
});
});
diff --git a/spec/frontend/blob/viewer/index_spec.js b/spec/frontend/blob/viewer/index_spec.js
index 7239f59c6fa..97ac42a10bf 100644
--- a/spec/frontend/blob/viewer/index_spec.js
+++ b/spec/frontend/blob/viewer/index_spec.js
@@ -24,11 +24,11 @@ describe('Blob viewer', () => {
blob = new BlobViewer();
- mock.onGet('http://test.host/snippets/1.json?viewer=rich').reply(200, {
+ mock.onGet('http://test.host/-/snippets/1.json?viewer=rich').reply(200, {
html: '<div>testing</div>',
});
- mock.onGet('http://test.host/snippets/1.json?viewer=simple').reply(200, {
+ mock.onGet('http://test.host/-/snippets/1.json?viewer=simple').reply(200, {
html: '<div>testing</div>',
});
diff --git a/spec/frontend/blob_edit/blob_bundle_spec.js b/spec/frontend/blob_edit/blob_bundle_spec.js
index f5cd623ebce..98fa96de124 100644
--- a/spec/frontend/blob_edit/blob_bundle_spec.js
+++ b/spec/frontend/blob_edit/blob_bundle_spec.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import blobBundle from '~/blob_edit/blob_bundle';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import blobBundle from '~/blob_edit/blob_bundle';
jest.mock('~/blob_edit/edit_blob');
diff --git a/spec/frontend/boards/board_card_spec.js b/spec/frontend/boards/board_card_spec.js
index 959c71d05ca..d01b895f996 100644
--- a/spec/frontend/boards/board_card_spec.js
+++ b/spec/frontend/boards/board_card_spec.js
@@ -5,8 +5,8 @@
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
import eventHub from '~/boards/eventhub';
import sidebarEventHub from '~/sidebar/event_hub';
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index 6853fe2559d..c06b7aceaad 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -2,14 +2,13 @@ import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
+import { listObj } from 'jest/boards/mock_data';
import Board from '~/boards/components/board_column.vue';
import List from '~/boards/models/list';
import { ListType } from '~/boards/constants';
import axios from '~/lib/utils/axios_utils';
-import { TEST_HOST } from 'helpers/test_constants';
-import { listObj } from 'jest/boards/mock_data';
-
describe('Board Column Component', () => {
let wrapper;
let axiosMock;
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index 94f607698d7..b1d277863e8 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -1,9 +1,9 @@
import { mount } from '@vue/test-utils';
+import { TEST_HOST } from 'jest/helpers/test_constants';
import boardsStore from '~/boards/stores/boards_store';
import boardForm from '~/boards/components/board_form.vue';
import DeprecatedModal from '~/vue_shared/components/deprecated_modal.vue';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('board_form.vue', () => {
let wrapper;
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 95673da1c56..76a3d5e71c8 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -2,14 +2,13 @@ import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
+import { listObj } from 'jest/boards/mock_data';
import BoardListHeader from '~/boards/components/board_list_header.vue';
import List from '~/boards/models/list';
import { ListType } from '~/boards/constants';
import axios from '~/lib/utils/axios_utils';
-import { TEST_HOST } from 'helpers/test_constants';
-import { listObj } from 'jest/boards/mock_data';
-
describe('Board List Header Component', () => {
let wrapper;
let axiosMock;
diff --git a/spec/frontend/boards/components/board_settings_sidebar_spec.js b/spec/frontend/boards/components/board_settings_sidebar_spec.js
new file mode 100644
index 00000000000..f39adc0fc49
--- /dev/null
+++ b/spec/frontend/boards/components/board_settings_sidebar_spec.js
@@ -0,0 +1,159 @@
+import '~/boards/models/list';
+import MockAdapter from 'axios-mock-adapter';
+import axios from 'axios';
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlDrawer, GlLabel } from '@gitlab/ui';
+import BoardSettingsSidebar from '~/boards/components/board_settings_sidebar.vue';
+import boardsStore from '~/boards/stores/boards_store';
+import sidebarEventHub from '~/sidebar/event_hub';
+import { inactiveId } from '~/boards/constants';
+
+const localVue = createLocalVue();
+
+localVue.use(Vuex);
+
+describe('BoardSettingsSidebar', () => {
+ let wrapper;
+ let mock;
+ let storeActions;
+ const labelTitle = 'test';
+ const labelColor = '#FFFF';
+ const listId = 1;
+
+ const createComponent = (state = { activeId: inactiveId }, actions = {}) => {
+ storeActions = actions;
+
+ const store = new Vuex.Store({
+ state,
+ actions: storeActions,
+ });
+
+ wrapper = shallowMount(BoardSettingsSidebar, {
+ store,
+ localVue,
+ });
+ };
+ const findLabel = () => wrapper.find(GlLabel);
+ const findDrawer = () => wrapper.find(GlDrawer);
+
+ beforeEach(() => {
+ boardsStore.create();
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ wrapper.destroy();
+ });
+
+ it('finds a GlDrawer component', () => {
+ createComponent();
+
+ expect(findDrawer().exists()).toBe(true);
+ });
+
+ describe('on close', () => {
+ it('calls closeSidebar', async () => {
+ const spy = jest.fn();
+ createComponent({ activeId: inactiveId }, { setActiveId: spy });
+
+ findDrawer().vm.$emit('close');
+
+ await wrapper.vm.$nextTick();
+
+ expect(storeActions.setActiveId).toHaveBeenCalledWith(
+ expect.anything(),
+ inactiveId,
+ undefined,
+ );
+ });
+
+ it('calls closeSidebar on sidebar.closeAll event', async () => {
+ createComponent({ activeId: inactiveId }, { setActiveId: jest.fn() });
+
+ sidebarEventHub.$emit('sidebar.closeAll');
+
+ await wrapper.vm.$nextTick();
+
+ expect(storeActions.setActiveId).toHaveBeenCalledWith(
+ expect.anything(),
+ inactiveId,
+ undefined,
+ );
+ });
+ });
+
+ describe('when activeId is zero', () => {
+ it('renders GlDrawer with open false', () => {
+ createComponent();
+
+ expect(findDrawer().props('open')).toBe(false);
+ });
+ });
+
+ describe('when activeId is greater than zero', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ boardsStore.addList({
+ id: listId,
+ label: { title: labelTitle, color: labelColor },
+ list_type: 'label',
+ });
+ });
+
+ afterEach(() => {
+ boardsStore.removeList(listId);
+ });
+
+ it('renders GlDrawer with open false', () => {
+ createComponent({ activeId: 1 });
+
+ expect(findDrawer().props('open')).toBe(true);
+ });
+ });
+
+ describe('when activeId is in boardsStore', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ boardsStore.addList({
+ id: listId,
+ label: { title: labelTitle, color: labelColor },
+ list_type: 'label',
+ });
+
+ createComponent({ activeId: listId });
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('renders label title', () => {
+ expect(findLabel().props('title')).toBe(labelTitle);
+ });
+
+ it('renders label background color', () => {
+ expect(findLabel().props('backgroundColor')).toBe(labelColor);
+ });
+ });
+
+ describe('when activeId is not in boardsStore', () => {
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ boardsStore.addList({ id: listId, label: { title: labelTitle, color: labelColor } });
+
+ createComponent({ activeId: inactiveId });
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('does not render GlLabel', () => {
+ expect(findLabel().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index b1ae86c2d3f..f2d4de238d1 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
-import { GlDropdown, GlLoadingIcon } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlLoadingIcon } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
import BoardsSelector from '~/boards/components/boards_selector.vue';
import boardsStore from '~/boards/stores/boards_store';
@@ -103,7 +103,7 @@ describe('BoardsSelector', () => {
});
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
- wrapper.find(GlDropdown).vm.$emit('show');
+ wrapper.find(GlDeprecatedDropdown).vm.$emit('show');
});
afterEach(() => {
diff --git a/spec/frontend/boards/components/sidebar/remove_issue_spec.js b/spec/frontend/boards/components/sidebar/remove_issue_spec.js
new file mode 100644
index 00000000000..a33e4046724
--- /dev/null
+++ b/spec/frontend/boards/components/sidebar/remove_issue_spec.js
@@ -0,0 +1,28 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+
+import RemoveIssue from '~/boards/components/sidebar/remove_issue.vue';
+
+describe('boards sidebar remove issue', () => {
+ let wrapper;
+
+ const findButton = () => wrapper.find(GlButton);
+
+ const createComponent = propsData => {
+ wrapper = shallowMount(RemoveIssue, {
+ propsData: {
+ issue: {},
+ list: {},
+ ...propsData,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders remove button', () => {
+ expect(findButton().exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/boards/issue_card_spec.js b/spec/frontend/boards/issue_card_spec.js
index 15750a161ae..dee8cb7b6e5 100644
--- a/spec/frontend/boards/issue_card_spec.js
+++ b/spec/frontend/boards/issue_card_spec.js
@@ -5,10 +5,10 @@ import '~/boards/models/label';
import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
+import { GlLabel } from '@gitlab/ui';
import IssueCardInner from '~/boards/components/issue_card_inner.vue';
import { listObj } from './mock_data';
import store from '~/boards/stores';
-import { GlLabel } from '@gitlab/ui';
describe('Issue card component', () => {
const user = new ListAssignee({
diff --git a/spec/frontend/boards/issue_spec.js b/spec/frontend/boards/issue_spec.js
index 412f20684f5..d68e17c06a7 100644
--- a/spec/frontend/boards/issue_spec.js
+++ b/spec/frontend/boards/issue_spec.js
@@ -5,7 +5,7 @@ import '~/boards/models/assignee';
import '~/boards/models/issue';
import '~/boards/models/list';
import boardsStore from '~/boards/stores/boards_store';
-import { setMockEndpoints } from './mock_data';
+import { setMockEndpoints, mockIssue } from './mock_data';
describe('Issue model', () => {
let issue;
@@ -14,28 +14,7 @@ describe('Issue model', () => {
setMockEndpoints();
boardsStore.create();
- issue = new ListIssue({
- title: 'Testing',
- id: 1,
- iid: 1,
- confidential: false,
- labels: [
- {
- id: 1,
- title: 'test',
- color: 'red',
- description: 'testing',
- },
- ],
- assignees: [
- {
- id: 1,
- name: 'name',
- username: 'username',
- avatar_url: 'http://avatar_url',
- },
- ],
- });
+ issue = new ListIssue(mockIssue);
});
it('has label', () => {
diff --git a/spec/frontend/boards/list_spec.js b/spec/frontend/boards/list_spec.js
index b30281f8df5..b731bb6e474 100644
--- a/spec/frontend/boards/list_spec.js
+++ b/spec/frontend/boards/list_spec.js
@@ -4,6 +4,7 @@
/* global ListLabel */
import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import '~/boards/models/label';
import '~/boards/models/assignee';
@@ -11,7 +12,6 @@ import '~/boards/models/issue';
import '~/boards/models/list';
import { ListType } from '~/boards/constants';
import boardsStore from '~/boards/stores/boards_store';
-import waitForPromises from 'helpers/wait_for_promises';
import { listObj, listObjDuplicate, boardsMockInterceptor } from './mock_data';
describe('List model', () => {
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index 97d49de6f2e..8ef6efe23c7 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -92,6 +92,29 @@ export const mockMilestone = {
due_date: '2019-12-31',
};
+export const mockIssue = {
+ title: 'Testing',
+ id: 1,
+ iid: 1,
+ confidential: false,
+ labels: [
+ {
+ id: 1,
+ title: 'test',
+ color: 'red',
+ description: 'testing',
+ },
+ ],
+ assignees: [
+ {
+ id: 1,
+ name: 'name',
+ username: 'username',
+ avatar_url: 'http://avatar_url',
+ },
+ ],
+};
+
export const BoardsMockData = {
GET: {
'/test/-/boards/1/lists/300/issues?id=300&page=1': {
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index 0debca1310a..d539cba76ca 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1,6 +1,7 @@
+import testAction from 'helpers/vuex_action_helper';
import actions from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
-import testAction from 'helpers/vuex_action_helper';
+import { inactiveId } from '~/boards/constants';
const expectNotImplemented = action => {
it('is not implemented', () => {
@@ -8,19 +9,36 @@ const expectNotImplemented = action => {
});
};
-describe('setEndpoints', () => {
- it('sets endpoints object', () => {
- const mockEndpoints = {
+describe('setInitialBoardData', () => {
+ it('sets data object', () => {
+ const mockData = {
foo: 'bar',
bar: 'baz',
};
return testAction(
- actions.setEndpoints,
- mockEndpoints,
+ actions.setInitialBoardData,
+ mockData,
{},
- [{ type: types.SET_ENDPOINTS, payload: mockEndpoints }],
+ [{ type: types.SET_INITIAL_BOARD_DATA, payload: mockData }],
+ [],
+ );
+ });
+});
+
+describe('setActiveId', () => {
+ it('should commit mutation SET_ACTIVE_ID', done => {
+ const state = {
+ activeId: inactiveId,
+ };
+
+ testAction(
+ actions.setActiveId,
+ 1,
+ state,
+ [{ type: types.SET_ACTIVE_ID, payload: 1 }],
[],
+ done,
);
});
});
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index bc57c30b354..c1f7f3dda6e 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -1,6 +1,6 @@
import mutations from '~/boards/stores/mutations';
-import * as types from '~/boards/stores/mutation_types';
import defaultState from '~/boards/stores/state';
+import { mockIssue } from '../mock_data';
const expectNotImplemented = action => {
it('is not implemented', () => {
@@ -15,7 +15,7 @@ describe('Board Store Mutations', () => {
state = defaultState();
});
- describe('SET_ENDPOINTS', () => {
+ describe('SET_INITIAL_BOARD_DATA', () => {
it('Should set initial Boards data to state', () => {
const endpoints = {
boardsEndpoint: '/boards/',
@@ -25,10 +25,22 @@ describe('Board Store Mutations', () => {
boardId: 1,
fullPath: 'gitlab-org',
};
+ const boardType = 'group';
- mutations[types.SET_ENDPOINTS](state, endpoints);
+ mutations.SET_INITIAL_BOARD_DATA(state, { ...endpoints, boardType });
expect(state.endpoints).toEqual(endpoints);
+ expect(state.boardType).toEqual(boardType);
+ });
+ });
+
+ describe('SET_ACTIVE_ID', () => {
+ it('updates activeListId to be the value that is passed', () => {
+ const expectedId = 1;
+
+ mutations.SET_ACTIVE_ID(state, expectedId);
+
+ expect(state.activeId).toBe(expectedId);
});
});
@@ -68,6 +80,35 @@ describe('Board Store Mutations', () => {
expectNotImplemented(mutations.RECEIVE_REMOVE_LIST_ERROR);
});
+ describe('REQUEST_ISSUES_FOR_ALL_LISTS', () => {
+ it('sets isLoadingIssues to true', () => {
+ expect(state.isLoadingIssues).toBe(false);
+
+ mutations.REQUEST_ISSUES_FOR_ALL_LISTS(state);
+
+ expect(state.isLoadingIssues).toBe(true);
+ });
+ });
+
+ describe('RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS', () => {
+ it('sets isLoadingIssues to false and updates issuesByListId object', () => {
+ const listIssues = {
+ '1': [mockIssue],
+ };
+
+ state = {
+ ...state,
+ isLoadingIssues: true,
+ issuesByListId: {},
+ };
+
+ mutations.RECEIVE_ISSUES_FOR_ALL_LISTS_SUCCESS(state, listIssues);
+
+ expect(state.isLoadingIssues).toBe(false);
+ expect(state.issuesByListId).toEqual(listIssues);
+ });
+ });
+
describe('REQUEST_ADD_ISSUE', () => {
expectNotImplemented(mutations.REQUEST_ADD_ISSUE);
});
diff --git a/spec/frontend/branches/components/__snapshots__/divergence_graph_spec.js.snap b/spec/frontend/branches/components/__snapshots__/divergence_graph_spec.js.snap
index c9948db95f8..261c406171e 100644
--- a/spec/frontend/branches/components/__snapshots__/divergence_graph_spec.js.snap
+++ b/spec/frontend/branches/components/__snapshots__/divergence_graph_spec.js.snap
@@ -12,7 +12,7 @@ exports[`Branch divergence graph component renders ahead and behind count 1`] =
/>
<div
- class="graph-separator pull-left mt-1"
+ class="graph-separator float-left mt-1"
/>
<graph-bar-stub
diff --git a/spec/frontend/ci_variable_list/components/ci_enviroments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/ci_enviroments_dropdown_spec.js
index a52b38599f7..7785d436834 100644
--- a/spec/frontend/ci_variable_list/components/ci_enviroments_dropdown_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_enviroments_dropdown_spec.js
@@ -1,7 +1,7 @@
import Vuex from 'vuex';
-import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlDropdownItem, GlIcon } from '@gitlab/ui';
+import { GlDeprecatedDropdownItem, GlIcon } from '@gitlab/ui';
+import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -26,8 +26,8 @@ describe('Ci environments dropdown', () => {
});
};
- const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
- const findDropdownItemByIndex = index => wrapper.findAll(GlDropdownItem).at(index);
+ const findAllDropdownItems = () => wrapper.findAll(GlDeprecatedDropdownItem);
+ const findDropdownItemByIndex = index => wrapper.findAll(GlDeprecatedDropdownItem).at(index);
const findActiveIconByIndex = index => wrapper.findAll(GlIcon).at(index);
afterEach(() => {
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
index ad398d6ccd6..4e35243f484 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
@@ -1,6 +1,6 @@
import Vuex from 'vuex';
import { createLocalVue, shallowMount, mount } from '@vue/test-utils';
-import { GlDeprecatedButton, GlFormCombobox } from '@gitlab/ui';
+import { GlButton, GlFormCombobox } from '@gitlab/ui';
import { AWS_ACCESS_KEY_ID } from '~/ci_variable_list/constants';
import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
import createStore from '~/ci_variable_list/store';
@@ -29,14 +29,14 @@ describe('Ci variable modal', () => {
};
const findModal = () => wrapper.find(ModalStub);
- const addOrUpdateButton = index =>
+ const findAddorUpdateButton = () =>
findModal()
- .findAll(GlDeprecatedButton)
- .at(index);
+ .findAll(GlButton)
+ .wrappers.find(button => button.props('variant') === 'success');
const deleteVariableButton = () =>
findModal()
- .findAll(GlDeprecatedButton)
- .at(1);
+ .findAll(GlButton)
+ .wrappers.find(button => button.props('variant') === 'danger');
afterEach(() => {
wrapper.destroy();
@@ -69,7 +69,7 @@ describe('Ci variable modal', () => {
});
it('button is disabled when no key/value pair are present', () => {
- expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
});
});
@@ -82,11 +82,11 @@ describe('Ci variable modal', () => {
});
it('button is enabled when key/value pair are present', () => {
- expect(addOrUpdateButton(1).attributes('disabled')).toBeFalsy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
});
it('Add variable button dispatches addVariable action', () => {
- addOrUpdateButton(1).vm.$emit('click');
+ findAddorUpdateButton().vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('addVariable');
});
@@ -152,11 +152,11 @@ describe('Ci variable modal', () => {
});
it('button text is Update variable when updating', () => {
- expect(addOrUpdateButton(2).text()).toBe('Update variable');
+ expect(findAddorUpdateButton().text()).toBe('Update variable');
});
it('Update variable button dispatches updateVariable with correct variable', () => {
- addOrUpdateButton(2).vm.$emit('click');
+ findAddorUpdateButton().vm.$emit('click');
expect(store.dispatch).toHaveBeenCalledWith('updateVariable');
});
@@ -189,7 +189,7 @@ describe('Ci variable modal', () => {
});
it('disables the submit button', () => {
- expect(addOrUpdateButton(1).attributes('disabled')).toBeTruthy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeTruthy();
});
it('shows the correct error text', () => {
@@ -213,7 +213,7 @@ describe('Ci variable modal', () => {
});
it('does not disable the submit button', () => {
- expect(addOrUpdateButton(1).attributes('disabled')).toBeFalsy();
+ expect(findAddorUpdateButton().attributes('disabled')).toBeFalsy();
});
});
});
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js b/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js
index 46f77a6f11e..5d37f059161 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js
+++ b/spec/frontend/ci_variable_list/components/ci_variable_popover_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import CiVariablePopover from '~/ci_variable_list/components/ci_variable_popover.vue';
import mockData from '../services/mock_data';
@@ -18,7 +18,7 @@ describe('Ci Variable Popover', () => {
});
};
- const findButton = () => wrapper.find(GlDeprecatedButton);
+ const findButton = () => wrapper.find(GlButton);
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js
index eb565d4c979..4b89e467df0 100644
--- a/spec/frontend/ci_variable_list/store/actions_spec.js
+++ b/spec/frontend/ci_variable_list/store/actions_spec.js
@@ -1,8 +1,8 @@
-import Api from '~/api';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import getInitialState from '~/ci_variable_list/store/state';
import * as actions from '~/ci_variable_list/store/actions';
import * as types from '~/ci_variable_list/store/mutation_types';
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index a9870e4db57..d3277cdb7cc 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -1,14 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
-import $ from 'jquery';
import { loadHTMLFixture } from 'helpers/fixtures';
import { setTestTimeout } from 'helpers/timeout';
import Clusters from '~/clusters/clusters_bundle';
-import {
- APPLICATION_STATUS,
- INGRESS_DOMAIN_SUFFIX,
- APPLICATIONS,
- RUNNER,
-} from '~/clusters/constants';
+import { APPLICATION_STATUS, APPLICATIONS, RUNNER } from '~/clusters/constants';
import axios from '~/lib/utils/axios_utils';
import initProjectSelectDropdown from '~/project_select';
@@ -63,25 +57,6 @@ describe('Clusters', () => {
});
});
- describe('toggle', () => {
- it('should update the button and the input field on click', done => {
- const toggleButton = document.querySelector(
- '.js-cluster-enable-toggle-area .js-project-feature-toggle',
- );
- const toggleInput = document.querySelector(
- '.js-cluster-enable-toggle-area .js-project-feature-toggle-input',
- );
-
- $(toggleInput).one('trigger-change', () => {
- expect(toggleButton.classList).not.toContain('is-checked');
- expect(toggleInput.getAttribute('value')).toEqual('false');
- done();
- });
-
- toggleButton.click();
- });
- });
-
describe('checkForNewInstalls', () => {
const INITIAL_APP_MAP = {
helm: { status: null, title: 'Helm Tiller' },
@@ -328,7 +303,6 @@ describe('Clusters', () => {
return promise.then(() => {
expect(cluster.store.state.applications.helm.status).toEqual(INSTALLED);
expect(cluster.store.state.applications.helm.uninstallFailed).toBe(true);
-
expect(cluster.store.state.applications.helm.requestReason).toBeDefined();
});
});
@@ -354,10 +328,8 @@ describe('Clusters', () => {
describe('handleClusterStatusSuccess', () => {
beforeEach(() => {
jest.spyOn(cluster.store, 'updateStateFromServer').mockReturnThis();
- jest.spyOn(cluster, 'toggleIngressDomainHelpText').mockReturnThis();
jest.spyOn(cluster, 'checkForNewInstalls').mockReturnThis();
jest.spyOn(cluster, 'updateContainer').mockReturnThis();
-
cluster.handleClusterStatusSuccess({ data: {} });
});
@@ -369,53 +341,11 @@ describe('Clusters', () => {
expect(cluster.checkForNewInstalls).toHaveBeenCalled();
});
- it('toggles ingress domain help text', () => {
- expect(cluster.toggleIngressDomainHelpText).toHaveBeenCalled();
- });
-
it('updates message containers', () => {
expect(cluster.updateContainer).toHaveBeenCalled();
});
});
- describe('toggleIngressDomainHelpText', () => {
- let ingressPreviousState;
- let ingressNewState;
-
- beforeEach(() => {
- ingressPreviousState = { externalIp: null };
- ingressNewState = { externalIp: '127.0.0.1' };
- });
-
- describe(`when ingress have an external ip assigned`, () => {
- beforeEach(() => {
- cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
- });
-
- it('displays custom domain help text', () => {
- expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(false);
- });
-
- it('updates ingress external ip address', () => {
- expect(cluster.ingressDomainSnippet.textContent).toEqual(
- `${ingressNewState.externalIp}${INGRESS_DOMAIN_SUFFIX}`,
- );
- });
- });
-
- describe(`when ingress does not have an external ip assigned`, () => {
- it('hides custom domain help text', () => {
- ingressPreviousState.externalIp = '127.0.0.1';
- ingressNewState.externalIp = null;
- cluster.ingressDomainHelpText.classList.remove('hide');
-
- cluster.toggleIngressDomainHelpText(ingressPreviousState, ingressNewState);
-
- expect(cluster.ingressDomainHelpText.classList.contains('hide')).toEqual(true);
- });
- });
- });
-
describe('updateApplication', () => {
const params = { version: '1.0.0' };
let storeUpdateApplication;
diff --git a/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap
index 92237590550..3328ec724fd 100644
--- a/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/applications_spec.js.snap
@@ -17,6 +17,22 @@ exports[`Applications Cert-Manager application shows the correct description 1`]
</p>
`;
+exports[`Applications Cilium application shows the correct description 1`] = `
+<p
+ data-testid="ciliumDescription"
+>
+ Protect your clusters with GitLab Container Network Policies by enforcing how pods communicate with each other and other network endpoints.
+ <a
+ class="gl-link"
+ href="cilium-help-path"
+ rel="noopener"
+ target="_blank"
+ >
+ Learn more about configuring Network Policies here.
+ </a>
+</p>
+`;
+
exports[`Applications Crossplane application shows the correct description 1`] = `
<p
data-testid="crossplaneDescription"
diff --git a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
index d4269bf14ba..93b757e008a 100644
--- a/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
+++ b/spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap
@@ -1,7 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Remove cluster confirmation modal renders splitbutton with modal included 1`] = `
-<div>
+<div
+ class="gl-display-flex gl-justify-content-end"
+>
<div
class="dropdown b-dropdown gl-dropdown btn-group"
>
diff --git a/spec/frontend/clusters/components/application_row_spec.js b/spec/frontend/clusters/components/application_row_spec.js
index 94bdd7b7778..b97d4dbf355 100644
--- a/spec/frontend/clusters/components/application_row_spec.js
+++ b/spec/frontend/clusters/components/application_row_spec.js
@@ -83,6 +83,12 @@ describe('Application Row', () => {
checkButtonState('Installing', true, true);
});
+ it('has disabled "Install" when APPLICATION_STATUS.UNINSTALLED', () => {
+ mountComponent({ status: APPLICATION_STATUS.UNINSTALLED });
+
+ checkButtonState('Install', false, true);
+ });
+
it('has disabled "Installed" when application is installed and not uninstallable', () => {
mountComponent({
status: APPLICATION_STATUS.INSTALLED,
@@ -112,6 +118,15 @@ describe('Application Row', () => {
checkButtonState('Install', false, false);
});
+ it('has disabled "Install" when installation disabled', () => {
+ mountComponent({
+ status: APPLICATION_STATUS.INSTALLABLE,
+ installable: false,
+ });
+
+ checkButtonState('Install', false, true);
+ });
+
it('has enabled "Install" when REQUEST_FAILURE (so you can try installing again)', () => {
mountComponent({ status: APPLICATION_STATUS.INSTALLABLE });
diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index 7fc771201c1..e0ccf36e868 100644
--- a/spec/frontend/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -14,10 +14,9 @@ describe('Applications', () => {
beforeEach(() => {
gon.features = gon.features || {};
- gon.features.managedAppsLocalTiller = false;
});
- const createApp = ({ applications, type } = {}, isShallow) => {
+ const createApp = ({ applications, type, props } = {}, isShallow) => {
const mountMethod = isShallow ? shallowMount : mount;
wrapper = mountMethod(Applications, {
@@ -25,6 +24,7 @@ describe('Applications', () => {
propsData: {
type,
applications: { ...APPLICATIONS_MOCK_STATE, ...applications },
+ ...props,
},
});
};
@@ -40,10 +40,6 @@ describe('Applications', () => {
createApp({ type: CLUSTER_TYPE.PROJECT });
});
- it('renders a row for Helm Tiller', () => {
- expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(true);
- });
-
it('renders a row for Ingress', () => {
expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
});
@@ -79,6 +75,9 @@ describe('Applications', () => {
it('renders a row for Fluentd', () => {
expect(wrapper.find('.js-cluster-application-row-fluentd').exists()).toBe(true);
});
+ it('renders a row for Cilium', () => {
+ expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
+ });
});
describe('Group cluster applications', () => {
@@ -86,10 +85,6 @@ describe('Applications', () => {
createApp({ type: CLUSTER_TYPE.GROUP });
});
- it('renders a row for Helm Tiller', () => {
- expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(true);
- });
-
it('renders a row for Ingress', () => {
expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
});
@@ -125,6 +120,10 @@ describe('Applications', () => {
it('renders a row for Fluentd', () => {
expect(wrapper.find('.js-cluster-application-row-fluentd').exists()).toBe(true);
});
+
+ it('renders a row for Cilium', () => {
+ expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
+ });
});
describe('Instance cluster applications', () => {
@@ -132,10 +131,6 @@ describe('Applications', () => {
createApp({ type: CLUSTER_TYPE.INSTANCE });
});
- it('renders a row for Helm Tiller', () => {
- expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(true);
- });
-
it('renders a row for Ingress', () => {
expect(wrapper.find('.js-cluster-application-row-ingress').exists()).toBe(true);
});
@@ -171,18 +166,16 @@ describe('Applications', () => {
it('renders a row for Fluentd', () => {
expect(wrapper.find('.js-cluster-application-row-fluentd').exists()).toBe(true);
});
+
+ it('renders a row for Cilium', () => {
+ expect(wrapper.find('.js-cluster-application-row-cilium').exists()).toBe(true);
+ });
});
describe('Helm application', () => {
- describe('when managedAppsLocalTiller enabled', () => {
- beforeEach(() => {
- gon.features.managedAppsLocalTiller = true;
- });
-
- it('does not render a row for Helm Tiller', () => {
- createApp();
- expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(false);
- });
+ it('does not render a row for Helm Tiller', () => {
+ createApp();
+ expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(false);
});
});
@@ -240,7 +233,6 @@ describe('Applications', () => {
externalHostname: 'localhost.localdomain',
modsecurity_enabled: false,
},
- helm: { title: 'Helm Tiller' },
cert_manager: { title: 'Cert-Manager' },
crossplane: { title: 'Crossplane', stack: '' },
runner: { title: 'GitLab Runner' },
@@ -249,6 +241,7 @@ describe('Applications', () => {
knative: { title: 'Knative', hostname: '' },
elastic_stack: { title: 'Elastic Stack' },
fluentd: { title: 'Fluentd' },
+ cilium: { title: 'GitLab Container Network Policies' },
},
});
@@ -365,7 +358,11 @@ describe('Applications', () => {
it('renders readonly input', () => {
createApp({
applications: {
- ingress: { title: 'Ingress', status: 'installed', externalIp: '1.1.1.1' },
+ ingress: {
+ title: 'Ingress',
+ status: 'installed',
+ externalIp: '1.1.1.1',
+ },
jupyter: { title: 'JupyterHub', status: 'installed', hostname: '' },
},
});
@@ -386,14 +383,6 @@ describe('Applications', () => {
false,
);
});
-
- it('renders disabled install button', () => {
- expect(
- wrapper
- .find('.js-cluster-application-row-jupyter .js-cluster-application-install-button')
- .attributes('disabled'),
- ).toEqual('disabled');
- });
});
});
@@ -513,7 +502,7 @@ describe('Applications', () => {
describe('Elastic Stack application', () => {
describe('with elastic stack installable', () => {
- it('renders hostname active input', () => {
+ it('renders the install button enabled', () => {
createApp();
expect(
@@ -522,7 +511,7 @@ describe('Applications', () => {
'.js-cluster-application-row-elastic_stack .js-cluster-application-install-button',
)
.attributes('disabled'),
- ).toEqual('disabled');
+ ).toBeUndefined();
});
});
@@ -552,4 +541,11 @@ describe('Applications', () => {
expect(wrapper.find(FluentdOutputSettings).exists()).toBe(true);
});
});
+
+ describe('Cilium application', () => {
+ it('shows the correct description', () => {
+ createApp({ props: { ciliumHelpPath: 'cilium-help-path' } });
+ expect(findByTestId('ciliumDescription').element).toMatchSnapshot();
+ });
+ });
});
diff --git a/spec/frontend/clusters/components/fluentd_output_settings_spec.js b/spec/frontend/clusters/components/fluentd_output_settings_spec.js
index f03f2535947..0bc4eb73bf9 100644
--- a/spec/frontend/clusters/components/fluentd_output_settings_spec.js
+++ b/spec/frontend/clusters/components/fluentd_output_settings_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
+import { GlAlert, GlDeprecatedDropdown, GlFormCheckbox } from '@gitlab/ui';
import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings.vue';
import { APPLICATION_STATUS, FLUENTD } from '~/clusters/constants';
-import { GlAlert, GlDropdown, GlFormCheckbox } from '@gitlab/ui';
import eventHub from '~/clusters/event_hub';
const { UPDATING } = APPLICATION_STATUS;
@@ -36,7 +36,7 @@ describe('FluentdOutputSettings', () => {
};
const findSaveButton = () => wrapper.find({ ref: 'saveBtn' });
const findCancelButton = () => wrapper.find({ ref: 'cancelBtn' });
- const findProtocolDropdown = () => wrapper.find(GlDropdown);
+ const findProtocolDropdown = () => wrapper.find(GlDeprecatedDropdown);
const findCheckbox = name =>
wrapper.findAll(GlFormCheckbox).wrappers.find(x => x.text() === name);
const findHost = () => wrapper.find('#fluentd-host');
diff --git a/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js b/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js
index 683f2e5c35a..3a9a608b2e2 100644
--- a/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js
+++ b/spec/frontend/clusters/components/ingress_modsecurity_settings_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
+import { GlAlert, GlToggle, GlDeprecatedDropdown } from '@gitlab/ui';
import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue';
import { APPLICATION_STATUS, INGRESS } from '~/clusters/constants';
-import { GlAlert, GlToggle, GlDropdown } from '@gitlab/ui';
import eventHub from '~/clusters/event_hub';
const { UPDATING } = APPLICATION_STATUS;
@@ -31,7 +31,7 @@ describe('IngressModsecuritySettings', () => {
const findSaveButton = () => wrapper.find('.btn-success');
const findCancelButton = () => wrapper.find('[variant="secondary"]');
const findModSecurityToggle = () => wrapper.find(GlToggle);
- const findModSecurityDropdown = () => wrapper.find(GlDropdown);
+ const findModSecurityDropdown = () => wrapper.find(GlDeprecatedDropdown);
describe('when ingress is installed', () => {
beforeEach(() => {
diff --git a/spec/frontend/clusters/components/knative_domain_editor_spec.js b/spec/frontend/clusters/components/knative_domain_editor_spec.js
index 73d08661199..a07258dcc69 100644
--- a/spec/frontend/clusters/components/knative_domain_editor_spec.js
+++ b/spec/frontend/clusters/components/knative_domain_editor_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdownItem } from '@gitlab/ui';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import LoadingButton from '~/vue_shared/components/loading_button.vue';
import { APPLICATION_STATUS } from '~/clusters/constants';
@@ -113,7 +113,7 @@ describe('KnativeDomainEditor', () => {
createComponent({ knative: { ...knative, availableDomains: [newDomain] } });
jest.spyOn(wrapper.vm, 'selectDomain');
- wrapper.find(GlDropdownItem).vm.$emit('click');
+ wrapper.find(GlDeprecatedDropdownItem).vm.$emit('click');
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.vm.selectDomain).toHaveBeenCalledWith(newDomain);
diff --git a/spec/frontend/clusters/forms/components/integration_form_spec.js b/spec/frontend/clusters/forms/components/integration_form_spec.js
new file mode 100644
index 00000000000..3a3700eb0b7
--- /dev/null
+++ b/spec/frontend/clusters/forms/components/integration_form_spec.js
@@ -0,0 +1,112 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlToggle, GlButton } from '@gitlab/ui';
+import IntegrationForm from '~/clusters/forms/components/integration_form.vue';
+import { createStore } from '~/clusters/forms/stores/index';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('ClusterIntegrationForm', () => {
+ let wrapper;
+
+ const defaultStoreValues = {
+ enabled: true,
+ editable: true,
+ environmentScope: '*',
+ baseDomain: 'testDomain',
+ applicationIngressExternalIp: null,
+ };
+
+ const createWrapper = (storeValues = defaultStoreValues) => {
+ wrapper = shallowMount(IntegrationForm, {
+ localVue,
+ store: createStore(storeValues),
+ provide: {
+ autoDevopsHelpPath: 'topics/autodevops/index',
+ externalEndpointHelpPath: 'user/clusters/applications.md',
+ },
+ });
+ };
+
+ const destroyWrapper = () => {
+ wrapper.destroy();
+ wrapper = null;
+ };
+
+ const findSubmitButton = () => wrapper.find(GlButton);
+ const findGlToggle = () => wrapper.find(GlToggle);
+
+ afterEach(() => {
+ destroyWrapper();
+ });
+
+ describe('rendering', () => {
+ beforeEach(() => createWrapper());
+
+ it('enables toggle if editable is true', () => {
+ expect(findGlToggle().props('disabled')).toBe(false);
+ });
+ it('sets the envScope to default', () => {
+ expect(wrapper.find('[id="cluster_environment_scope"]').attributes('value')).toBe('*');
+ });
+
+ it('sets the baseDomain to default', () => {
+ expect(wrapper.find('[id="cluster_base_domain"]').attributes('value')).toBe('testDomain');
+ });
+
+ describe('when editable is false', () => {
+ beforeEach(() => {
+ createWrapper({ ...defaultStoreValues, editable: false });
+ });
+
+ it('disables toggle if editable is false', () => {
+ expect(findGlToggle().props('disabled')).toBe(true);
+ });
+
+ it('does not render the save button', () => {
+ expect(findSubmitButton().exists()).toBe(false);
+ });
+ });
+
+ it('does not render external IP block if applicationIngressExternalIp was not passed', () => {
+ createWrapper({ ...defaultStoreValues });
+
+ expect(wrapper.find('.js-ingress-domain-help-text').exists()).toBe(false);
+ });
+
+ it('renders external IP block if applicationIngressExternalIp was passed', () => {
+ createWrapper({ ...defaultStoreValues, applicationIngressExternalIp: '127.0.0.1' });
+
+ expect(wrapper.find('.js-ingress-domain-help-text').exists()).toBe(true);
+ });
+ });
+
+ describe('reactivity', () => {
+ beforeEach(() => createWrapper());
+
+ it('enables the submit button on changing toggle to different value', () => {
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ // setData is a bad approach because it changes the internal implementation which we should not touch
+ // but our GlFormInput lacks the ability to set a new value.
+ wrapper.setData({ toggleEnabled: !defaultStoreValues.enabled });
+ })
+ .then(() => {
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ });
+ });
+
+ it('enables the submit button on changing input values', () => {
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ wrapper.setData({ envScope: `${defaultStoreValues.environmentScope}1` });
+ })
+ .then(() => {
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/clusters/services/application_state_machine_spec.js b/spec/frontend/clusters/services/application_state_machine_spec.js
index b27cd2c80fd..7eee54949fa 100644
--- a/spec/frontend/clusters/services/application_state_machine_spec.js
+++ b/spec/frontend/clusters/services/application_state_machine_spec.js
@@ -19,6 +19,7 @@ const {
UPDATE_ERRORED,
UNINSTALLING,
UNINSTALL_ERRORED,
+ UNINSTALLED,
} = APPLICATION_STATUS;
const NO_EFFECTS = 'no effects';
@@ -40,6 +41,7 @@ describe('applicationStateMachine', () => {
${INSTALLED} | ${UPDATE_ERRORED} | ${{ updateFailed: true }}
${UNINSTALLING} | ${UNINSTALLING} | ${NO_EFFECTS}
${INSTALLED} | ${UNINSTALL_ERRORED} | ${{ uninstallFailed: true }}
+ ${UNINSTALLED} | ${UNINSTALLED} | ${NO_EFFECTS}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
@@ -74,8 +76,9 @@ describe('applicationStateMachine', () => {
it.each`
expectedState | event | effects
${INSTALLING} | ${INSTALL_EVENT} | ${{ installFailed: false }}
- ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${INSTALLED} | ${INSTALLED} | ${{ installFailed: false }}
${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
+ ${UNINSTALLED} | ${UNINSTALLED} | ${{ installFailed: false }}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
@@ -113,6 +116,8 @@ describe('applicationStateMachine', () => {
${UPDATING} | ${UPDATE_EVENT} | ${{ updateFailed: false, updateSuccessful: false }}
${UNINSTALLING} | ${UNINSTALL_EVENT} | ${{ uninstallFailed: false, uninstallSuccessful: false }}
${NOT_INSTALLABLE} | ${NOT_INSTALLABLE} | ${NO_EFFECTS}
+ ${UNINSTALLED} | ${UNINSTALLED} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
`(`transitions to $expectedState on $event event and applies $effects`, data => {
const { expectedState, event, effects } = data;
const currentAppState = {
@@ -162,6 +167,23 @@ describe('applicationStateMachine', () => {
});
});
+ describe(`current state is ${UNINSTALLED}`, () => {
+ it.each`
+ expectedState | event | effects
+ ${INSTALLED} | ${INSTALLED} | ${NO_EFFECTS}
+ ${INSTALLABLE} | ${ERROR} | ${{ installFailed: true }}
+ `(`transitions to $expectedState on $event event and applies $effects`, data => {
+ const { expectedState, event, effects } = data;
+ const currentAppState = {
+ status: UNINSTALLED,
+ };
+
+ expect(transitionApplicationState(currentAppState, event)).toEqual({
+ status: expectedState,
+ ...noEffectsToEmptyObject(effects),
+ });
+ });
+ });
describe('current state is undefined', () => {
it('returns the current state without having any effects', () => {
const currentAppState = {};
diff --git a/spec/frontend/clusters/services/crossplane_provider_stack_spec.js b/spec/frontend/clusters/services/crossplane_provider_stack_spec.js
index 3e5f8de8e7b..57c538d2650 100644
--- a/spec/frontend/clusters/services/crossplane_provider_stack_spec.js
+++ b/spec/frontend/clusters/services/crossplane_provider_stack_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDropdownItem, GlIcon } from '@gitlab/ui';
+import { GlDeprecatedDropdownItem, GlIcon } from '@gitlab/ui';
import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
describe('CrossplaneProviderStack component', () => {
@@ -37,7 +37,7 @@ describe('CrossplaneProviderStack component', () => {
createComponent({ crossplane });
});
- const findDropdownElements = () => wrapper.findAll(GlDropdownItem);
+ const findDropdownElements = () => wrapper.findAll(GlDeprecatedDropdownItem);
const findFirstDropdownElement = () => findDropdownElements().at(0);
afterEach(() => {
diff --git a/spec/frontend/clusters/services/mock_data.js b/spec/frontend/clusters/services/mock_data.js
index c5ec3f6e6a8..4f8b27d623c 100644
--- a/spec/frontend/clusters/services/mock_data.js
+++ b/spec/frontend/clusters/services/mock_data.js
@@ -151,7 +151,11 @@ const DEFAULT_APPLICATION_STATE = {
const APPLICATIONS_MOCK_STATE = {
helm: { title: 'Helm Tiller', status: 'installable' },
- ingress: { title: 'Ingress', status: 'installable', modsecurity_enabled: false },
+ ingress: {
+ title: 'Ingress',
+ status: 'installable',
+ modsecurity_enabled: false,
+ },
crossplane: { title: 'Crossplane', status: 'installable', stack: '' },
cert_manager: { title: 'Cert-Manager', status: 'installable' },
runner: { title: 'GitLab Runner' },
@@ -160,6 +164,10 @@ const APPLICATIONS_MOCK_STATE = {
knative: { title: 'Knative ', status: 'installable', hostname: '' },
elastic_stack: { title: 'Elastic Stack', status: 'installable' },
fluentd: { title: 'Fluentd', status: 'installable' },
+ cilium: {
+ title: 'GitLab Container Network Policies',
+ status: 'not_installable',
+ },
};
export { CLUSTERS_MOCK_DATA, DEFAULT_APPLICATION_STATE, APPLICATIONS_MOCK_STATE };
diff --git a/spec/frontend/clusters/stores/clusters_store_spec.js b/spec/frontend/clusters/stores/clusters_store_spec.js
index 36e99c37be5..ed862818c7b 100644
--- a/spec/frontend/clusters/stores/clusters_store_spec.js
+++ b/spec/frontend/clusters/stores/clusters_store_spec.js
@@ -66,6 +66,7 @@ describe('Clusters Store', () => {
status: mockResponseData.applications[0].status,
statusReason: mockResponseData.applications[0].status_reason,
requestReason: null,
+ installable: true,
installed: false,
installFailed: false,
uninstallable: false,
@@ -80,6 +81,7 @@ describe('Clusters Store', () => {
requestReason: null,
externalIp: null,
externalHostname: null,
+ installable: true,
installed: false,
isEditingModSecurityEnabled: false,
isEditingModSecurityMode: false,
@@ -100,6 +102,7 @@ describe('Clusters Store', () => {
version: mockResponseData.applications[2].version,
updateAvailable: mockResponseData.applications[2].update_available,
chartRepo: 'https://gitlab.com/gitlab-org/charts/gitlab-runner',
+ installable: true,
installed: false,
installFailed: false,
updateFailed: false,
@@ -114,6 +117,7 @@ describe('Clusters Store', () => {
status: APPLICATION_STATUS.INSTALLABLE,
statusReason: mockResponseData.applications[3].status_reason,
requestReason: null,
+ installable: true,
installed: false,
installFailed: true,
uninstallable: false,
@@ -130,6 +134,7 @@ describe('Clusters Store', () => {
ciliumLogEnabled: null,
host: null,
protocol: null,
+ installable: true,
installed: false,
isEditingSettings: false,
installFailed: false,
@@ -145,6 +150,7 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[4].status_reason,
requestReason: null,
hostname: '',
+ installable: true,
installed: false,
installFailed: false,
uninstallable: false,
@@ -161,6 +167,7 @@ describe('Clusters Store', () => {
isEditingDomain: false,
externalIp: null,
externalHostname: null,
+ installable: true,
installed: false,
installFailed: false,
uninstallable: false,
@@ -177,6 +184,7 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[6].status_reason,
requestReason: null,
email: mockResponseData.applications[6].email,
+ installable: true,
installed: false,
uninstallable: false,
uninstallSuccessful: false,
@@ -189,6 +197,7 @@ describe('Clusters Store', () => {
installFailed: true,
statusReason: mockResponseData.applications[7].status_reason,
requestReason: null,
+ installable: true,
installed: false,
uninstallable: false,
uninstallSuccessful: false,
@@ -201,12 +210,26 @@ describe('Clusters Store', () => {
installFailed: true,
statusReason: mockResponseData.applications[8].status_reason,
requestReason: null,
+ installable: true,
installed: false,
uninstallable: false,
uninstallSuccessful: false,
uninstallFailed: false,
validationError: null,
},
+ cilium: {
+ title: 'GitLab Container Network Policies',
+ status: null,
+ statusReason: null,
+ requestReason: null,
+ installable: false,
+ installed: false,
+ installFailed: false,
+ uninstallable: false,
+ uninstallSuccessful: false,
+ uninstallFailed: false,
+ validationError: null,
+ },
},
environments: [],
fetchingEnvironments: false,
diff --git a/spec/frontend/clusters_list/components/ancestor_notice_spec.js b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
index c931912eaf9..cff84180f26 100644
--- a/spec/frontend/clusters_list/components/ancestor_notice_spec.js
+++ b/spec/frontend/clusters_list/components/ancestor_notice_spec.js
@@ -1,7 +1,7 @@
-import AncestorNotice from '~/clusters_list/components/ancestor_notice.vue';
-import ClusterStore from '~/clusters_list/store';
import { shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui';
+import AncestorNotice from '~/clusters_list/components/ancestor_notice.vue';
+import ClusterStore from '~/clusters_list/store';
describe('ClustersAncestorNotice', () => {
let store;
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index deb275a9bb9..c6a5f66a627 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -1,11 +1,11 @@
-import axios from '~/lib/utils/axios_utils';
-import Clusters from '~/clusters_list/components/clusters.vue';
-import ClusterStore from '~/clusters_list/store';
import MockAdapter from 'axios-mock-adapter';
-import { apiData } from '../mock_data';
import { mount } from '@vue/test-utils';
import { GlLoadingIcon, GlPagination, GlSkeletonLoading, GlTable } from '@gitlab/ui';
import * as Sentry from '@sentry/browser';
+import axios from '~/lib/utils/axios_utils';
+import Clusters from '~/clusters_list/components/clusters.vue';
+import ClusterStore from '~/clusters_list/store';
+import { apiData } from '../mock_data';
describe('Clusters', () => {
let mock;
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index c8556350747..053128a179a 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -1,14 +1,14 @@
import MockAdapter from 'axios-mock-adapter';
-import Poll from '~/lib/utils/poll';
-import flashError from '~/flash';
import testAction from 'helpers/vuex_action_helper';
-import axios from '~/lib/utils/axios_utils';
import waitForPromises from 'helpers/wait_for_promises';
+import * as Sentry from '@sentry/browser';
+import Poll from '~/lib/utils/poll';
+import { deprecatedCreateFlash as flashError } from '~/flash';
+import axios from '~/lib/utils/axios_utils';
import { apiData } from '../mock_data';
import { MAX_REQUESTS } from '~/clusters_list/constants';
import * as types from '~/clusters_list/store/mutation_types';
import * as actions from '~/clusters_list/store/actions';
-import * as Sentry from '@sentry/browser';
jest.mock('~/flash.js');
diff --git a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
index 161c2bade05..745a163951a 100644
--- a/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
+++ b/spec/frontend/code_navigation/components/__snapshots__/popover_spec.js.snap
@@ -56,7 +56,7 @@ exports[`Code navigation popover component renders popover 1`] = `
class="popover-body border-top"
>
<gl-button-stub
- category="tertiary"
+ category="primary"
class="w-100"
data-testid="go-to-definition-btn"
href="http://gitlab.com/test.js"
diff --git a/spec/frontend/collapsed_sidebar_todo_spec.js b/spec/frontend/collapsed_sidebar_todo_spec.js
index 0ea797ce4b3..0c74491aa74 100644
--- a/spec/frontend/collapsed_sidebar_todo_spec.js
+++ b/spec/frontend/collapsed_sidebar_todo_spec.js
@@ -1,10 +1,10 @@
/* eslint-disable no-new */
import { clone } from 'lodash';
import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import Sidebar from '~/right_sidebar';
import waitForPromises from './helpers/wait_for_promises';
-import { TEST_HOST } from 'spec/test_constants';
describe('Issuable right sidebar collapsed todo toggle', () => {
const fixtureName = 'issues/open-issue.html';
diff --git a/spec/frontend/commit/commit_pipeline_status_component_spec.js b/spec/frontend/commit/commit_pipeline_status_component_spec.js
index 9281d1d02a3..1086985eec0 100644
--- a/spec/frontend/commit/commit_pipeline_status_component_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_component_spec.js
@@ -2,7 +2,7 @@ import Visibility from 'visibilityjs';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Poll from '~/lib/utils/poll';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
import { getJSONFixture } from '../helpers/fixtures';
diff --git a/spec/frontend/commit/pipelines/pipelines_spec.js b/spec/frontend/commit/pipelines/pipelines_spec.js
index 86ae207e7b7..fdf3c2e85f3 100644
--- a/spec/frontend/commit/pipelines/pipelines_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_spec.js
@@ -121,14 +121,14 @@ describe('Pipelines table in Commits and Merge requests', () => {
pipelineCopy = { ...pipeline };
});
- describe('when latest pipeline has detached flag and canRunPipeline is true', () => {
+ describe('when latest pipeline has detached flag', () => {
it('renders the run pipeline button', done => {
pipelineCopy.flags.detached_merge_request_pipeline = true;
pipelineCopy.flags.merge_request_pipeline = true;
mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
- vm = mountComponent(PipelinesTable, { ...props, canRunPipeline: true });
+ vm = mountComponent(PipelinesTable, { ...props });
setImmediate(() => {
expect(vm.$el.querySelector('.js-run-mr-pipeline')).not.toBeNull();
@@ -137,14 +137,14 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
});
- describe('when latest pipeline has detached flag and canRunPipeline is false', () => {
+ describe('when latest pipeline does not have detached flag', () => {
it('does not render the run pipeline button', done => {
- pipelineCopy.flags.detached_merge_request_pipeline = true;
- pipelineCopy.flags.merge_request_pipeline = true;
+ pipelineCopy.flags.detached_merge_request_pipeline = false;
+ pipelineCopy.flags.merge_request_pipeline = false;
mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
- vm = mountComponent(PipelinesTable, { ...props, canRunPipeline: false });
+ vm = mountComponent(PipelinesTable, { ...props });
setImmediate(() => {
expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull();
@@ -153,39 +153,47 @@ describe('Pipelines table in Commits and Merge requests', () => {
});
});
- describe('when latest pipeline does not have detached flag and canRunPipeline is true', () => {
- it('does not render the run pipeline button', done => {
- pipelineCopy.flags.detached_merge_request_pipeline = false;
- pipelineCopy.flags.merge_request_pipeline = false;
+ describe('on click', () => {
+ const findModal = () =>
+ document.querySelector('#create-pipeline-for-fork-merge-request-modal');
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+ beforeEach(() => {
+ pipelineCopy.flags.detached_merge_request_pipeline = true;
- vm = mountComponent(PipelinesTable, { ...props, canRunPipeline: true });
+ mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
- setImmediate(() => {
- expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull();
- done();
+ vm = mountComponent(PipelinesTable, {
+ ...props,
+ canRunPipeline: true,
+ projectId: '5',
+ mergeRequestId: 3,
});
});
- });
- describe('when latest pipeline does not have detached flag and merge_request_pipeline is true', () => {
- it('does not render the run pipeline button', done => {
- pipelineCopy.flags.detached_merge_request_pipeline = false;
- pipelineCopy.flags.merge_request_pipeline = true;
+ it('updates the loading state', done => {
+ jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve());
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+ setImmediate(() => {
+ vm.$el.querySelector('.js-run-mr-pipeline').click();
- vm = mountComponent(PipelinesTable, { ...props, canRunPipeline: false });
+ vm.$nextTick(() => {
+ expect(findModal()).toBeNull();
+ expect(vm.state.isRunningMergeRequestPipeline).toBe(true);
- setImmediate(() => {
- expect(vm.$el.querySelector('.js-run-mr-pipeline')).toBeNull();
- done();
+ setImmediate(() => {
+ expect(vm.state.isRunningMergeRequestPipeline).toBe(false);
+
+ done();
+ });
+ });
});
});
});
- describe('on click', () => {
+ describe('on click for fork merge request', () => {
+ const findModal = () =>
+ document.querySelector('#create-pipeline-for-fork-merge-request-modal');
+
beforeEach(() => {
pipelineCopy.flags.detached_merge_request_pipeline = true;
@@ -193,26 +201,23 @@ describe('Pipelines table in Commits and Merge requests', () => {
vm = mountComponent(PipelinesTable, {
...props,
- canRunPipeline: true,
projectId: '5',
mergeRequestId: 3,
+ canCreatePipelineInTargetProject: true,
+ sourceProjectFullPath: 'test/parent-project',
+ targetProjectFullPath: 'test/fork-project',
});
});
- it('updates the loading state', done => {
+ it('shows a security warning modal', done => {
jest.spyOn(Api, 'postMergeRequestPipeline').mockReturnValue(Promise.resolve());
setImmediate(() => {
vm.$el.querySelector('.js-run-mr-pipeline').click();
vm.$nextTick(() => {
- expect(vm.state.isRunningMergeRequestPipeline).toBe(true);
-
- setImmediate(() => {
- expect(vm.state.isRunningMergeRequestPipeline).toBe(false);
-
- done();
- });
+ expect(findModal()).not.toBeNull();
+ done();
});
});
});
diff --git a/spec/frontend/confidential_merge_request/components/dropdown_spec.js b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
index 69495f3c161..3e95cd6c0d7 100644
--- a/spec/frontend/confidential_merge_request/components/dropdown_spec.js
+++ b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdownItem } from '@gitlab/ui';
import Dropdown from '~/confidential_merge_request/components/dropdown.vue';
let vm;
@@ -30,7 +30,7 @@ describe('Confidential merge request project dropdown component', () => {
},
]);
- expect(vm.findAll(GlDropdownItem).length).toBe(2);
+ expect(vm.findAll(GlDeprecatedDropdownItem).length).toBe(2);
});
it('renders selected project icon', () => {
diff --git a/spec/frontend/confirm_modal_spec.js b/spec/frontend/confirm_modal_spec.js
index b14d1c3e01d..70076532a94 100644
--- a/spec/frontend/confirm_modal_spec.js
+++ b/spec/frontend/confirm_modal_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import initConfirmModal from '~/confirm_modal';
import { TEST_HOST } from 'helpers/test_constants';
+import initConfirmModal from '~/confirm_modal';
describe('ConfirmModal', () => {
const buttons = [
diff --git a/spec/frontend/contributors/store/actions_spec.js b/spec/frontend/contributors/store/actions_spec.js
index 55437da837c..ad490ea4b67 100644
--- a/spec/frontend/contributors/store/actions_spec.js
+++ b/spec/frontend/contributors/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
-import flashError from '~/flash';
+import { deprecatedCreateFlash as flashError } from '~/flash';
import * as actions from '~/contributors/stores/actions';
import * as types from '~/contributors/stores/mutation_types';
diff --git a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
index 01f7ada9cd6..882a4a002bd 100644
--- a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
@@ -23,7 +23,7 @@ import {
CREATE_CLUSTER_ERROR,
} from '~/create_cluster/eks_cluster/store/mutation_types';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
new file mode 100644
index 00000000000..9ecf6bf375b
--- /dev/null
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_modal_spec.js
@@ -0,0 +1,92 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlButton, GlModal } from '@gitlab/ui';
+import DeployFreezeModal from '~/deploy_freeze/components/deploy_freeze_modal.vue';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
+import createStore from '~/deploy_freeze/store';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Deploy freeze modal', () => {
+ let wrapper;
+ let store;
+ const freezePeriodsFixture = getJSONFixture('/api/freeze-periods/freeze_periods.json');
+ const timezoneDataFixture = getJSONFixture('/api/freeze-periods/timezone_data.json');
+
+ beforeEach(() => {
+ store = createStore({
+ projectId: '8',
+ timezoneData: timezoneDataFixture,
+ });
+ wrapper = shallowMount(DeployFreezeModal, {
+ attachToDocument: true,
+ stubs: {
+ GlModal,
+ },
+ localVue,
+ store,
+ });
+ });
+
+ const findModal = () => wrapper.find(GlModal);
+ const addDeployFreezeButton = () =>
+ findModal()
+ .findAll(GlButton)
+ .at(1);
+
+ const setInput = (freezeStartCron, freezeEndCron, selectedTimezone) => {
+ store.state.freezeStartCron = freezeStartCron;
+ store.state.freezeEndCron = freezeEndCron;
+ store.state.selectedTimezone = selectedTimezone;
+
+ wrapper.find('#deploy-freeze-start').trigger('input');
+ wrapper.find('#deploy-freeze-end').trigger('input');
+ wrapper.find(TimezoneDropdown).trigger('input');
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('Basic interactions', () => {
+ it('button is disabled when freeze period is invalid', () => {
+ expect(addDeployFreezeButton().attributes('disabled')).toBeTruthy();
+ });
+ });
+
+ describe('Adding a new deploy freeze', () => {
+ beforeEach(() => {
+ const { freeze_start, freeze_end, cron_timezone } = freezePeriodsFixture[0];
+ setInput(freeze_start, freeze_end, cron_timezone);
+ });
+
+ it('button is enabled when valid freeze period settings are present', () => {
+ expect(addDeployFreezeButton().attributes('disabled')).toBeUndefined();
+ });
+ });
+
+ describe('Validations', () => {
+ describe('when the cron state is invalid', () => {
+ beforeEach(() => {
+ setInput('invalid cron', 'invalid cron', 'invalid timezone');
+ });
+
+ it('disables the add deploy freeze button', () => {
+ expect(addDeployFreezeButton().attributes('disabled')).toBeTruthy();
+ });
+ });
+
+ describe('when the cron state is valid', () => {
+ beforeEach(() => {
+ const { freeze_start, freeze_end, cron_timezone } = freezePeriodsFixture[0];
+ setInput(freeze_start, freeze_end, cron_timezone);
+ });
+
+ it('does not disable the submit button', () => {
+ expect(addDeployFreezeButton().attributes('disabled')).toBeFalsy();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
new file mode 100644
index 00000000000..d40df7de7d1
--- /dev/null
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_settings_spec.js
@@ -0,0 +1,42 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import DeployFreezeSettings from '~/deploy_freeze/components/deploy_freeze_settings.vue';
+import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue';
+import DeployFreezeModal from '~/deploy_freeze/components/deploy_freeze_modal.vue';
+import createStore from '~/deploy_freeze/store';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Deploy freeze settings', () => {
+ let wrapper;
+ let store;
+ const timezoneDataFixture = getJSONFixture('/api/freeze-periods/timezone_data.json');
+
+ beforeEach(() => {
+ store = createStore({
+ projectId: '8',
+ timezoneData: timezoneDataFixture,
+ });
+ jest.spyOn(store, 'dispatch').mockImplementation();
+ wrapper = shallowMount(DeployFreezeSettings, {
+ localVue,
+ store,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('Deploy freeze table contains components', () => {
+ it('contains deploy freeze table', () => {
+ expect(wrapper.find(DeployFreezeTable).exists()).toBe(true);
+ });
+
+ it('contains deploy freeze modal', () => {
+ expect(wrapper.find(DeployFreezeModal).exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
new file mode 100644
index 00000000000..383ffa90b22
--- /dev/null
+++ b/spec/frontend/deploy_freeze/components/deploy_freeze_table_spec.js
@@ -0,0 +1,70 @@
+import Vuex from 'vuex';
+import { createLocalVue, mount } from '@vue/test-utils';
+import DeployFreezeTable from '~/deploy_freeze/components/deploy_freeze_table.vue';
+import createStore from '~/deploy_freeze/store';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Deploy freeze table', () => {
+ let wrapper;
+ let store;
+ const timezoneDataFixture = getJSONFixture('/api/freeze-periods/timezone_data.json');
+
+ const createComponent = () => {
+ store = createStore({
+ projectId: '8',
+ timezoneData: timezoneDataFixture,
+ });
+ jest.spyOn(store, 'dispatch').mockImplementation();
+ wrapper = mount(DeployFreezeTable, {
+ attachToDocument: true,
+ localVue,
+ store,
+ });
+ };
+
+ const findEmptyFreezePeriods = () => wrapper.find('[data-testid="empty-freeze-periods"]');
+ const findAddDeployFreezeButton = () => wrapper.find('[data-testid="add-deploy-freeze"]');
+ const findDeployFreezeTable = () => wrapper.find('[data-testid="deploy-freeze-table"]');
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('dispatches fetchFreezePeriods when mounted', () => {
+ expect(store.dispatch).toHaveBeenCalledWith('fetchFreezePeriods');
+ });
+
+ describe('Renders correct data', () => {
+ it('displays empty', () => {
+ expect(findEmptyFreezePeriods().exists()).toBe(true);
+ expect(findEmptyFreezePeriods().text()).toBe(
+ 'No deploy freezes exist for this project. To add one, click Add deploy freeze',
+ );
+ });
+
+ it('displays data', () => {
+ const freezePeriodsFixture = getJSONFixture('/api/freeze-periods/freeze_periods.json');
+ store.state.freezePeriods = freezePeriodsFixture;
+
+ return wrapper.vm.$nextTick(() => {
+ const tableRows = findDeployFreezeTable().findAll('tbody tr');
+ expect(tableRows.length).toBe(freezePeriodsFixture.length);
+ expect(findEmptyFreezePeriods().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('Table click actions', () => {
+ it('displays add deploy freeze button', () => {
+ expect(findAddDeployFreezeButton().exists()).toBe(true);
+ expect(findAddDeployFreezeButton().text()).toBe('Add deploy freeze');
+ });
+ });
+});
diff --git a/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
new file mode 100644
index 00000000000..644cd0b5f27
--- /dev/null
+++ b/spec/frontend/deploy_freeze/components/timezone_dropdown_spec.js
@@ -0,0 +1,98 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlDeprecatedDropdownItem, GlNewDropdown } from '@gitlab/ui';
+import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown.vue';
+import createStore from '~/deploy_freeze/store';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Deploy freeze timezone dropdown', () => {
+ let wrapper;
+ let store;
+ const timezoneDataFixture = getJSONFixture('/api/freeze-periods/timezone_data.json');
+
+ const createComponent = (searchTerm, selectedTimezone) => {
+ store = createStore({
+ projectId: '8',
+ timezoneData: timezoneDataFixture,
+ });
+ wrapper = shallowMount(TimezoneDropdown, {
+ store,
+ localVue,
+ propsData: {
+ value: selectedTimezone,
+ timezoneData: timezoneDataFixture,
+ },
+ });
+
+ wrapper.setData({ searchTerm });
+ };
+
+ const findAllDropdownItems = () => wrapper.findAll(GlDeprecatedDropdownItem);
+ const findDropdownItemByIndex = index => wrapper.findAll(GlDeprecatedDropdownItem).at(index);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('No time zones found', () => {
+ beforeEach(() => {
+ createComponent('UTC timezone');
+ });
+
+ it('renders empty results message', () => {
+ expect(findDropdownItemByIndex(0).text()).toBe('No matching results');
+ });
+ });
+
+ describe('Search term is empty', () => {
+ beforeEach(() => {
+ createComponent('');
+ });
+
+ it('renders all timezones when search term is empty', () => {
+ expect(findAllDropdownItems()).toHaveLength(timezoneDataFixture.length);
+ });
+ });
+
+ describe('Time zones found', () => {
+ beforeEach(() => {
+ createComponent('Alaska');
+ });
+
+ it('renders only the time zone searched for', () => {
+ expect(findAllDropdownItems()).toHaveLength(1);
+ expect(findDropdownItemByIndex(0).text()).toBe('[UTC -8] Alaska');
+ });
+
+ it('should not display empty results message', () => {
+ expect(wrapper.find('[data-testid="noMatchingResults"]').exists()).toBe(false);
+ });
+
+ describe('Custom events', () => {
+ it('should emit input if a time zone is clicked', () => {
+ findDropdownItemByIndex(0).vm.$emit('click');
+ expect(wrapper.emitted('input')).toEqual([
+ [
+ {
+ formattedTimezone: '[UTC -8] Alaska',
+ identifier: 'America/Juneau',
+ },
+ ],
+ ]);
+ });
+ });
+ });
+
+ describe('Selected time zone', () => {
+ beforeEach(() => {
+ createComponent('', 'Alaska');
+ });
+
+ it('renders selected time zone as dropdown label', () => {
+ expect(wrapper.find(GlNewDropdown).vm.text).toBe('Alaska');
+ });
+ });
+});
diff --git a/spec/frontend/deploy_freeze/store/actions_spec.js b/spec/frontend/deploy_freeze/store/actions_spec.js
new file mode 100644
index 00000000000..97f94cdbf5e
--- /dev/null
+++ b/spec/frontend/deploy_freeze/store/actions_spec.js
@@ -0,0 +1,123 @@
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import Api from '~/api';
+import axios from '~/lib/utils/axios_utils';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import getInitialState from '~/deploy_freeze/store/state';
+import * as actions from '~/deploy_freeze/store/actions';
+import * as types from '~/deploy_freeze/store/mutation_types';
+
+jest.mock('~/api.js');
+jest.mock('~/flash.js');
+
+describe('deploy freeze store actions', () => {
+ let mock;
+ let state;
+ const freezePeriodsFixture = getJSONFixture('/api/freeze-periods/freeze_periods.json');
+ const timezoneDataFixture = getJSONFixture('/api/freeze-periods/timezone_data.json');
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ state = getInitialState({
+ projectId: '8',
+ timezoneData: timezoneDataFixture,
+ });
+ Api.freezePeriods.mockResolvedValue({ data: freezePeriodsFixture });
+ Api.createFreezePeriod.mockResolvedValue();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('setSelectedTimezone', () => {
+ it('commits SET_SELECTED_TIMEZONE mutation', () => {
+ testAction(actions.setSelectedTimezone, {}, {}, [
+ {
+ payload: {},
+ type: types.SET_SELECTED_TIMEZONE,
+ },
+ ]);
+ });
+ });
+
+ describe('setFreezeStartCron', () => {
+ it('commits SET_FREEZE_START_CRON mutation', () => {
+ testAction(actions.setFreezeStartCron, {}, {}, [
+ {
+ type: types.SET_FREEZE_START_CRON,
+ },
+ ]);
+ });
+ });
+
+ describe('setFreezeEndCron', () => {
+ it('commits SET_FREEZE_END_CRON mutation', () => {
+ testAction(actions.setFreezeEndCron, {}, {}, [
+ {
+ type: types.SET_FREEZE_END_CRON,
+ },
+ ]);
+ });
+ });
+
+ describe('addFreezePeriod', () => {
+ it('dispatch correct actions on adding a freeze period', () => {
+ testAction(
+ actions.addFreezePeriod,
+ {},
+ state,
+ [{ type: 'RESET_MODAL' }],
+ [
+ { type: 'requestAddFreezePeriod' },
+ { type: 'receiveAddFreezePeriodSuccess' },
+ { type: 'fetchFreezePeriods' },
+ ],
+ );
+ });
+
+ it('should show flash error and set error in state on add failure', () => {
+ Api.createFreezePeriod.mockRejectedValue();
+
+ testAction(
+ actions.addFreezePeriod,
+ {},
+ state,
+ [],
+ [{ type: 'requestAddFreezePeriod' }, { type: 'receiveAddFreezePeriodError' }],
+ () => expect(createFlash).toHaveBeenCalled(),
+ );
+ });
+ });
+
+ describe('fetchFreezePeriods', () => {
+ it('dispatch correct actions on fetchFreezePeriods', () => {
+ testAction(
+ actions.fetchFreezePeriods,
+ {},
+ state,
+ [
+ { type: types.REQUEST_FREEZE_PERIODS },
+ { type: types.RECEIVE_FREEZE_PERIODS_SUCCESS, payload: freezePeriodsFixture },
+ ],
+ [],
+ );
+ });
+
+ it('should show flash error and set error in state on fetch variables failure', () => {
+ Api.freezePeriods.mockRejectedValue();
+
+ testAction(
+ actions.fetchFreezePeriods,
+ {},
+ state,
+ [{ type: types.REQUEST_FREEZE_PERIODS }],
+ [],
+ () =>
+ expect(createFlash).toHaveBeenCalledWith(
+ 'There was an error fetching the deploy freezes.',
+ ),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/deploy_freeze/store/mutations_spec.js b/spec/frontend/deploy_freeze/store/mutations_spec.js
new file mode 100644
index 00000000000..0453e037e15
--- /dev/null
+++ b/spec/frontend/deploy_freeze/store/mutations_spec.js
@@ -0,0 +1,72 @@
+import state from '~/deploy_freeze/store/state';
+import mutations from '~/deploy_freeze/store/mutations';
+import * as types from '~/deploy_freeze/store/mutation_types';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+
+describe('Deploy freeze mutations', () => {
+ let stateCopy;
+ const timezoneDataFixture = getJSONFixture('/api/freeze-periods/timezone_data.json');
+
+ beforeEach(() => {
+ stateCopy = state({
+ projectId: '8',
+ timezoneData: timezoneDataFixture,
+ });
+ });
+
+ describe('RESET_MODAL', () => {
+ it('should reset modal state', () => {
+ mutations[types.RESET_MODAL](stateCopy);
+
+ expect(stateCopy.freezeStartCron).toBe('');
+ expect(stateCopy.freezeEndCron).toBe('');
+ expect(stateCopy.selectedTimezone).toBe('');
+ expect(stateCopy.selectedTimezoneIdentifier).toBe('');
+ });
+ });
+
+ describe('RECEIVE_FREEZE_PERIODS_SUCCESS', () => {
+ it('should set freeze periods and format timezones from identifiers to names', () => {
+ const timezoneNames = ['Berlin', 'UTC', 'Eastern Time (US & Canada)'];
+ const freezePeriodsFixture = getJSONFixture('/api/freeze-periods/freeze_periods.json');
+
+ mutations[types.RECEIVE_FREEZE_PERIODS_SUCCESS](stateCopy, freezePeriodsFixture);
+
+ const expectedFreezePeriods = freezePeriodsFixture.map((freezePeriod, index) => ({
+ ...convertObjectPropsToCamelCase(freezePeriod),
+ cronTimezone: timezoneNames[index],
+ }));
+
+ expect(stateCopy.freezePeriods).toMatchObject(expectedFreezePeriods);
+ });
+ });
+
+ describe('SET_SELECTED_TIMEZONE', () => {
+ it('should set the cron timezone', () => {
+ const timezone = {
+ formattedTimezone: '[UTC -7] Pacific Time (US & Canada)',
+ identifier: 'America/Los_Angeles',
+ };
+ mutations[types.SET_SELECTED_TIMEZONE](stateCopy, timezone);
+
+ expect(stateCopy.selectedTimezone).toEqual(timezone.formattedTimezone);
+ expect(stateCopy.selectedTimezoneIdentifier).toEqual(timezone.identifier);
+ });
+ });
+
+ describe('SET_FREEZE_START_CRON', () => {
+ it('should set freezeStartCron', () => {
+ mutations[types.SET_FREEZE_START_CRON](stateCopy, '5 0 * 8 *');
+
+ expect(stateCopy.freezeStartCron).toBe('5 0 * 8 *');
+ });
+ });
+
+ describe('SET_FREEZE_ENDT_CRON', () => {
+ it('should set freezeEndCron', () => {
+ mutations[types.SET_FREEZE_END_CRON](stateCopy, '5 0 * 8 *');
+
+ expect(stateCopy.freezeEndCron).toBe('5 0 * 8 *');
+ });
+ });
+});
diff --git a/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap b/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap
index 4c848256e5b..62a0f675cff 100644
--- a/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap
+++ b/spec/frontend/design_management/components/__snapshots__/design_note_pin_spec.js.snap
@@ -3,13 +3,13 @@
exports[`Design note pin component should match the snapshot of note when repositioning 1`] = `
<button
aria-label="Comment form position"
- class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center btn-transparent comment-indicator"
+ class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 btn-transparent comment-indicator"
style="left: 10px; top: 10px; cursor: move;"
type="button"
>
- <icon-stub
+ <gl-icon-stub
name="image-comment-dark"
- size="16"
+ size="24"
/>
</button>
`;
@@ -17,7 +17,7 @@ exports[`Design note pin component should match the snapshot of note when reposi
exports[`Design note pin component should match the snapshot of note with index 1`] = `
<button
aria-label="Comment '1' position"
- class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center js-image-badge badge badge-pill"
+ class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 js-image-badge badge badge-pill"
style="left: 10px; top: 10px;"
type="button"
>
@@ -30,13 +30,13 @@ exports[`Design note pin component should match the snapshot of note with index
exports[`Design note pin component should match the snapshot of note without index 1`] = `
<button
aria-label="Comment form position"
- class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center btn-transparent comment-indicator"
+ class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 btn-transparent comment-indicator"
style="left: 10px; top: 10px;"
type="button"
>
- <icon-stub
+ <gl-icon-stub
name="image-comment-dark"
- size="16"
+ size="24"
/>
</button>
`;
diff --git a/spec/frontend/design_management/components/delete_button_spec.js b/spec/frontend/design_management/components/delete_button_spec.js
index 9d3bcd98e44..cd4ef1f0ccd 100644
--- a/spec/frontend/design_management/components/delete_button_spec.js
+++ b/spec/frontend/design_management/components/delete_button_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
import BatchDeleteButton from '~/design_management/components/delete_button.vue';
describe('Batch delete button component', () => {
let wrapper;
- const findButton = () => wrapper.find(GlDeprecatedButton);
+ const findButton = () => wrapper.find(GlButton);
const findModal = () => wrapper.find(GlModal);
function createComponent(isDeleting = false) {
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 102e8e0664c..176c10ea584 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
@@ -61,6 +61,10 @@ describe('Design discussions component', () => {
...data,
};
},
+ provide: {
+ projectPath: 'project-path',
+ issueIid: '1',
+ },
mocks: {
$apollo,
$route: {
diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
index 9cd427f6aae..d76b6e712fe 100644
--- a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
+++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
@@ -8,328 +8,9 @@ exports[`Design management list item component when item appears in view after i
/>
`;
-exports[`Design management list item component with no notes renders item with correct status icon for creation event 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <div
- class="design-event position-absolute"
- >
- <span
- aria-label="Added in this version"
- title="Added in this version"
- >
- <icon-stub
- class="text-success-500"
- name="file-addition-solid"
- size="18"
- />
- </span>
- </div>
-
- <gl-intersection-observer-stub
- options="[object Object]"
- >
- <!---->
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <!---->
- </div>
-</router-link-stub>
-`;
-
-exports[`Design management list item component with no notes renders item with correct status icon for deletion event 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <div
- class="design-event position-absolute"
- >
- <span
- aria-label="Deleted in this version"
- title="Deleted in this version"
- >
- <icon-stub
- class="text-danger-500"
- name="file-deletion-solid"
- size="18"
- />
- </span>
- </div>
-
- <gl-intersection-observer-stub
- options="[object Object]"
- >
- <!---->
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <!---->
- </div>
-</router-link-stub>
-`;
-
-exports[`Design management list item component with no notes renders item with correct status icon for modification event 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <div
- class="design-event position-absolute"
- >
- <span
- aria-label="Modified in this version"
- title="Modified in this version"
- >
- <icon-stub
- class="text-primary-500"
- name="file-modified-solid"
- size="18"
- />
- </span>
- </div>
-
- <gl-intersection-observer-stub
- options="[object Object]"
- >
- <!---->
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <!---->
- </div>
-</router-link-stub>
-`;
-
-exports[`Design management list item component with no notes renders item with no status icon for none event 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <!---->
-
- <gl-intersection-observer-stub
- options="[object Object]"
- >
- <!---->
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <!---->
- </div>
-</router-link-stub>
-`;
-
-exports[`Design management list item component with no notes renders loading spinner when isUploading is true 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <!---->
-
- <gl-intersection-observer-stub
- options="[object Object]"
- >
- <gl-loading-icon-stub
- color="orange"
- label="Loading"
- size="md"
- />
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- style="display: none;"
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <!---->
- </div>
-</router-link-stub>
-`;
-
exports[`Design management list item component with notes renders item with multiple comments 1`] = `
<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item"
+ class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
to="[object Object]"
>
<div
@@ -337,9 +18,7 @@ exports[`Design management list item component with notes renders item with mult
>
<!---->
- <gl-intersection-observer-stub
- options="[object Object]"
- >
+ <gl-intersection-observer-stub>
<!---->
<img
@@ -401,7 +80,7 @@ exports[`Design management list item component with notes renders item with mult
exports[`Design management list item component with notes renders item with single comment 1`] = `
<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item"
+ class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
to="[object Object]"
>
<div
@@ -409,9 +88,7 @@ exports[`Design management list item component with notes renders item with sing
>
<!---->
- <gl-intersection-observer-stub
- options="[object Object]"
- >
+ <gl-intersection-observer-stub>
<!---->
<img
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
index 705b532454f..d1c90bd57b0 100644
--- a/spec/frontend/design_management/components/list/item_spec.js
+++ b/spec/frontend/design_management/components/list/item_spec.js
@@ -1,6 +1,7 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlIcon, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import VueRouter from 'vue-router';
+import Icon from '~/vue_shared/components/icon.vue';
import Item from '~/design_management/components/list/item.vue';
const localVue = createLocalVue();
@@ -18,6 +19,10 @@ const DESIGN_VERSION_EVENT = {
describe('Design management list item component', () => {
let wrapper;
+ const findDesignEvent = () => wrapper.find('[data-testid="designEvent"]');
+ const findEventIcon = () => findDesignEvent().find(Icon);
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
function createComponent({
notesCount = 0,
event = DESIGN_VERSION_EVENT.NO_CHANGE,
@@ -134,35 +139,31 @@ describe('Design management list item component', () => {
});
});
- describe('with no notes', () => {
- it('renders item with no status icon for none event', () => {
- createComponent();
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders item with correct status icon for modification event', () => {
- createComponent({ event: DESIGN_VERSION_EVENT.MODIFICATION });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders item with correct status icon for deletion event', () => {
- createComponent({ event: DESIGN_VERSION_EVENT.DELETION });
+ it('renders loading spinner when isUploading is true', () => {
+ createComponent({ isUploading: true });
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
- it('renders item with correct status icon for creation event', () => {
- createComponent({ event: DESIGN_VERSION_EVENT.CREATION });
+ it('renders item with no status icon for none event', () => {
+ createComponent();
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders loading spinner when isUploading is true', () => {
- createComponent({ isUploading: true });
+ expect(findDesignEvent().exists()).toBe(false);
+ });
- expect(wrapper.element).toMatchSnapshot();
+ describe('with associated event', () => {
+ it.each`
+ event | icon | className
+ ${DESIGN_VERSION_EVENT.MODIFICATION} | ${'file-modified-solid'} | ${'text-primary-500'}
+ ${DESIGN_VERSION_EVENT.DELETION} | ${'file-deletion-solid'} | ${'text-danger-500'}
+ ${DESIGN_VERSION_EVENT.CREATION} | ${'file-addition-solid'} | ${'text-success-500'}
+ `('renders item with correct status icon for $event event', ({ event, icon, className }) => {
+ createComponent({ event });
+ const eventIcon = findEventIcon();
+
+ expect(eventIcon.exists()).toBe(true);
+ expect(eventIcon.props('name')).toBe(icon);
+ expect(eventIcon.classes()).toContain(className);
});
});
});
diff --git a/spec/frontend/design_management_new/components/toolbar/__snapshots__/pagination_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap
index 0197b4bff79..a7d6145285c 100644
--- a/spec/frontend/design_management_new/components/toolbar/__snapshots__/pagination_spec.js.snap
+++ b/spec/frontend/design_management/components/toolbar/__snapshots__/design_navigation_spec.js.snap
@@ -2,28 +2,34 @@
exports[`Design management pagination component hides components when designs are empty 1`] = `<!---->`;
-exports[`Design management pagination component renders pagination buttons 1`] = `
+exports[`Design management pagination component renders navigation buttons 1`] = `
<div
class="d-flex align-items-center"
>
0 of 2
- <div
- class="btn-group ml-3 mr-3"
+ <gl-button-group-stub
+ class="ml-3 mr-3"
>
- <pagination-button-stub
+ <gl-button-stub
+ category="primary"
class="js-previous-design"
- iconname="angle-left"
+ disabled="true"
+ icon="angle-left"
+ size="medium"
title="Go to previous design"
+ variant="default"
/>
- <pagination-button-stub
+ <gl-button-stub
+ category="primary"
class="js-next-design"
- design="[object Object]"
- iconname="angle-right"
+ icon="angle-right"
+ size="medium"
title="Go to next design"
+ variant="default"
/>
- </div>
+ </gl-button-group-stub>
</div>
`;
diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
index e55cff8de3d..b286a74ebb8 100644
--- a/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/components/toolbar/__snapshots__/index_spec.js.snap
@@ -2,60 +2,60 @@
exports[`Design management toolbar component renders design and updated data 1`] = `
<header
- class="d-flex p-2 bg-white align-items-center js-design-header"
+ class="gl-display-flex gl-align-items-center gl-justify-content-space-between gl-bg-white gl-py-4 gl-pl-4 js-design-header"
>
- <a
- aria-label="Go back to designs"
- class="mr-3 text-plain d-flex justify-content-center align-items-center"
- >
- <icon-stub
- name="close"
- size="18"
- />
- </a>
-
<div
- class="overflow-hidden d-flex align-items-center"
+ class="gl-display-flex gl-align-items-center"
>
- <h2
- class="m-0 str-truncated-100 gl-font-base"
+ <a
+ aria-label="Go back to designs"
+ class="gl-mr-5 gl-display-flex gl-align-items-center gl-justify-content-center text-plain"
+ data-testid="close-design"
>
- test.jpg
- </h2>
+ <gl-icon-stub
+ name="close"
+ size="16"
+ />
+ </a>
- <small
- class="text-secondary"
+ <div
+ class="overflow-hidden d-flex align-items-center"
>
- Updated 1 hour ago by Test Name
- </small>
+ <h2
+ class="m-0 str-truncated-100 gl-font-base"
+ >
+ test.jpg
+ </h2>
+
+ <small
+ class="text-secondary"
+ >
+ Updated 1 hour ago by Test Name
+ </small>
+ </div>
</div>
- <pagination-stub
+ <design-navigation-stub
class="ml-auto flex-shrink-0"
id="1"
/>
- <gl-deprecated-button-stub
- class="mr-2"
+ <gl-button-stub
+ category="primary"
href="/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d"
- size="md"
- variant="secondary"
- >
- <icon-stub
- name="download"
- size="18"
- />
- </gl-deprecated-button-stub>
+ icon="download"
+ size="medium"
+ variant="default"
+ />
<delete-button-stub
+ buttoncategory="secondary"
buttonclass=""
- buttonvariant="danger"
+ buttonicon="archive"
+ buttonsize="medium"
+ buttonvariant="warning"
+ class="gl-ml-3"
hasselecteddesigns="true"
- >
- <icon-stub
- name="remove"
- size="18"
- />
- </delete-button-stub>
+ />
</header>
`;
diff --git a/spec/frontend/design_management_new/components/toolbar/pagination_spec.js b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
index 45dce15e292..1c6588a9628 100644
--- a/spec/frontend/design_management_new/components/toolbar/pagination_spec.js
+++ b/spec/frontend/design_management/components/toolbar/design_navigation_spec.js
@@ -1,8 +1,8 @@
/* global Mousetrap */
import 'mousetrap';
import { shallowMount } from '@vue/test-utils';
-import Pagination from '~/design_management_new/components/toolbar/pagination.vue';
-import { DESIGN_ROUTE_NAME } from '~/design_management_new/router/constants';
+import DesignNavigation from '~/design_management/components/toolbar/design_navigation.vue';
+import { DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
const push = jest.fn();
const $router = {
@@ -18,7 +18,7 @@ describe('Design management pagination component', () => {
let wrapper;
function createComponent() {
- wrapper = shallowMount(Pagination, {
+ wrapper = shallowMount(DesignNavigation, {
propsData: {
id: '2',
},
@@ -41,7 +41,7 @@ describe('Design management pagination component', () => {
expect(wrapper.element).toMatchSnapshot();
});
- it('renders pagination buttons', () => {
+ it('renders navigation buttons', () => {
wrapper.setData({
designs: [{ id: '1' }, { id: '2' }],
});
diff --git a/spec/frontend/design_management/components/toolbar/index_spec.js b/spec/frontend/design_management/components/toolbar/index_spec.js
index 2910b2f62ba..2914365b0df 100644
--- a/spec/frontend/design_management/components/toolbar/index_spec.js
+++ b/spec/frontend/design_management/components/toolbar/index_spec.js
@@ -1,9 +1,9 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueRouter from 'vue-router';
+import { GlButton } from '@gitlab/ui';
import Toolbar from '~/design_management/components/toolbar/index.vue';
import DeleteButton from '~/design_management/components/delete_button.vue';
import { DESIGNS_ROUTE_NAME } from '~/design_management/router/constants';
-import { GlDeprecatedButton } from '@gitlab/ui';
const localVue = createLocalVue();
localVue.use(VueRouter);
@@ -116,7 +116,7 @@ describe('Design management toolbar component', () => {
});
it('renders download button with correct link', () => {
- expect(wrapper.find(GlDeprecatedButton).attributes('href')).toBe(
+ expect(wrapper.find(GlButton).attributes('href')).toBe(
'/-/designs/306/7f747adcd4693afadbe968d7ba7d983349b9012d',
);
});
diff --git a/spec/frontend/design_management/components/toolbar/pagination_button_spec.js b/spec/frontend/design_management/components/toolbar/pagination_button_spec.js
deleted file mode 100644
index b7df201795b..00000000000
--- a/spec/frontend/design_management/components/toolbar/pagination_button_spec.js
+++ /dev/null
@@ -1,61 +0,0 @@
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import VueRouter from 'vue-router';
-import PaginationButton from '~/design_management/components/toolbar/pagination_button.vue';
-import { DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
-
-const localVue = createLocalVue();
-localVue.use(VueRouter);
-const router = new VueRouter();
-
-describe('Design management pagination button component', () => {
- let wrapper;
-
- function createComponent(design = null) {
- wrapper = shallowMount(PaginationButton, {
- localVue,
- router,
- propsData: {
- design,
- title: 'Test title',
- iconName: 'angle-right',
- },
- stubs: ['router-link'],
- });
- }
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('disables button when no design is passed', () => {
- createComponent();
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders router-link', () => {
- createComponent({ id: '2' });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- describe('designLink', () => {
- it('returns empty link when design is null', () => {
- createComponent();
-
- expect(wrapper.vm.designLink).toEqual({});
- });
-
- it('returns design link', () => {
- createComponent({ id: '2', filename: 'test' });
-
- wrapper.vm.$router.replace('/root/test-project/issues/1/designs/test?version=1');
-
- expect(wrapper.vm.designLink).toEqual({
- name: DESIGN_ROUTE_NAME,
- params: { id: 'test' },
- query: { version: '1' },
- });
- });
- });
-});
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
index 27c0ba589e6..3d7939df28e 100644
--- a/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
+++ b/spec/frontend/design_management/components/upload/__snapshots__/button_spec.js.snap
@@ -4,16 +4,18 @@ exports[`Design management upload button component renders inverted upload desig
<div
isinverted="true"
>
- <gl-deprecated-button-stub
- size="md"
+ <gl-button-stub
+ category="primary"
+ icon=""
+ size="small"
title="Adding a design with the same filename replaces the file in a new version."
- variant="success"
+ variant="default"
>
Upload designs
<!---->
- </gl-deprecated-button-stub>
+ </gl-button-stub>
<input
accept="image/*"
@@ -27,11 +29,13 @@ exports[`Design management upload button component renders inverted upload desig
exports[`Design management upload button component renders loading icon 1`] = `
<div>
- <gl-deprecated-button-stub
+ <gl-button-stub
+ category="primary"
disabled="true"
- size="md"
+ icon=""
+ size="small"
title="Adding a design with the same filename replaces the file in a new version."
- variant="success"
+ variant="default"
>
Upload designs
@@ -43,7 +47,7 @@ exports[`Design management upload button component renders loading icon 1`] = `
label="Loading"
size="sm"
/>
- </gl-deprecated-button-stub>
+ </gl-button-stub>
<input
accept="image/*"
@@ -57,16 +61,18 @@ exports[`Design management upload button component renders loading icon 1`] = `
exports[`Design management upload button component renders upload design button 1`] = `
<div>
- <gl-deprecated-button-stub
- size="md"
+ <gl-button-stub
+ category="primary"
+ icon=""
+ size="small"
title="Adding a design with the same filename replaces the file in a new version."
- variant="success"
+ variant="default"
>
Upload designs
<!---->
- </gl-deprecated-button-stub>
+ </gl-button-stub>
<input
accept="image/*"
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/design_dropzone_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/design_dropzone_spec.js.snap
index 0737b9729a2..9284099b40d 100644
--- a/spec/frontend/design_management/components/upload/__snapshots__/design_dropzone_spec.js.snap
+++ b/spec/frontend/design_management/components/upload/__snapshots__/design_dropzone_spec.js.snap
@@ -5,20 +5,23 @@ exports[`Design management dropzone component when dragging renders correct temp
class="w-100 position-relative"
>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
>
<div
- class="d-flex-center flex-column text-center"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ data-testid="dropzone-area"
>
<gl-icon-stub
- class="mb-4"
- name="doc-new"
- size="48"
+ class="gl-mb-2"
+ name="upload"
+ size="24"
/>
- <p>
+ <p
+ class="gl-mb-0"
+ >
<gl-sprintf-stub
- message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
+ message="Drop or %{linkStart}upload%{linkEnd} designs to attach"
/>
</p>
</div>
@@ -43,7 +46,9 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style="display: none;"
>
- <h3>
+ <h3
+ class=""
+ >
Oh no!
</h3>
@@ -56,7 +61,9 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style=""
>
- <h3>
+ <h3
+ class=""
+ >
Incoming!
</h3>
@@ -74,20 +81,23 @@ exports[`Design management dropzone component when dragging renders correct temp
class="w-100 position-relative"
>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
>
<div
- class="d-flex-center flex-column text-center"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ data-testid="dropzone-area"
>
<gl-icon-stub
- class="mb-4"
- name="doc-new"
- size="48"
+ class="gl-mb-2"
+ name="upload"
+ size="24"
/>
- <p>
+ <p
+ class="gl-mb-0"
+ >
<gl-sprintf-stub
- message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
+ message="Drop or %{linkStart}upload%{linkEnd} designs to attach"
/>
</p>
</div>
@@ -112,7 +122,9 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style="display: none;"
>
- <h3>
+ <h3
+ class=""
+ >
Oh no!
</h3>
@@ -125,7 +137,9 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style=""
>
- <h3>
+ <h3
+ class=""
+ >
Incoming!
</h3>
@@ -143,20 +157,23 @@ exports[`Design management dropzone component when dragging renders correct temp
class="w-100 position-relative"
>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
>
<div
- class="d-flex-center flex-column text-center"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ data-testid="dropzone-area"
>
<gl-icon-stub
- class="mb-4"
- name="doc-new"
- size="48"
+ class="gl-mb-2"
+ name="upload"
+ size="24"
/>
- <p>
+ <p
+ class="gl-mb-0"
+ >
<gl-sprintf-stub
- message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
+ message="Drop or %{linkStart}upload%{linkEnd} designs to attach"
/>
</p>
</div>
@@ -180,7 +197,9 @@ exports[`Design management dropzone component when dragging renders correct temp
<div
class="mw-50 text-center"
>
- <h3>
+ <h3
+ class=""
+ >
Oh no!
</h3>
@@ -193,7 +212,9 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style="display: none;"
>
- <h3>
+ <h3
+ class=""
+ >
Incoming!
</h3>
@@ -211,20 +232,23 @@ exports[`Design management dropzone component when dragging renders correct temp
class="w-100 position-relative"
>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
>
<div
- class="d-flex-center flex-column text-center"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ data-testid="dropzone-area"
>
<gl-icon-stub
- class="mb-4"
- name="doc-new"
- size="48"
+ class="gl-mb-2"
+ name="upload"
+ size="24"
/>
- <p>
+ <p
+ class="gl-mb-0"
+ >
<gl-sprintf-stub
- message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
+ message="Drop or %{linkStart}upload%{linkEnd} designs to attach"
/>
</p>
</div>
@@ -248,7 +272,9 @@ exports[`Design management dropzone component when dragging renders correct temp
<div
class="mw-50 text-center"
>
- <h3>
+ <h3
+ class=""
+ >
Oh no!
</h3>
@@ -261,7 +287,9 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style="display: none;"
>
- <h3>
+ <h3
+ class=""
+ >
Incoming!
</h3>
@@ -279,20 +307,23 @@ exports[`Design management dropzone component when dragging renders correct temp
class="w-100 position-relative"
>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
>
<div
- class="d-flex-center flex-column text-center"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ data-testid="dropzone-area"
>
<gl-icon-stub
- class="mb-4"
- name="doc-new"
- size="48"
+ class="gl-mb-2"
+ name="upload"
+ size="24"
/>
- <p>
+ <p
+ class="gl-mb-0"
+ >
<gl-sprintf-stub
- message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
+ message="Drop or %{linkStart}upload%{linkEnd} designs to attach"
/>
</p>
</div>
@@ -316,7 +347,9 @@ exports[`Design management dropzone component when dragging renders correct temp
<div
class="mw-50 text-center"
>
- <h3>
+ <h3
+ class=""
+ >
Oh no!
</h3>
@@ -329,7 +362,9 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style="display: none;"
>
- <h3>
+ <h3
+ class=""
+ >
Incoming!
</h3>
@@ -347,20 +382,23 @@ exports[`Design management dropzone component when no slot provided renders defa
class="w-100 position-relative"
>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
>
<div
- class="d-flex-center flex-column text-center"
+ class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
+ data-testid="dropzone-area"
>
<gl-icon-stub
- class="mb-4"
- name="doc-new"
- size="48"
+ class="gl-mb-2"
+ name="upload"
+ size="24"
/>
- <p>
+ <p
+ class="gl-mb-0"
+ >
<gl-sprintf-stub
- message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
+ message="Drop or %{linkStart}upload%{linkEnd} designs to attach"
/>
</p>
</div>
@@ -384,7 +422,9 @@ exports[`Design management dropzone component when no slot provided renders defa
<div
class="mw-50 text-center"
>
- <h3>
+ <h3
+ class=""
+ >
Oh no!
</h3>
@@ -397,7 +437,9 @@ exports[`Design management dropzone component when no slot provided renders defa
class="mw-50 text-center"
style="display: none;"
>
- <h3>
+ <h3
+ class=""
+ >
Incoming!
</h3>
@@ -428,7 +470,9 @@ exports[`Design management dropzone component when slot provided renders dropzon
<div
class="mw-50 text-center"
>
- <h3>
+ <h3
+ class=""
+ >
Oh no!
</h3>
@@ -441,7 +485,9 @@ exports[`Design management dropzone component when slot provided renders dropzon
class="mw-50 text-center"
style="display: none;"
>
- <h3>
+ <h3
+ class=""
+ >
Incoming!
</h3>
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
index 00f1a40dfb2..d6fd09eb698 100644
--- a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
+++ b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
@@ -1,111 +1,77 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management design version dropdown component renders design version dropdown button 1`] = `
-<gl-dropdown-stub
- class="design-version-dropdown"
+<gl-new-dropdown-stub
+ category="tertiary"
+ headertext=""
issueiid=""
projectpath=""
- text="Showing Latest Version"
- variant="link"
+ size="small"
+ text="Showing latest version"
+ variant="default"
>
- <gl-dropdown-item-stub>
- <router-link-stub
- class="d-flex js-version-link"
- to="[object Object]"
- >
- <div
- class="flex-grow-1 ml-2"
- >
- <div>
- <strong>
- Version 2
-
- <span>
- (latest)
- </span>
- </strong>
- </div>
- </div>
-
- <i
- class="fa fa-check pull-right"
- />
- </router-link-stub>
- </gl-dropdown-item-stub>
- <gl-dropdown-item-stub>
- <router-link-stub
- class="d-flex js-version-link"
- to="[object Object]"
- >
- <div
- class="flex-grow-1 ml-2"
- >
- <div>
- <strong>
- Version 1
-
- <!---->
- </strong>
- </div>
- </div>
-
- <!---->
- </router-link-stub>
- </gl-dropdown-item-stub>
-</gl-dropdown-stub>
+ <gl-new-dropdown-item-stub
+ avatarurl=""
+ iconcolor=""
+ iconname=""
+ iconrightname=""
+ ischecked="true"
+ ischeckitem="true"
+ secondarytext=""
+ >
+ Version
+ 2
+ (latest)
+ </gl-new-dropdown-item-stub>
+ <gl-new-dropdown-item-stub
+ avatarurl=""
+ iconcolor=""
+ iconname=""
+ iconrightname=""
+ ischeckitem="true"
+ secondarytext=""
+ >
+ Version
+ 1
+
+ </gl-new-dropdown-item-stub>
+</gl-new-dropdown-stub>
`;
exports[`Design management design version dropdown component renders design version list 1`] = `
-<gl-dropdown-stub
- class="design-version-dropdown"
+<gl-new-dropdown-stub
+ category="tertiary"
+ headertext=""
issueiid=""
projectpath=""
- text="Showing Latest Version"
- variant="link"
+ size="small"
+ text="Showing latest version"
+ variant="default"
>
- <gl-dropdown-item-stub>
- <router-link-stub
- class="d-flex js-version-link"
- to="[object Object]"
- >
- <div
- class="flex-grow-1 ml-2"
- >
- <div>
- <strong>
- Version 2
-
- <span>
- (latest)
- </span>
- </strong>
- </div>
- </div>
-
- <i
- class="fa fa-check pull-right"
- />
- </router-link-stub>
- </gl-dropdown-item-stub>
- <gl-dropdown-item-stub>
- <router-link-stub
- class="d-flex js-version-link"
- to="[object Object]"
- >
- <div
- class="flex-grow-1 ml-2"
- >
- <div>
- <strong>
- Version 1
-
- <!---->
- </strong>
- </div>
- </div>
-
- <!---->
- </router-link-stub>
- </gl-dropdown-item-stub>
-</gl-dropdown-stub>
+ <gl-new-dropdown-item-stub
+ avatarurl=""
+ iconcolor=""
+ iconname=""
+ iconrightname=""
+ ischecked="true"
+ ischeckitem="true"
+ secondarytext=""
+ >
+ Version
+ 2
+ (latest)
+ </gl-new-dropdown-item-stub>
+ <gl-new-dropdown-item-stub
+ avatarurl=""
+ iconcolor=""
+ iconname=""
+ iconrightname=""
+ ischeckitem="true"
+ secondarytext=""
+ >
+ Version
+ 1
+
+ </gl-new-dropdown-item-stub>
+</gl-new-dropdown-stub>
`;
diff --git a/spec/frontend/design_management/components/upload/design_dropzone_spec.js b/spec/frontend/design_management/components/upload/design_dropzone_spec.js
index 9b86b5b2878..bf97399368f 100644
--- a/spec/frontend/design_management/components/upload/design_dropzone_spec.js
+++ b/spec/frontend/design_management/components/upload/design_dropzone_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
import DesignDropzone from '~/design_management/components/upload/design_dropzone.vue';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
@@ -12,10 +13,16 @@ describe('Design management dropzone component', () => {
};
const findDropzoneCard = () => wrapper.find('.design-dropzone-card');
+ const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]');
+ const findIcon = () => wrapper.find(GlIcon);
- function createComponent({ slots = {}, data = {} } = {}) {
+ function createComponent({ slots = {}, data = {}, props = {} } = {}) {
wrapper = shallowMount(DesignDropzone, {
slots,
+ propsData: {
+ hasDesigns: true,
+ ...props,
+ },
data() {
return data;
},
@@ -129,4 +136,18 @@ describe('Design management dropzone component', () => {
});
});
});
+
+ it('applies correct classes when there are no designs or no design saving loader', () => {
+ createComponent({ props: { hasDesigns: false } });
+ expect(findDropzoneArea().classes()).not.toContain('gl-flex-direction-column');
+ expect(findIcon().classes()).toEqual(['gl-mr-3', 'gl-text-gray-500']);
+ expect(findIcon().props('size')).toBe(16);
+ });
+
+ it('applies correct classes when there are designs or design saving loader', () => {
+ createComponent({ props: { hasDesigns: true } });
+ expect(findDropzoneArea().classes()).toContain('gl-flex-direction-column');
+ expect(findIcon().classes()).toEqual(['gl-mb-2']);
+ expect(findIcon().props('size')).toBe(24);
+ });
});
diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
index 7521b9fad2a..f4206cdaeb3 100644
--- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
+++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
+import { GlNewDropdown, GlNewDropdownItem, GlSprintf } from '@gitlab/ui';
import DesignVersionDropdown from '~/design_management/components/upload/design_version_dropdown.vue';
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import mockAllVersions from './mock_data/all_versions';
const LATEST_VERSION_ID = 3;
@@ -30,7 +30,7 @@ describe('Design management design version dropdown component', () => {
mocks: {
$route,
},
- stubs: ['router-link'],
+ stubs: { GlSprintf },
});
wrapper.setData({
@@ -42,7 +42,7 @@ describe('Design management design version dropdown component', () => {
wrapper.destroy();
});
- const findVersionLink = index => wrapper.findAll('.js-version-link').at(index);
+ const findVersionLink = index => wrapper.findAll(GlNewDropdownItem).at(index);
it('renders design version dropdown button', () => {
createComponent();
@@ -75,7 +75,7 @@ describe('Design management design version dropdown component', () => {
createComponent();
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing Latest Version');
+ expect(wrapper.find(GlNewDropdown).attributes('text')).toBe('Showing latest version');
});
});
@@ -83,7 +83,7 @@ describe('Design management design version dropdown component', () => {
createComponent({ maxVersions: 1 });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing Latest Version');
+ expect(wrapper.find(GlNewDropdown).attributes('text')).toBe('Showing latest version');
});
});
@@ -91,7 +91,7 @@ describe('Design management design version dropdown component', () => {
createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDropdown).attributes('text')).toBe(`Showing Version #1`);
+ expect(wrapper.find(GlNewDropdown).attributes('text')).toBe(`Showing version #1`);
});
});
@@ -99,7 +99,7 @@ describe('Design management design version dropdown component', () => {
createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlDropdown).attributes('text')).toBe('Showing Latest Version');
+ expect(wrapper.find(GlNewDropdown).attributes('text')).toBe('Showing latest version');
});
});
@@ -107,7 +107,7 @@ describe('Design management design version dropdown component', () => {
createComponent();
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.findAll(GlDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
+ expect(wrapper.findAll(GlNewDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
});
});
});
diff --git a/spec/frontend/design_management/components/upload/mock_data/all_versions.js b/spec/frontend/design_management/components/upload/mock_data/all_versions.js
index e76bbd261bd..237e1654f9b 100644
--- a/spec/frontend/design_management/components/upload/mock_data/all_versions.js
+++ b/spec/frontend/design_management/components/upload/mock_data/all_versions.js
@@ -1,14 +1,10 @@
export default [
{
- node: {
- id: 'gid://gitlab/DesignManagement::Version/3',
- sha: '0945756378e0b1588b9dd40d5a6b99e8b7198f55',
- },
+ id: 'gid://gitlab/DesignManagement::Version/3',
+ sha: '0945756378e0b1588b9dd40d5a6b99e8b7198f55',
},
{
- node: {
- id: 'gid://gitlab/DesignManagement::Version/2',
- sha: '5b063fef0cd7213b312db65b30e24f057df21b20',
- },
+ id: 'gid://gitlab/DesignManagement::Version/2',
+ sha: '5b063fef0cd7213b312db65b30e24f057df21b20',
},
];
diff --git a/spec/frontend/design_management/mock_data/all_versions.js b/spec/frontend/design_management/mock_data/all_versions.js
index c389fdb8747..2b216574e27 100644
--- a/spec/frontend/design_management/mock_data/all_versions.js
+++ b/spec/frontend/design_management/mock_data/all_versions.js
@@ -1,8 +1,6 @@
export default [
{
- node: {
- id: 'gid://gitlab/DesignManagement::Version/1',
- sha: 'b389071a06c153509e11da1f582005b316667001',
- },
+ id: 'gid://gitlab/DesignManagement::Version/1',
+ sha: 'b389071a06c153509e11da1f582005b316667001',
},
];
diff --git a/spec/frontend/design_management/mock_data/apollo_mock.js b/spec/frontend/design_management/mock_data/apollo_mock.js
new file mode 100644
index 00000000000..5e2df3877a5
--- /dev/null
+++ b/spec/frontend/design_management/mock_data/apollo_mock.js
@@ -0,0 +1,106 @@
+export const designListQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ issue: {
+ designCollection: {
+ designs: {
+ nodes: [
+ {
+ id: '1',
+ event: 'NONE',
+ filename: 'fox_1.jpg',
+ notesCount: 3,
+ image: 'image-1',
+ imageV432x230: 'image-1',
+ },
+ {
+ id: '2',
+ event: 'NONE',
+ filename: 'fox_2.jpg',
+ notesCount: 2,
+ image: 'image-2',
+ imageV432x230: 'image-2',
+ },
+ {
+ id: '3',
+ event: 'NONE',
+ filename: 'fox_3.jpg',
+ notesCount: 1,
+ image: 'image-3',
+ imageV432x230: 'image-3',
+ },
+ ],
+ },
+ versions: {
+ nodes: [],
+ },
+ },
+ },
+ },
+ },
+};
+
+export const permissionsQueryResponse = {
+ data: {
+ project: {
+ id: '1',
+ issue: {
+ userPermissions: { createDesign: true },
+ },
+ },
+ },
+};
+
+export const reorderedDesigns = [
+ {
+ id: '2',
+ event: 'NONE',
+ filename: 'fox_2.jpg',
+ notesCount: 2,
+ image: 'image-2',
+ imageV432x230: 'image-2',
+ },
+ {
+ id: '1',
+ event: 'NONE',
+ filename: 'fox_1.jpg',
+ notesCount: 3,
+ image: 'image-1',
+ imageV432x230: 'image-1',
+ },
+ {
+ id: '3',
+ event: 'NONE',
+ filename: 'fox_3.jpg',
+ notesCount: 1,
+ image: 'image-3',
+ imageV432x230: 'image-3',
+ },
+];
+
+export const moveDesignMutationResponse = {
+ data: {
+ designManagementMove: {
+ designCollection: {
+ designs: {
+ nodes: [...reorderedDesigns],
+ },
+ },
+ errors: [],
+ },
+ },
+};
+
+export const moveDesignMutationResponseWithErrors = {
+ data: {
+ designManagementMove: {
+ designCollection: {
+ designs: {
+ nodes: [...reorderedDesigns],
+ },
+ },
+ errors: ['Houston, we have a problem'],
+ },
+ },
+};
diff --git a/spec/frontend/design_management/mock_data/design.js b/spec/frontend/design_management/mock_data/design.js
index 675198b9408..72be33fef1d 100644
--- a/spec/frontend/design_management/mock_data/design.js
+++ b/spec/frontend/design_management/mock_data/design.js
@@ -12,14 +12,12 @@ export default {
webPath: 'full-issue-path',
webUrl: 'full-issue-url',
participants: {
- edges: [
+ nodes: [
{
- node: {
- name: 'Administrator',
- username: 'root',
- webUrl: 'link-to-author',
- avatarUrl: 'link-to-avatar',
- },
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'link-to-author',
+ avatarUrl: 'link-to-avatar',
},
],
},
diff --git a/spec/frontend/design_management/mock_data/designs.js b/spec/frontend/design_management/mock_data/designs.js
index 07f5c1b7457..98a24081ae6 100644
--- a/spec/frontend/design_management/mock_data/designs.js
+++ b/spec/frontend/design_management/mock_data/designs.js
@@ -5,11 +5,7 @@ export default {
issue: {
designCollection: {
designs: {
- edges: [
- {
- node: design,
- },
- ],
+ nodes: [design],
},
},
},
diff --git a/spec/frontend/design_management/mock_data/no_designs.js b/spec/frontend/design_management/mock_data/no_designs.js
index 9db0ffcade2..0ccb83492fc 100644
--- a/spec/frontend/design_management/mock_data/no_designs.js
+++ b/spec/frontend/design_management/mock_data/no_designs.js
@@ -3,7 +3,7 @@ export default {
issue: {
designCollection: {
designs: {
- edges: [],
+ nodes: [],
},
},
},
diff --git a/spec/frontend/design_management/mock_data/versions_list.js b/spec/frontend/design_management/mock_data/versions_list.js
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/spec/frontend/design_management/mock_data/versions_list.js
diff --git a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
index 3ba63fd14f0..3881b2d7679 100644
--- a/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/__snapshots__/index_spec.js.snap
@@ -1,7 +1,10 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management index page designs does not render toolbar when there is no permission 1`] = `
-<div>
+<div
+ class="gl-mt-5"
+ data-testid="designs-root"
+>
<!---->
<div
@@ -11,18 +14,22 @@ exports[`Design management index page designs does not render toolbar when there
class="list-unstyled row"
>
<li
- class="col-md-6 col-lg-4 mb-3"
+ class="gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3"
+ data-testid="design-dropzone-wrapper"
>
<design-dropzone-stub
- class="design-list-item"
+ class="design-list-item design-list-item-new"
+ hasdesigns="true"
/>
</li>
-
<li
- class="col-md-6 col-lg-4 mb-3"
+ class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
>
- <design-dropzone-stub>
+ <design-dropzone-stub
+ hasdesigns="true"
+ >
<design-stub
+ class="gl-bg-white"
event="NONE"
filename="design-1-name"
id="design-1"
@@ -34,10 +41,13 @@ exports[`Design management index page designs does not render toolbar when there
<!---->
</li>
<li
- class="col-md-6 col-lg-4 mb-3"
+ class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
>
- <design-dropzone-stub>
+ <design-dropzone-stub
+ hasdesigns="true"
+ >
<design-stub
+ class="gl-bg-white"
event="NONE"
filename="design-2-name"
id="design-2"
@@ -49,10 +59,13 @@ exports[`Design management index page designs does not render toolbar when there
<!---->
</li>
<li
- class="col-md-6 col-lg-4 mb-3"
+ class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
>
- <design-dropzone-stub>
+ <design-dropzone-stub
+ hasdesigns="true"
+ >
<design-stub
+ class="gl-bg-white"
event="NONE"
filename="design-3-name"
id="design-3"
@@ -73,35 +86,50 @@ exports[`Design management index page designs does not render toolbar when there
`;
exports[`Design management index page designs renders designs list and header with upload button 1`] = `
-<div>
+<div
+ class="gl-mt-5"
+ data-testid="designs-root"
+>
<header
class="row-content-block border-top-0 p-2 d-flex"
>
<div
- class="d-flex justify-content-between align-items-center w-100"
+ class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full"
>
- <design-version-dropdown-stub />
+ <div>
+ <span
+ class="gl-font-weight-bold gl-mr-3"
+ >
+ Designs
+ </span>
+
+ <design-version-dropdown-stub />
+ </div>
<div
- class="qa-selector-toolbar d-flex"
+ class="qa-selector-toolbar gl-display-flex gl-align-items-center"
>
- <gl-deprecated-button-stub
- class="mr-2 js-select-all"
- size="md"
+ <gl-button-stub
+ category="primary"
+ class="gl-mr-3 js-select-all"
+ icon=""
+ size="small"
variant="link"
>
Select all
- </gl-deprecated-button-stub>
+
+ </gl-button-stub>
<div>
<delete-button-stub
- buttonclass="btn-danger btn-inverted mr-2"
- buttonvariant=""
+ buttoncategory="secondary"
+ buttonclass="gl-mr-3"
+ buttonsize="small"
+ buttonvariant="warning"
>
- Delete selected
-
- <!---->
+ Archive selected
+
</delete-button-stub>
</div>
@@ -117,18 +145,22 @@ exports[`Design management index page designs renders designs list and header wi
class="list-unstyled row"
>
<li
- class="col-md-6 col-lg-4 mb-3"
+ class="gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3"
+ data-testid="design-dropzone-wrapper"
>
<design-dropzone-stub
- class="design-list-item"
+ class="design-list-item design-list-item-new"
+ hasdesigns="true"
/>
</li>
-
<li
- class="col-md-6 col-lg-4 mb-3"
+ class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
>
- <design-dropzone-stub>
+ <design-dropzone-stub
+ hasdesigns="true"
+ >
<design-stub
+ class="gl-bg-white"
event="NONE"
filename="design-1-name"
id="design-1"
@@ -143,10 +175,13 @@ exports[`Design management index page designs renders designs list and header wi
/>
</li>
<li
- class="col-md-6 col-lg-4 mb-3"
+ class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
>
- <design-dropzone-stub>
+ <design-dropzone-stub
+ hasdesigns="true"
+ >
<design-stub
+ class="gl-bg-white"
event="NONE"
filename="design-2-name"
id="design-2"
@@ -161,10 +196,13 @@ exports[`Design management index page designs renders designs list and header wi
/>
</li>
<li
- class="col-md-6 col-lg-4 mb-3"
+ class="col-md-6 col-lg-3 gl-mb-3 gl-bg-transparent gl-shadow-none js-design-tile"
>
- <design-dropzone-stub>
+ <design-dropzone-stub
+ hasdesigns="true"
+ >
<design-stub
+ class="gl-bg-white"
event="NONE"
filename="design-3-name"
id="design-3"
@@ -188,7 +226,10 @@ exports[`Design management index page designs renders designs list and header wi
`;
exports[`Design management index page designs renders error 1`] = `
-<div>
+<div
+ class="gl-mt-5"
+ data-testid="designs-root"
+>
<!---->
<div
@@ -216,7 +257,10 @@ exports[`Design management index page designs renders error 1`] = `
`;
exports[`Design management index page designs renders loading icon 1`] = `
-<div>
+<div
+ class="gl-mt-5"
+ data-testid="designs-root"
+>
<!---->
<div
@@ -235,8 +279,11 @@ exports[`Design management index page designs renders loading icon 1`] = `
</div>
`;
-exports[`Design management index page when has no designs renders empty text 1`] = `
-<div>
+exports[`Design management index page when has no designs renders design dropzone 1`] = `
+<div
+ class="gl-mt-5"
+ data-testid="designs-root"
+>
<!---->
<div
@@ -246,13 +293,13 @@ exports[`Design management index page when has no designs renders empty text 1`]
class="list-unstyled row"
>
<li
- class="col-md-6 col-lg-4 mb-3"
+ class="col-12"
+ data-testid="design-dropzone-wrapper"
>
<design-dropzone-stub
- class="design-list-item"
+ class=""
/>
</li>
-
</ol>
</div>
diff --git a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
index 65c4811536e..823294efc38 100644
--- a/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management/pages/design/__snapshots__/index_spec.js.snap
@@ -10,7 +10,7 @@ exports[`Design management design index page renders design index 1`] = `
<design-destroyer-stub
filenames="test.jpg"
iid="1"
- projectpath=""
+ project-path="project-path"
/>
<!---->
@@ -41,7 +41,7 @@ exports[`Design management design index page renders design index 1`] = `
</h2>
<a
- class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
+ class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
href="full-issue-url"
>
ull-issue-path
@@ -60,13 +60,13 @@ exports[`Design management design index page renders design index 1`] = `
designid="test"
discussion="[object Object]"
discussionwithopenform=""
- markdownpreviewpath="//preview_markdown?target_type=Issue"
+ markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
noteableid="design-id"
/>
<gl-button-stub
- category="tertiary"
- class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-mb-4"
+ category="primary"
+ class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4"
data-testid="resolved-comments"
icon="chevron-right"
id="resolved-comments"
@@ -108,7 +108,7 @@ exports[`Design management design index page renders design index 1`] = `
designid="test"
discussion="[object Object]"
discussionwithopenform=""
- markdownpreviewpath="//preview_markdown?target_type=Issue"
+ markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
noteableid="design-id"
/>
</gl-collapse-stub>
@@ -140,7 +140,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
<design-destroyer-stub
filenames="test.jpg"
iid="1"
- projectpath=""
+ project-path="project-path"
/>
<div
@@ -188,7 +188,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
</h2>
<a
- class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
+ class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
href="full-issue-url"
>
ull-issue-path
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index 82b607eb77d..369c8667f4d 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -2,7 +2,7 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueRouter from 'vue-router';
import { GlAlert } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import DesignIndex from '~/design_management/pages/design/index.vue';
import DesignSidebar from '~/design_management/components/design_sidebar.vue';
import DesignPresentation from '~/design_management/components/design_presentation.vue';
@@ -95,9 +95,12 @@ describe('Design management design index page', () => {
DesignSidebar,
DesignReplyForm,
},
+ provide: {
+ issueIid: '1',
+ projectPath: 'project-path',
+ },
data() {
return {
- issueIid: '1',
activeDiscussion: {
id: null,
source: null,
@@ -149,7 +152,7 @@ describe('Design management design index page', () => {
expect(findSidebar().props()).toEqual({
design,
- markdownPreviewPath: '//preview_markdown?target_type=Issue',
+ markdownPreviewPath: '/project-path/preview_markdown?target_type=Issue',
resolvedDiscussionsExpanded: false,
});
});
diff --git a/spec/frontend/design_management/pages/index_apollo_spec.js b/spec/frontend/design_management/pages/index_apollo_spec.js
new file mode 100644
index 00000000000..3ea711c2cfa
--- /dev/null
+++ b/spec/frontend/design_management/pages/index_apollo_spec.js
@@ -0,0 +1,162 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { createMockClient } from 'mock-apollo-client';
+import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
+import VueDraggable from 'vuedraggable';
+import { InMemoryCache } from 'apollo-cache-inmemory';
+import Design from '~/design_management/components/list/item.vue';
+import createRouter from '~/design_management/router';
+import getDesignListQuery from '~/design_management/graphql/queries/get_design_list.query.graphql';
+import permissionsQuery from '~/design_management/graphql/queries/design_permissions.query.graphql';
+import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import Index from '~/design_management/pages/index.vue';
+import {
+ designListQueryResponse,
+ permissionsQueryResponse,
+ moveDesignMutationResponse,
+ reorderedDesigns,
+ moveDesignMutationResponseWithErrors,
+} from '../mock_data/apollo_mock';
+
+jest.mock('~/flash');
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+const router = createRouter();
+localVue.use(VueRouter);
+
+const designToMove = {
+ __typename: 'Design',
+ id: '2',
+ event: 'NONE',
+ filename: 'fox_2.jpg',
+ notesCount: 2,
+ image: 'image-2',
+ imageV432x230: 'image-2',
+};
+
+describe('Design management index page with Apollo mock', () => {
+ let wrapper;
+ let mockClient;
+ let apolloProvider;
+ let moveDesignHandler;
+
+ async function moveDesigns(localWrapper) {
+ await jest.runOnlyPendingTimers();
+ await localWrapper.vm.$nextTick();
+
+ localWrapper.find(VueDraggable).vm.$emit('input', reorderedDesigns);
+ localWrapper.find(VueDraggable).vm.$emit('change', {
+ moved: {
+ newIndex: 0,
+ element: designToMove,
+ },
+ });
+ }
+
+ const fragmentMatcher = { match: () => true };
+
+ const cache = new InMemoryCache({
+ fragmentMatcher,
+ addTypename: false,
+ });
+
+ const findDesigns = () => wrapper.findAll(Design);
+
+ function createComponent({
+ moveHandler = jest.fn().mockResolvedValue(moveDesignMutationResponse),
+ }) {
+ mockClient = createMockClient({ cache });
+
+ mockClient.setRequestHandler(
+ getDesignListQuery,
+ jest.fn().mockResolvedValue(designListQueryResponse),
+ );
+
+ mockClient.setRequestHandler(
+ permissionsQuery,
+ jest.fn().mockResolvedValue(permissionsQueryResponse),
+ );
+
+ moveDesignHandler = moveHandler;
+
+ mockClient.setRequestHandler(moveDesignMutation, moveDesignHandler);
+
+ apolloProvider = new VueApollo({
+ defaultClient: mockClient,
+ });
+
+ wrapper = shallowMount(Index, {
+ localVue,
+ apolloProvider,
+ router,
+ stubs: { VueDraggable },
+ });
+ }
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ mockClient = null;
+ apolloProvider = null;
+ });
+
+ it('has a design with id 1 as a first one', async () => {
+ createComponent({});
+
+ await jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ expect(findDesigns()).toHaveLength(3);
+ expect(
+ findDesigns()
+ .at(0)
+ .props('id'),
+ ).toBe('1');
+ });
+
+ it('calls a mutation with correct parameters and reorders designs', async () => {
+ createComponent({});
+
+ await moveDesigns(wrapper);
+
+ expect(moveDesignHandler).toHaveBeenCalled();
+
+ await wrapper.vm.$nextTick();
+
+ expect(
+ findDesigns()
+ .at(0)
+ .props('id'),
+ ).toBe('2');
+ });
+
+ it('displays flash if mutation had a recoverable error', async () => {
+ createComponent({
+ moveHandler: jest.fn().mockResolvedValue(moveDesignMutationResponseWithErrors),
+ });
+
+ await moveDesigns(wrapper);
+
+ await wrapper.vm.$nextTick();
+
+ expect(createFlash).toHaveBeenCalledWith('Houston, we have a problem');
+ });
+
+ it('displays flash if mutation had a non-recoverable error', async () => {
+ createComponent({
+ moveHandler: jest.fn().mockRejectedValue('Error'),
+ });
+
+ await moveDesigns(wrapper);
+
+ await jest.runOnlyPendingTimers();
+ await wrapper.vm.$nextTick();
+
+ expect(createFlash).toHaveBeenCalledWith(
+ 'Something went wrong when reordering designs. Please try again',
+ );
+ });
+});
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index d3761bf09e9..093fa155d2e 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -1,5 +1,6 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
+import VueDraggable from 'vuedraggable';
import VueRouter from 'vue-router';
import { GlEmptyState } from '@gitlab/ui';
import Index from '~/design_management/pages/index.vue';
@@ -12,7 +13,7 @@ import {
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
} from '~/design_management/utils/error_messages';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import createRouter from '~/design_management/router';
import * as utils from '~/design_management/utils/design_management_utils';
import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management/constants';
@@ -25,6 +26,9 @@ const mockPageEl = {
};
jest.spyOn(utils, 'getPageLayoutElement').mockReturnValue(mockPageEl);
+const scrollIntoViewMock = jest.fn();
+HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
+
const localVue = createLocalVue();
const router = createRouter();
localVue.use(VueRouter);
@@ -54,9 +58,7 @@ const mockDesigns = [
];
const mockVersion = {
- node: {
- id: 'gid://gitlab/DesignManagement::Version/1',
- },
+ id: 'gid://gitlab/DesignManagement::Version/1',
};
describe('Design management index page', () => {
@@ -68,7 +70,10 @@ describe('Design management index page', () => {
const findToolbar = () => wrapper.find('.qa-selector-toolbar');
const findDeleteButton = () => wrapper.find(DeleteButton);
const findDropzone = () => wrapper.findAll(DesignDropzone).at(0);
+ const dropzoneClasses = () => findDropzone().classes();
+ const findDropzoneWrapper = () => wrapper.find('[data-testid="design-dropzone-wrapper"]');
const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
+ const findDesignsWrapper = () => wrapper.find('[data-testid="designs-root"]');
function createComponent({
loading = false,
@@ -92,19 +97,23 @@ describe('Design management index page', () => {
};
wrapper = shallowMount(Index, {
+ data() {
+ return {
+ designs,
+ allVersions,
+ permissions: {
+ createDesign,
+ },
+ };
+ },
mocks: { $apollo },
localVue,
router,
- stubs: { DesignDestroyer, ApolloMutation, ...stubs },
+ stubs: { DesignDestroyer, ApolloMutation, VueDraggable, ...stubs },
attachToDocument: true,
- });
-
- wrapper.setData({
- designs,
- allVersions,
- issueIid: '1',
- permissions: {
- createDesign,
+ provide: {
+ projectPath: 'project-path',
+ issueIid: '1',
},
});
}
@@ -117,9 +126,7 @@ describe('Design management index page', () => {
it('renders loading icon', () => {
createComponent({ loading: true });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchSnapshot();
});
it('renders error', () => {
@@ -135,25 +142,35 @@ describe('Design management index page', () => {
it('renders a toolbar with buttons when there are designs', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
- return wrapper.vm.$nextTick().then(() => {
- expect(findToolbar().exists()).toBe(true);
- });
+ expect(findToolbar().exists()).toBe(true);
});
it('renders designs list and header with upload button', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchSnapshot();
});
it('does not render toolbar when there is no permission', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false });
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('has correct classes applied to design dropzone', () => {
+ createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
+ expect(dropzoneClasses()).toContain('design-list-item');
+ expect(dropzoneClasses()).toContain('design-list-item-new');
+ });
+
+ it('has correct classes applied to dropzone wrapper', () => {
+ createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
+ expect(findDropzoneWrapper().classes()).toEqual([
+ 'gl-flex-direction-column',
+ 'col-md-6',
+ 'col-lg-3',
+ 'gl-mb-3',
+ ]);
});
});
@@ -162,11 +179,20 @@ describe('Design management index page', () => {
createComponent();
});
- it('renders empty text', () =>
+ it('renders design dropzone', () =>
wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot();
}));
+ it('has correct classes applied to design dropzone', () => {
+ expect(dropzoneClasses()).not.toContain('design-list-item');
+ expect(dropzoneClasses()).not.toContain('design-list-item-new');
+ });
+
+ it('has correct classes applied to dropzone wrapper', () => {
+ expect(findDropzoneWrapper().classes()).toEqual(['col-12']);
+ });
+
it('does not render a toolbar with buttons', () =>
wrapper.vm.$nextTick().then(() => {
expect(findToolbar().exists()).toBe(false);
@@ -185,7 +211,7 @@ describe('Design management index page', () => {
mutation: uploadDesignQuery,
variables: {
files: [{ name: 'test' }],
- projectPath: '',
+ projectPath: 'project-path',
iid: '1',
},
optimisticResponse: {
@@ -214,13 +240,10 @@ describe('Design management index page', () => {
},
versions: {
__typename: 'DesignVersionConnection',
- edges: {
- __typename: 'DesignVersionEdge',
- node: {
- __typename: 'DesignVersion',
- id: expect.anything(),
- sha: expect.anything(),
- },
+ nodes: {
+ __typename: 'DesignVersion',
+ id: expect.anything(),
+ sha: expect.anything(),
},
},
},
@@ -231,12 +254,18 @@ describe('Design management index page', () => {
},
};
- return wrapper.vm.$nextTick().then(() => {
- findDropzone().vm.$emit('change', [{ name: 'test' }]);
- expect(mutate).toHaveBeenCalledWith(mutationVariables);
- expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]);
- expect(wrapper.vm.isSaving).toBeTruthy();
- });
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ findDropzone().vm.$emit('change', [{ name: 'test' }]);
+ expect(mutate).toHaveBeenCalledWith(mutationVariables);
+ expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]);
+ expect(wrapper.vm.isSaving).toBeTruthy();
+ })
+ .then(() => {
+ expect(dropzoneClasses()).toContain('design-list-item');
+ expect(dropzoneClasses()).toContain('design-list-item-new');
+ });
});
it('sets isSaving', () => {
@@ -384,8 +413,7 @@ describe('Design management index page', () => {
it('renders toolbar buttons', () => {
expect(findToolbar().exists()).toBe(true);
- expect(findToolbar().classes()).toContain('d-flex');
- expect(findToolbar().classes()).not.toContain('d-none');
+ expect(findToolbar().isVisible()).toBe(true);
});
it('adds two designs to selected designs when their checkboxes are checked', () => {
@@ -442,9 +470,9 @@ describe('Design management index page', () => {
});
});
- it('on latest version when has no designs does not render toolbar buttons', () => {
+ it('on latest version when has no designs toolbar buttons are invisible', () => {
createComponent({ designs: [], allVersions: [mockVersion] });
- expect(findToolbar().exists()).toBe(false);
+ expect(findToolbar().isVisible()).toBe(false);
});
describe('on non-latest version', () => {
@@ -482,6 +510,10 @@ describe('Design management index page', () => {
});
event = new Event('paste');
+ event.clipboardData = {
+ files: [{ name: 'image.png', type: 'image/png' }],
+ getData: () => 'test.png',
+ };
router.replace({
name: DESIGNS_ROUTE_NAME,
@@ -491,43 +523,52 @@ describe('Design management index page', () => {
});
});
- it('calls onUploadDesign with valid paste', () => {
- event.clipboardData = {
- files: [{ name: 'image.png', type: 'image/png' }],
- getData: () => 'test.png',
- };
-
+ it('does not call paste event if designs wrapper is not hovered', () => {
document.dispatchEvent(event);
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
- new File([{ name: 'image.png' }], 'test.png'),
- ]);
+ expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
});
- it('renames a design if it has an image.png filename', () => {
- event.clipboardData = {
- files: [{ name: 'image.png', type: 'image/png' }],
- getData: () => 'image.png',
- };
+ describe('when designs wrapper is hovered', () => {
+ beforeEach(() => {
+ findDesignsWrapper().trigger('mouseenter');
+ });
- document.dispatchEvent(event);
+ it('calls onUploadDesign with valid paste', () => {
+ document.dispatchEvent(event);
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
- expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
- new File([{ name: 'image.png' }], `design_${Date.now()}.png`),
- ]);
- });
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
+ new File([{ name: 'image.png' }], 'test.png'),
+ ]);
+ });
- it('does not call onUploadDesign with invalid paste', () => {
- event.clipboardData = {
- items: [{ type: 'text/plain' }, { type: 'text' }],
- files: [],
- };
+ it('renames a design if it has an image.png filename', () => {
+ document.dispatchEvent(event);
- document.dispatchEvent(event);
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.onUploadDesign).toHaveBeenCalledWith([
+ new File([{ name: 'image.png' }], `design_${Date.now()}.png`),
+ ]);
+ });
- expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
+ it('does not call onUploadDesign with invalid paste', () => {
+ event.clipboardData = {
+ items: [{ type: 'text/plain' }, { type: 'text' }],
+ files: [],
+ };
+
+ document.dispatchEvent(event);
+
+ expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
+ });
+
+ it('removes onPaste listener after mouseleave event', async () => {
+ findDesignsWrapper().trigger('mouseleave');
+ document.dispatchEvent(event);
+
+ expect(wrapper.vm.onUploadDesign).not.toHaveBeenCalled();
+ });
});
});
@@ -535,9 +576,18 @@ describe('Design management index page', () => {
it('ensures fullscreen layout is not applied', () => {
createComponent(true);
- wrapper.vm.$router.push('/designs');
+ wrapper.vm.$router.push('/');
expect(mockPageEl.classList.remove).toHaveBeenCalledTimes(1);
expect(mockPageEl.classList.remove).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
});
+
+ it('should trigger a scrollIntoView method if designs route is detected', () => {
+ router.replace({
+ path: '/designs',
+ });
+ createComponent(true);
+
+ expect(scrollIntoViewMock).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/design_management/router_spec.js b/spec/frontend/design_management/router_spec.js
index d6488d3837a..2b8c7ee959b 100644
--- a/spec/frontend/design_management/router_spec.js
+++ b/spec/frontend/design_management/router_spec.js
@@ -5,11 +5,7 @@ import App from '~/design_management/components/app.vue';
import Designs from '~/design_management/pages/index.vue';
import DesignDetail from '~/design_management/pages/design/index.vue';
import createRouter from '~/design_management/router';
-import {
- ROOT_ROUTE_NAME,
- DESIGNS_ROUTE_NAME,
- DESIGN_ROUTE_NAME,
-} from '~/design_management/router/constants';
+import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
import '~/commons/bootstrap';
function factory(routeArg) {
@@ -49,7 +45,7 @@ describe('Design management router', () => {
window.location.hash = '';
});
- describe.each([['/'], [{ name: ROOT_ROUTE_NAME }]])('root route', routeArg => {
+ describe.each([['/'], [{ name: DESIGNS_ROUTE_NAME }]])('root route', routeArg => {
it('pushes home component', () => {
const wrapper = factory(routeArg);
@@ -57,14 +53,6 @@ describe('Design management router', () => {
});
});
- describe.each([['/designs'], [{ name: DESIGNS_ROUTE_NAME }]])('designs route', routeArg => {
- it('pushes designs root component', () => {
- const wrapper = factory(routeArg);
-
- expect(wrapper.find(Designs).exists()).toBe(true);
- });
- });
-
describe.each([['/designs/1'], [{ name: DESIGN_ROUTE_NAME, params: { id: '1' } }]])(
'designs detail route',
routeArg => {
diff --git a/spec/frontend/design_management/utils/cache_update_spec.js b/spec/frontend/design_management/utils/cache_update_spec.js
index 641d35ff9ff..e8a5cf3949d 100644
--- a/spec/frontend/design_management/utils/cache_update_spec.js
+++ b/spec/frontend/design_management/utils/cache_update_spec.js
@@ -13,7 +13,7 @@ import {
UPDATE_IMAGE_DIFF_NOTE_ERROR,
} from '~/design_management/utils/error_messages';
import design from '../mock_data/design';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash.js');
diff --git a/spec/frontend/design_management/utils/design_management_utils_spec.js b/spec/frontend/design_management/utils/design_management_utils_spec.js
index 478ebadc8f6..e6d836b9157 100644
--- a/spec/frontend/design_management/utils/design_management_utils_spec.js
+++ b/spec/frontend/design_management/utils/design_management_utils_spec.js
@@ -51,7 +51,7 @@ describe('extractDiscussions', () => {
};
});
- it('discards the edges.node artifacts of GraphQL', () => {
+ it('discards the node artifacts of GraphQL', () => {
expect(extractDiscussions(discussions)).toEqual([
{ id: 1, notes: ['a'], index: 1 },
{ id: 2, notes: ['b'], index: 2 },
@@ -96,10 +96,7 @@ describe('optimistic responses', () => {
discussions: { __typename: 'DesignDiscussion', nodes: [] },
versions: {
__typename: 'DesignVersionConnection',
- edges: {
- __typename: 'DesignVersionEdge',
- node: { __typename: 'DesignVersion', id: -1, sha: -1 },
- },
+ nodes: { __typename: 'DesignVersion', id: -1, sha: -1 },
},
},
],
diff --git a/spec/frontend/design_management/utils/error_messages_spec.js b/spec/frontend/design_management/utils/error_messages_spec.js
index 635ff931d7d..f5072c3b6b7 100644
--- a/spec/frontend/design_management/utils/error_messages_spec.js
+++ b/spec/frontend/design_management/utils/error_messages_spec.js
@@ -10,8 +10,8 @@ const mockFilenames = n =>
describe('Error message', () => {
describe('designDeletionError', () => {
- const singularMsg = 'Could not delete a design. Please try again.';
- const pluralMsg = 'Could not delete designs. Please try again.';
+ const singularMsg = 'Could not archive a design. Please try again.';
+ const pluralMsg = 'Could not archive designs. Please try again.';
describe('when [singular=true]', () => {
it.each([[undefined], [true]])('uses singular grammar', singularOption => {
@@ -55,7 +55,7 @@ describe('Error message', () => {
'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg, and 2 more.',
],
])('designUploadSkippedWarning', (uploadedFiles, skippedFiles, expected) => {
- test('returns expected warning message', () => {
+ it('returns expected warning message', () => {
expect(designUploadSkippedWarning(uploadedFiles, skippedFiles)).toBe(expected);
});
});
diff --git a/spec/frontend/design_management_new/components/__snapshots__/design_note_pin_spec.js.snap b/spec/frontend/design_management_legacy/components/__snapshots__/design_note_pin_spec.js.snap
index 4c848256e5b..62a0f675cff 100644
--- a/spec/frontend/design_management_new/components/__snapshots__/design_note_pin_spec.js.snap
+++ b/spec/frontend/design_management_legacy/components/__snapshots__/design_note_pin_spec.js.snap
@@ -3,13 +3,13 @@
exports[`Design note pin component should match the snapshot of note when repositioning 1`] = `
<button
aria-label="Comment form position"
- class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center btn-transparent comment-indicator"
+ class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 btn-transparent comment-indicator"
style="left: 10px; top: 10px; cursor: move;"
type="button"
>
- <icon-stub
+ <gl-icon-stub
name="image-comment-dark"
- size="16"
+ size="24"
/>
</button>
`;
@@ -17,7 +17,7 @@ exports[`Design note pin component should match the snapshot of note when reposi
exports[`Design note pin component should match the snapshot of note with index 1`] = `
<button
aria-label="Comment '1' position"
- class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center js-image-badge badge badge-pill"
+ class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 js-image-badge badge badge-pill"
style="left: 10px; top: 10px;"
type="button"
>
@@ -30,13 +30,13 @@ exports[`Design note pin component should match the snapshot of note with index
exports[`Design note pin component should match the snapshot of note without index 1`] = `
<button
aria-label="Comment form position"
- class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center btn-transparent comment-indicator"
+ class="design-pin gl-absolute gl-display-flex gl-align-items-center gl-justify-content-center gl-p-0 btn-transparent comment-indicator"
style="left: 10px; top: 10px;"
type="button"
>
- <icon-stub
+ <gl-icon-stub
name="image-comment-dark"
- size="16"
+ size="24"
/>
</button>
`;
diff --git a/spec/frontend/design_management_new/components/__snapshots__/design_presentation_spec.js.snap b/spec/frontend/design_management_legacy/components/__snapshots__/design_presentation_spec.js.snap
index 189962c5b2e..189962c5b2e 100644
--- a/spec/frontend/design_management_new/components/__snapshots__/design_presentation_spec.js.snap
+++ b/spec/frontend/design_management_legacy/components/__snapshots__/design_presentation_spec.js.snap
diff --git a/spec/frontend/design_management_new/components/__snapshots__/design_scaler_spec.js.snap b/spec/frontend/design_management_legacy/components/__snapshots__/design_scaler_spec.js.snap
index cb4575cbd11..cb4575cbd11 100644
--- a/spec/frontend/design_management_new/components/__snapshots__/design_scaler_spec.js.snap
+++ b/spec/frontend/design_management_legacy/components/__snapshots__/design_scaler_spec.js.snap
diff --git a/spec/frontend/design_management_new/components/__snapshots__/image_spec.js.snap b/spec/frontend/design_management_legacy/components/__snapshots__/image_spec.js.snap
index acaa62b11eb..acaa62b11eb 100644
--- a/spec/frontend/design_management_new/components/__snapshots__/image_spec.js.snap
+++ b/spec/frontend/design_management_legacy/components/__snapshots__/image_spec.js.snap
diff --git a/spec/frontend/design_management_new/components/delete_button_spec.js b/spec/frontend/design_management_legacy/components/delete_button_spec.js
index 218c58847a6..73b4908d06a 100644
--- a/spec/frontend/design_management_new/components/delete_button_spec.js
+++ b/spec/frontend/design_management_legacy/components/delete_button_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import { GlButton, GlModal, GlModalDirective } from '@gitlab/ui';
-import BatchDeleteButton from '~/design_management_new/components/delete_button.vue';
+import { GlDeprecatedButton, GlModal, GlModalDirective } from '@gitlab/ui';
+import BatchDeleteButton from '~/design_management_legacy/components/delete_button.vue';
describe('Batch delete button component', () => {
let wrapper;
- const findButton = () => wrapper.find(GlButton);
+ const findButton = () => wrapper.find(GlDeprecatedButton);
const findModal = () => wrapper.find(GlModal);
function createComponent(isDeleting = false) {
diff --git a/spec/frontend/design_management_new/components/design_note_pin_spec.js b/spec/frontend/design_management_legacy/components/design_note_pin_spec.js
index 8e2caa604f4..3077928cf86 100644
--- a/spec/frontend/design_management_new/components/design_note_pin_spec.js
+++ b/spec/frontend/design_management_legacy/components/design_note_pin_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import DesignNotePin from '~/design_management_new/components/design_note_pin.vue';
+import DesignNotePin from '~/design_management_legacy/components/design_note_pin.vue';
describe('Design note pin component', () => {
let wrapper;
diff --git a/spec/frontend/design_management_new/components/design_notes/__snapshots__/design_note_spec.js.snap b/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_note_spec.js.snap
index b55bacb6fc5..b55bacb6fc5 100644
--- a/spec/frontend/design_management_new/components/design_notes/__snapshots__/design_note_spec.js.snap
+++ b/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_note_spec.js.snap
diff --git a/spec/frontend/design_management_new/components/design_notes/__snapshots__/design_reply_form_spec.js.snap b/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
index e01c79e3520..e01c79e3520 100644
--- a/spec/frontend/design_management_new/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
+++ b/spec/frontend/design_management_legacy/components/design_notes/__snapshots__/design_reply_form_spec.js.snap
diff --git a/spec/frontend/design_management_new/components/design_notes/design_discussion_spec.js b/spec/frontend/design_management_legacy/components/design_notes/design_discussion_spec.js
index 401ce64e859..d20be97f470 100644
--- a/spec/frontend/design_management_new/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management_legacy/components/design_notes/design_discussion_spec.js
@@ -1,13 +1,13 @@
import { mount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
import notes from '../../mock_data/notes';
-import DesignDiscussion from '~/design_management_new/components/design_notes/design_discussion.vue';
-import DesignNote from '~/design_management_new/components/design_notes/design_note.vue';
-import DesignReplyForm from '~/design_management_new/components/design_notes/design_reply_form.vue';
-import createNoteMutation from '~/design_management_new/graphql/mutations/create_note.mutation.graphql';
-import toggleResolveDiscussionMutation from '~/design_management_new/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
+import DesignDiscussion from '~/design_management_legacy/components/design_notes/design_discussion.vue';
+import DesignNote from '~/design_management_legacy/components/design_notes/design_note.vue';
+import DesignReplyForm from '~/design_management_legacy/components/design_notes/design_reply_form.vue';
+import createNoteMutation from '~/design_management_legacy/graphql/mutations/create_note.mutation.graphql';
+import toggleResolveDiscussionMutation from '~/design_management_legacy/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
-import ToggleRepliesWidget from '~/design_management_new/components/design_notes/toggle_replies_widget.vue';
+import ToggleRepliesWidget from '~/design_management_legacy/components/design_notes/toggle_replies_widget.vue';
const discussion = {
id: '0',
@@ -61,10 +61,6 @@ describe('Design discussions component', () => {
...data,
};
},
- provide: {
- projectPath: 'project-path',
- issueIid: '1',
- },
mocks: {
$apollo,
$route: {
diff --git a/spec/frontend/design_management_new/components/design_notes/design_note_spec.js b/spec/frontend/design_management_legacy/components/design_notes/design_note_spec.js
index b0e3e85b9c6..aa187cd1388 100644
--- a/spec/frontend/design_management_new/components/design_notes/design_note_spec.js
+++ b/spec/frontend/design_management_legacy/components/design_notes/design_note_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
-import DesignNote from '~/design_management_new/components/design_notes/design_note.vue';
+import DesignNote from '~/design_management_legacy/components/design_notes/design_note.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import DesignReplyForm from '~/design_management_new/components/design_notes/design_reply_form.vue';
+import DesignReplyForm from '~/design_management_legacy/components/design_notes/design_reply_form.vue';
const scrollIntoViewMock = jest.fn();
const note = {
diff --git a/spec/frontend/design_management_new/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management_legacy/components/design_notes/design_reply_form_spec.js
index 9c1d6154516..088a71b64af 100644
--- a/spec/frontend/design_management_new/components/design_notes/design_reply_form_spec.js
+++ b/spec/frontend/design_management_legacy/components/design_notes/design_reply_form_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import DesignReplyForm from '~/design_management_new/components/design_notes/design_reply_form.vue';
+import DesignReplyForm from '~/design_management_legacy/components/design_notes/design_reply_form.vue';
const showModal = jest.fn();
diff --git a/spec/frontend/design_management_new/components/design_notes/toggle_replies_widget_spec.js b/spec/frontend/design_management_legacy/components/design_notes/toggle_replies_widget_spec.js
index d3c89075a24..acc7cbbca52 100644
--- a/spec/frontend/design_management_new/components/design_notes/toggle_replies_widget_spec.js
+++ b/spec/frontend/design_management_legacy/components/design_notes/toggle_replies_widget_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon, GlButton, GlLink } from '@gitlab/ui';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import ToggleRepliesWidget from '~/design_management_new/components/design_notes/toggle_replies_widget.vue';
+import ToggleRepliesWidget from '~/design_management_legacy/components/design_notes/toggle_replies_widget.vue';
import notes from '../../mock_data/notes';
describe('Toggle replies widget component', () => {
diff --git a/spec/frontend/design_management_new/components/design_overlay_spec.js b/spec/frontend/design_management_legacy/components/design_overlay_spec.js
index 4ca69c143a8..c014f3479f4 100644
--- a/spec/frontend/design_management_new/components/design_overlay_spec.js
+++ b/spec/frontend/design_management_legacy/components/design_overlay_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
-import DesignOverlay from '~/design_management_new/components/design_overlay.vue';
-import updateActiveDiscussion from '~/design_management_new/graphql/mutations/update_active_discussion.mutation.graphql';
+import DesignOverlay from '~/design_management_legacy/components/design_overlay.vue';
+import updateActiveDiscussion from '~/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql';
import notes from '../mock_data/notes';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '~/design_management_new/constants';
+import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '~/design_management_legacy/constants';
const mutate = jest.fn(() => Promise.resolve());
diff --git a/spec/frontend/design_management_new/components/design_presentation_spec.js b/spec/frontend/design_management_legacy/components/design_presentation_spec.js
index d043a762cd2..ceff86b0549 100644
--- a/spec/frontend/design_management_new/components/design_presentation_spec.js
+++ b/spec/frontend/design_management_legacy/components/design_presentation_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import DesignPresentation from '~/design_management_new/components/design_presentation.vue';
-import DesignOverlay from '~/design_management_new/components/design_overlay.vue';
+import DesignPresentation from '~/design_management_legacy/components/design_presentation.vue';
+import DesignOverlay from '~/design_management_legacy/components/design_overlay.vue';
const mockOverlayData = {
overlayDimensions: {
diff --git a/spec/frontend/design_management_new/components/design_scaler_spec.js b/spec/frontend/design_management_legacy/components/design_scaler_spec.js
index 5ff2554cd60..30ef5ab159b 100644
--- a/spec/frontend/design_management_new/components/design_scaler_spec.js
+++ b/spec/frontend/design_management_legacy/components/design_scaler_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import DesignScaler from '~/design_management_new/components/design_scaler.vue';
+import DesignScaler from '~/design_management_legacy/components/design_scaler.vue';
describe('Design management design scaler component', () => {
let wrapper;
diff --git a/spec/frontend/design_management_new/components/design_sidebar_spec.js b/spec/frontend/design_management_legacy/components/design_sidebar_spec.js
index f1d442a7b21..fc0f618c359 100644
--- a/spec/frontend/design_management_new/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management_legacy/components/design_sidebar_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
import { GlCollapse, GlPopover } from '@gitlab/ui';
import Cookies from 'js-cookie';
-import DesignSidebar from '~/design_management_new/components/design_sidebar.vue';
+import DesignSidebar from '~/design_management_legacy/components/design_sidebar.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
-import DesignDiscussion from '~/design_management_new/components/design_notes/design_discussion.vue';
+import DesignDiscussion from '~/design_management_legacy/components/design_notes/design_discussion.vue';
import design from '../mock_data/design';
-import updateActiveDiscussionMutation from '~/design_management_new/graphql/mutations/update_active_discussion.mutation.graphql';
+import updateActiveDiscussionMutation from '~/design_management_legacy/graphql/mutations/update_active_discussion.mutation.graphql';
const updateActiveDiscussionMutationVariables = {
mutation: updateActiveDiscussionMutation,
diff --git a/spec/frontend/design_management_new/components/image_spec.js b/spec/frontend/design_management_legacy/components/image_spec.js
index c1a8a8767df..265c91abb4e 100644
--- a/spec/frontend/design_management_new/components/image_spec.js
+++ b/spec/frontend/design_management_legacy/components/image_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
-import DesignImage from '~/design_management_new/components/image.vue';
+import DesignImage from '~/design_management_legacy/components/image.vue';
describe('Design management large image component', () => {
let wrapper;
diff --git a/spec/frontend/design_management_legacy/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management_legacy/components/list/__snapshots__/item_spec.js.snap
new file mode 100644
index 00000000000..168b9424006
--- /dev/null
+++ b/spec/frontend/design_management_legacy/components/list/__snapshots__/item_spec.js.snap
@@ -0,0 +1,149 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Design management list item component when item appears in view after image is loaded renders media broken icon when image onerror triggered 1`] = `
+<gl-icon-stub
+ class="text-secondary"
+ name="media-broken"
+ size="32"
+/>
+`;
+
+exports[`Design management list item component with notes renders item with multiple comments 1`] = `
+<router-link-stub
+ class="card cursor-pointer text-plain js-design-list-item design-list-item"
+ to="[object Object]"
+>
+ <div
+ class="card-body p-0 d-flex-center overflow-hidden position-relative"
+ >
+ <!---->
+
+ <gl-intersection-observer-stub>
+ <!---->
+
+ <img
+ alt="test"
+ class="block mx-auto mw-100 mh-100 design-img"
+ data-qa-selector="design_image"
+ src=""
+ />
+ </gl-intersection-observer-stub>
+ </div>
+
+ <div
+ class="card-footer d-flex w-100"
+ >
+ <div
+ class="d-flex flex-column str-truncated-100"
+ >
+ <span
+ class="bold str-truncated-100"
+ data-qa-selector="design_file_name"
+ >
+ test
+ </span>
+
+ <span
+ class="str-truncated-100"
+ >
+
+ Updated
+ <timeago-stub
+ cssclass=""
+ time="01-01-2019"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </div>
+
+ <div
+ class="ml-auto d-flex align-items-center text-secondary"
+ >
+ <icon-stub
+ class="ml-1"
+ name="comments"
+ size="16"
+ />
+
+ <span
+ aria-label="2 comments"
+ class="ml-1"
+ >
+
+ 2
+
+ </span>
+ </div>
+ </div>
+</router-link-stub>
+`;
+
+exports[`Design management list item component with notes renders item with single comment 1`] = `
+<router-link-stub
+ class="card cursor-pointer text-plain js-design-list-item design-list-item"
+ to="[object Object]"
+>
+ <div
+ class="card-body p-0 d-flex-center overflow-hidden position-relative"
+ >
+ <!---->
+
+ <gl-intersection-observer-stub>
+ <!---->
+
+ <img
+ alt="test"
+ class="block mx-auto mw-100 mh-100 design-img"
+ data-qa-selector="design_image"
+ src=""
+ />
+ </gl-intersection-observer-stub>
+ </div>
+
+ <div
+ class="card-footer d-flex w-100"
+ >
+ <div
+ class="d-flex flex-column str-truncated-100"
+ >
+ <span
+ class="bold str-truncated-100"
+ data-qa-selector="design_file_name"
+ >
+ test
+ </span>
+
+ <span
+ class="str-truncated-100"
+ >
+
+ Updated
+ <timeago-stub
+ cssclass=""
+ time="01-01-2019"
+ tooltipplacement="bottom"
+ />
+ </span>
+ </div>
+
+ <div
+ class="ml-auto d-flex align-items-center text-secondary"
+ >
+ <icon-stub
+ class="ml-1"
+ name="comments"
+ size="16"
+ />
+
+ <span
+ aria-label="1 comment"
+ class="ml-1"
+ >
+
+ 1
+
+ </span>
+ </div>
+ </div>
+</router-link-stub>
+`;
diff --git a/spec/frontend/design_management_new/components/list/item_spec.js b/spec/frontend/design_management_legacy/components/list/item_spec.js
index 5e3e6832acb..e9bb0fc3f29 100644
--- a/spec/frontend/design_management_new/components/list/item_spec.js
+++ b/spec/frontend/design_management_legacy/components/list/item_spec.js
@@ -1,7 +1,8 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlIcon, GlLoadingIcon, GlIntersectionObserver } from '@gitlab/ui';
import VueRouter from 'vue-router';
-import Item from '~/design_management_new/components/list/item.vue';
+import Icon from '~/vue_shared/components/icon.vue';
+import Item from '~/design_management_legacy/components/list/item.vue';
const localVue = createLocalVue();
localVue.use(VueRouter);
@@ -18,6 +19,10 @@ const DESIGN_VERSION_EVENT = {
describe('Design management list item component', () => {
let wrapper;
+ const findDesignEvent = () => wrapper.find('[data-testid="designEvent"]');
+ const findEventIcon = () => findDesignEvent().find(Icon);
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
function createComponent({
notesCount = 0,
event = DESIGN_VERSION_EVENT.NO_CHANGE,
@@ -134,35 +139,31 @@ describe('Design management list item component', () => {
});
});
- describe('with no notes', () => {
- it('renders item with no status icon for none event', () => {
- createComponent();
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders item with correct status icon for modification event', () => {
- createComponent({ event: DESIGN_VERSION_EVENT.MODIFICATION });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders item with correct status icon for deletion event', () => {
- createComponent({ event: DESIGN_VERSION_EVENT.DELETION });
+ it('renders loading spinner when isUploading is true', () => {
+ createComponent({ isUploading: true });
- expect(wrapper.element).toMatchSnapshot();
- });
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
- it('renders item with correct status icon for creation event', () => {
- createComponent({ event: DESIGN_VERSION_EVENT.CREATION });
+ it('renders item with no status icon for none event', () => {
+ createComponent();
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders loading spinner when isUploading is true', () => {
- createComponent({ isUploading: true });
+ expect(findDesignEvent().exists()).toBe(false);
+ });
- expect(wrapper.element).toMatchSnapshot();
+ describe('with associated event', () => {
+ it.each`
+ event | icon | className
+ ${DESIGN_VERSION_EVENT.MODIFICATION} | ${'file-modified-solid'} | ${'text-primary-500'}
+ ${DESIGN_VERSION_EVENT.DELETION} | ${'file-deletion-solid'} | ${'text-danger-500'}
+ ${DESIGN_VERSION_EVENT.CREATION} | ${'file-addition-solid'} | ${'text-success-500'}
+ `('renders item with correct status icon for $event event', ({ event, icon, className }) => {
+ createComponent({ event });
+ const eventIcon = findEventIcon();
+
+ expect(eventIcon.exists()).toBe(true);
+ expect(eventIcon.props('name')).toBe(icon);
+ expect(eventIcon.classes()).toContain(className);
});
});
});
diff --git a/spec/frontend/design_management_new/components/toolbar/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/index_spec.js.snap
index f251171ecda..e55cff8de3d 100644
--- a/spec/frontend/design_management_new/components/toolbar/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/index_spec.js.snap
@@ -7,7 +7,6 @@ exports[`Design management toolbar component renders design and updated data 1`]
<a
aria-label="Go back to designs"
class="mr-3 text-plain d-flex justify-content-center align-items-center"
- data-testid="close-design"
>
<icon-stub
name="close"
@@ -50,7 +49,6 @@ exports[`Design management toolbar component renders design and updated data 1`]
<delete-button-stub
buttonclass=""
- buttonsize="medium"
buttonvariant="danger"
hasselecteddesigns="true"
>
diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/pagination_button_spec.js.snap b/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_button_spec.js.snap
index 08662a04f15..08662a04f15 100644
--- a/spec/frontend/design_management/components/toolbar/__snapshots__/pagination_button_spec.js.snap
+++ b/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_button_spec.js.snap
diff --git a/spec/frontend/design_management/components/toolbar/__snapshots__/pagination_spec.js.snap b/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_spec.js.snap
index 0197b4bff79..0197b4bff79 100644
--- a/spec/frontend/design_management/components/toolbar/__snapshots__/pagination_spec.js.snap
+++ b/spec/frontend/design_management_legacy/components/toolbar/__snapshots__/pagination_spec.js.snap
diff --git a/spec/frontend/design_management_new/components/toolbar/index_spec.js b/spec/frontend/design_management_legacy/components/toolbar/index_spec.js
index eb5ae15ed58..8207cad4136 100644
--- a/spec/frontend/design_management_new/components/toolbar/index_spec.js
+++ b/spec/frontend/design_management_legacy/components/toolbar/index_spec.js
@@ -1,9 +1,9 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueRouter from 'vue-router';
-import Toolbar from '~/design_management_new/components/toolbar/index.vue';
-import DeleteButton from '~/design_management_new/components/delete_button.vue';
-import { DESIGNS_ROUTE_NAME } from '~/design_management_new/router/constants';
import { GlDeprecatedButton } from '@gitlab/ui';
+import Toolbar from '~/design_management_legacy/components/toolbar/index.vue';
+import DeleteButton from '~/design_management_legacy/components/delete_button.vue';
+import { DESIGNS_ROUTE_NAME } from '~/design_management_legacy/router/constants';
const localVue = createLocalVue();
localVue.use(VueRouter);
diff --git a/spec/frontend/design_management_new/components/toolbar/pagination_button_spec.js b/spec/frontend/design_management_legacy/components/toolbar/pagination_button_spec.js
index 5f33d65fc1f..d2153adca45 100644
--- a/spec/frontend/design_management_new/components/toolbar/pagination_button_spec.js
+++ b/spec/frontend/design_management_legacy/components/toolbar/pagination_button_spec.js
@@ -1,7 +1,7 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import VueRouter from 'vue-router';
-import PaginationButton from '~/design_management_new/components/toolbar/pagination_button.vue';
-import { DESIGN_ROUTE_NAME } from '~/design_management_new/router/constants';
+import PaginationButton from '~/design_management_legacy/components/toolbar/pagination_button.vue';
+import { DESIGN_ROUTE_NAME } from '~/design_management_legacy/router/constants';
const localVue = createLocalVue();
localVue.use(VueRouter);
diff --git a/spec/frontend/design_management/components/toolbar/pagination_spec.js b/spec/frontend/design_management_legacy/components/toolbar/pagination_spec.js
index db5a36dadf6..21b55113a6e 100644
--- a/spec/frontend/design_management/components/toolbar/pagination_spec.js
+++ b/spec/frontend/design_management_legacy/components/toolbar/pagination_spec.js
@@ -1,8 +1,8 @@
/* global Mousetrap */
import 'mousetrap';
import { shallowMount } from '@vue/test-utils';
-import Pagination from '~/design_management/components/toolbar/pagination.vue';
-import { DESIGN_ROUTE_NAME } from '~/design_management/router/constants';
+import Pagination from '~/design_management_legacy/components/toolbar/pagination.vue';
+import { DESIGN_ROUTE_NAME } from '~/design_management_legacy/router/constants';
const push = jest.fn();
const $router = {
diff --git a/spec/frontend/design_management_new/components/upload/__snapshots__/button_spec.js.snap b/spec/frontend/design_management_legacy/components/upload/__snapshots__/button_spec.js.snap
index b498becc606..27c0ba589e6 100644
--- a/spec/frontend/design_management_new/components/upload/__snapshots__/button_spec.js.snap
+++ b/spec/frontend/design_management_legacy/components/upload/__snapshots__/button_spec.js.snap
@@ -4,10 +4,8 @@ exports[`Design management upload button component renders inverted upload desig
<div
isinverted="true"
>
- <gl-button-stub
- category="tertiary"
- icon=""
- size="small"
+ <gl-deprecated-button-stub
+ size="md"
title="Adding a design with the same filename replaces the file in a new version."
variant="success"
>
@@ -15,7 +13,7 @@ exports[`Design management upload button component renders inverted upload desig
Upload designs
<!---->
- </gl-button-stub>
+ </gl-deprecated-button-stub>
<input
accept="image/*"
@@ -29,11 +27,9 @@ exports[`Design management upload button component renders inverted upload desig
exports[`Design management upload button component renders loading icon 1`] = `
<div>
- <gl-button-stub
- category="tertiary"
+ <gl-deprecated-button-stub
disabled="true"
- icon=""
- size="small"
+ size="md"
title="Adding a design with the same filename replaces the file in a new version."
variant="success"
>
@@ -47,7 +43,7 @@ exports[`Design management upload button component renders loading icon 1`] = `
label="Loading"
size="sm"
/>
- </gl-button-stub>
+ </gl-deprecated-button-stub>
<input
accept="image/*"
@@ -61,10 +57,8 @@ exports[`Design management upload button component renders loading icon 1`] = `
exports[`Design management upload button component renders upload design button 1`] = `
<div>
- <gl-button-stub
- category="tertiary"
- icon=""
- size="small"
+ <gl-deprecated-button-stub
+ size="md"
title="Adding a design with the same filename replaces the file in a new version."
variant="success"
>
@@ -72,7 +66,7 @@ exports[`Design management upload button component renders upload design button
Upload designs
<!---->
- </gl-button-stub>
+ </gl-deprecated-button-stub>
<input
accept="image/*"
diff --git a/spec/frontend/design_management_new/components/upload/__snapshots__/design_dropzone_spec.js.snap b/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_dropzone_spec.js.snap
index c53c6c889b0..0737b9729a2 100644
--- a/spec/frontend/design_management_new/components/upload/__snapshots__/design_dropzone_spec.js.snap
+++ b/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_dropzone_spec.js.snap
@@ -5,23 +5,20 @@ exports[`Design management dropzone component when dragging renders correct temp
class="w-100 position-relative"
>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
- data-testid="dropzone-area"
+ class="d-flex-center flex-column text-center"
>
<gl-icon-stub
- class="gl-mb-2"
- name="upload"
- size="24"
+ class="mb-4"
+ name="doc-new"
+ size="48"
/>
- <p
- class="gl-font-weight-bold gl-mb-0"
- >
+ <p>
<gl-sprintf-stub
- message="Drop or %{linkStart}upload%{linkEnd} Designs to attach"
+ message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
/>
</p>
</div>
@@ -46,9 +43,7 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style="display: none;"
>
- <h3
- class=""
- >
+ <h3>
Oh no!
</h3>
@@ -61,9 +56,7 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style=""
>
- <h3
- class=""
- >
+ <h3>
Incoming!
</h3>
@@ -81,23 +74,20 @@ exports[`Design management dropzone component when dragging renders correct temp
class="w-100 position-relative"
>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
- data-testid="dropzone-area"
+ class="d-flex-center flex-column text-center"
>
<gl-icon-stub
- class="gl-mb-2"
- name="upload"
- size="24"
+ class="mb-4"
+ name="doc-new"
+ size="48"
/>
- <p
- class="gl-font-weight-bold gl-mb-0"
- >
+ <p>
<gl-sprintf-stub
- message="Drop or %{linkStart}upload%{linkEnd} Designs to attach"
+ message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
/>
</p>
</div>
@@ -122,9 +112,7 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style="display: none;"
>
- <h3
- class=""
- >
+ <h3>
Oh no!
</h3>
@@ -137,9 +125,7 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style=""
>
- <h3
- class=""
- >
+ <h3>
Incoming!
</h3>
@@ -157,23 +143,20 @@ exports[`Design management dropzone component when dragging renders correct temp
class="w-100 position-relative"
>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
- data-testid="dropzone-area"
+ class="d-flex-center flex-column text-center"
>
<gl-icon-stub
- class="gl-mb-2"
- name="upload"
- size="24"
+ class="mb-4"
+ name="doc-new"
+ size="48"
/>
- <p
- class="gl-font-weight-bold gl-mb-0"
- >
+ <p>
<gl-sprintf-stub
- message="Drop or %{linkStart}upload%{linkEnd} Designs to attach"
+ message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
/>
</p>
</div>
@@ -197,9 +180,7 @@ exports[`Design management dropzone component when dragging renders correct temp
<div
class="mw-50 text-center"
>
- <h3
- class=""
- >
+ <h3>
Oh no!
</h3>
@@ -212,9 +193,7 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style="display: none;"
>
- <h3
- class=""
- >
+ <h3>
Incoming!
</h3>
@@ -232,23 +211,20 @@ exports[`Design management dropzone component when dragging renders correct temp
class="w-100 position-relative"
>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
- data-testid="dropzone-area"
+ class="d-flex-center flex-column text-center"
>
<gl-icon-stub
- class="gl-mb-2"
- name="upload"
- size="24"
+ class="mb-4"
+ name="doc-new"
+ size="48"
/>
- <p
- class="gl-font-weight-bold gl-mb-0"
- >
+ <p>
<gl-sprintf-stub
- message="Drop or %{linkStart}upload%{linkEnd} Designs to attach"
+ message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
/>
</p>
</div>
@@ -272,9 +248,7 @@ exports[`Design management dropzone component when dragging renders correct temp
<div
class="mw-50 text-center"
>
- <h3
- class=""
- >
+ <h3>
Oh no!
</h3>
@@ -287,9 +261,7 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style="display: none;"
>
- <h3
- class=""
- >
+ <h3>
Incoming!
</h3>
@@ -307,23 +279,20 @@ exports[`Design management dropzone component when dragging renders correct temp
class="w-100 position-relative"
>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
- data-testid="dropzone-area"
+ class="d-flex-center flex-column text-center"
>
<gl-icon-stub
- class="gl-mb-2"
- name="upload"
- size="24"
+ class="mb-4"
+ name="doc-new"
+ size="48"
/>
- <p
- class="gl-font-weight-bold gl-mb-0"
- >
+ <p>
<gl-sprintf-stub
- message="Drop or %{linkStart}upload%{linkEnd} Designs to attach"
+ message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
/>
</p>
</div>
@@ -347,9 +316,7 @@ exports[`Design management dropzone component when dragging renders correct temp
<div
class="mw-50 text-center"
>
- <h3
- class=""
- >
+ <h3>
Oh no!
</h3>
@@ -362,9 +329,7 @@ exports[`Design management dropzone component when dragging renders correct temp
class="mw-50 text-center"
style="display: none;"
>
- <h3
- class=""
- >
+ <h3>
Incoming!
</h3>
@@ -382,23 +347,20 @@ exports[`Design management dropzone component when no slot provided renders defa
class="w-100 position-relative"
>
<button
- class="card design-dropzone-card design-dropzone-border w-100 h-100 gl-align-items-center gl-justify-content-center gl-p-3"
+ class="card design-dropzone-card design-dropzone-border w-100 h-100 d-flex-center p-3"
>
<div
- class="gl-display-flex gl-align-items-center gl-justify-content-center gl-text-center gl-flex-direction-column"
- data-testid="dropzone-area"
+ class="d-flex-center flex-column text-center"
>
<gl-icon-stub
- class="gl-mb-2"
- name="upload"
- size="24"
+ class="mb-4"
+ name="doc-new"
+ size="48"
/>
- <p
- class="gl-font-weight-bold gl-mb-0"
- >
+ <p>
<gl-sprintf-stub
- message="Drop or %{linkStart}upload%{linkEnd} Designs to attach"
+ message="%{lineOneStart}Drag and drop to upload your designs%{lineOneEnd} or %{linkStart}click to upload%{linkEnd}."
/>
</p>
</div>
@@ -422,9 +384,7 @@ exports[`Design management dropzone component when no slot provided renders defa
<div
class="mw-50 text-center"
>
- <h3
- class=""
- >
+ <h3>
Oh no!
</h3>
@@ -437,9 +397,7 @@ exports[`Design management dropzone component when no slot provided renders defa
class="mw-50 text-center"
style="display: none;"
>
- <h3
- class=""
- >
+ <h3>
Incoming!
</h3>
@@ -470,9 +428,7 @@ exports[`Design management dropzone component when slot provided renders dropzon
<div
class="mw-50 text-center"
>
- <h3
- class=""
- >
+ <h3>
Oh no!
</h3>
@@ -485,9 +441,7 @@ exports[`Design management dropzone component when slot provided renders dropzon
class="mw-50 text-center"
style="display: none;"
>
- <h3
- class=""
- >
+ <h3>
Incoming!
</h3>
diff --git a/spec/frontend/design_management_new/components/upload/__snapshots__/design_version_dropdown_spec.js.snap b/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
index 0d16acdef54..d34b925f33d 100644
--- a/spec/frontend/design_management_new/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
+++ b/spec/frontend/design_management_legacy/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
@@ -1,23 +1,14 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management design version dropdown component renders design version dropdown button 1`] = `
-<gl-new-dropdown-stub
- category="tertiary"
+<gl-deprecated-dropdown-stub
class="design-version-dropdown"
- headertext=""
issueiid=""
projectpath=""
- size="small"
text="Showing Latest Version"
- variant="default"
+ variant="link"
>
- <gl-new-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightname=""
- secondarytext=""
- >
+ <gl-deprecated-dropdown-item-stub>
<router-link-stub
class="d-flex js-version-link"
to="[object Object]"
@@ -37,17 +28,11 @@ exports[`Design management design version dropdown component renders design vers
</div>
<i
- class="fa fa-check pull-right"
+ class="fa fa-check float-right gl-mr-2"
/>
</router-link-stub>
- </gl-new-dropdown-item-stub>
- <gl-new-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightname=""
- secondarytext=""
- >
+ </gl-deprecated-dropdown-item-stub>
+ <gl-deprecated-dropdown-item-stub>
<router-link-stub
class="d-flex js-version-link"
to="[object Object]"
@@ -66,28 +51,19 @@ exports[`Design management design version dropdown component renders design vers
<!---->
</router-link-stub>
- </gl-new-dropdown-item-stub>
-</gl-new-dropdown-stub>
+ </gl-deprecated-dropdown-item-stub>
+</gl-deprecated-dropdown-stub>
`;
exports[`Design management design version dropdown component renders design version list 1`] = `
-<gl-new-dropdown-stub
- category="tertiary"
+<gl-deprecated-dropdown-stub
class="design-version-dropdown"
- headertext=""
issueiid=""
projectpath=""
- size="small"
text="Showing Latest Version"
- variant="default"
+ variant="link"
>
- <gl-new-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightname=""
- secondarytext=""
- >
+ <gl-deprecated-dropdown-item-stub>
<router-link-stub
class="d-flex js-version-link"
to="[object Object]"
@@ -107,17 +83,11 @@ exports[`Design management design version dropdown component renders design vers
</div>
<i
- class="fa fa-check pull-right"
+ class="fa fa-check float-right gl-mr-2"
/>
</router-link-stub>
- </gl-new-dropdown-item-stub>
- <gl-new-dropdown-item-stub
- avatarurl=""
- iconcolor=""
- iconname=""
- iconrightname=""
- secondarytext=""
- >
+ </gl-deprecated-dropdown-item-stub>
+ <gl-deprecated-dropdown-item-stub>
<router-link-stub
class="d-flex js-version-link"
to="[object Object]"
@@ -136,6 +106,6 @@ exports[`Design management design version dropdown component renders design vers
<!---->
</router-link-stub>
- </gl-new-dropdown-item-stub>
-</gl-new-dropdown-stub>
+ </gl-deprecated-dropdown-item-stub>
+</gl-deprecated-dropdown-stub>
`;
diff --git a/spec/frontend/design_management_new/components/upload/button_spec.js b/spec/frontend/design_management_legacy/components/upload/button_spec.js
index 7f751982491..dde5c694194 100644
--- a/spec/frontend/design_management_new/components/upload/button_spec.js
+++ b/spec/frontend/design_management_legacy/components/upload/button_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import UploadButton from '~/design_management_new/components/upload/button.vue';
+import UploadButton from '~/design_management_legacy/components/upload/button.vue';
describe('Design management upload button component', () => {
let wrapper;
diff --git a/spec/frontend/design_management_new/components/upload/design_dropzone_spec.js b/spec/frontend/design_management_legacy/components/upload/design_dropzone_spec.js
index c48cbb10172..1907a3124a6 100644
--- a/spec/frontend/design_management_new/components/upload/design_dropzone_spec.js
+++ b/spec/frontend/design_management_legacy/components/upload/design_dropzone_spec.js
@@ -1,7 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import DesignDropzone from '~/design_management_new/components/upload/design_dropzone.vue';
-import createFlash from '~/flash';
-import { GlIcon } from '@gitlab/ui';
+import DesignDropzone from '~/design_management_legacy/components/upload/design_dropzone.vue';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
@@ -13,16 +12,10 @@ describe('Design management dropzone component', () => {
};
const findDropzoneCard = () => wrapper.find('.design-dropzone-card');
- const findDropzoneArea = () => wrapper.find('[data-testid="dropzone-area"]');
- const findIcon = () => wrapper.find(GlIcon);
- function createComponent({ slots = {}, data = {}, props = {} } = {}) {
+ function createComponent({ slots = {}, data = {} } = {}) {
wrapper = shallowMount(DesignDropzone, {
slots,
- propsData: {
- hasDesigns: true,
- ...props,
- },
data() {
return data;
},
@@ -136,16 +129,4 @@ describe('Design management dropzone component', () => {
});
});
});
-
- it('applies correct classes when there are no designs or no design saving loader', () => {
- createComponent({ props: { hasDesigns: false } });
- expect(findDropzoneArea().classes()).not.toContain('gl-flex-direction-column');
- expect(findIcon().classes()).toEqual(['gl-mr-4']);
- });
-
- it('applies correct classes when there are designs or design saving loader', () => {
- createComponent({ props: { hasDesigns: true } });
- expect(findDropzoneArea().classes()).toContain('gl-flex-direction-column');
- expect(findIcon().classes()).toEqual(['gl-mb-2']);
- });
});
diff --git a/spec/frontend/design_management_new/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management_legacy/components/upload/design_version_dropdown_spec.js
index 74e7f3f88fc..7fb85f357c7 100644
--- a/spec/frontend/design_management_new/components/upload/design_version_dropdown_spec.js
+++ b/spec/frontend/design_management_legacy/components/upload/design_version_dropdown_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import DesignVersionDropdown from '~/design_management_new/components/upload/design_version_dropdown.vue';
-import { GlNewDropdown, GlNewDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
+import DesignVersionDropdown from '~/design_management_legacy/components/upload/design_version_dropdown.vue';
import mockAllVersions from './mock_data/all_versions';
const LATEST_VERSION_ID = 3;
@@ -75,7 +75,9 @@ describe('Design management design version dropdown component', () => {
createComponent();
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlNewDropdown).attributes('text')).toBe('Showing Latest Version');
+ expect(wrapper.find(GlDeprecatedDropdown).attributes('text')).toBe(
+ 'Showing Latest Version',
+ );
});
});
@@ -83,7 +85,9 @@ describe('Design management design version dropdown component', () => {
createComponent({ maxVersions: 1 });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlNewDropdown).attributes('text')).toBe('Showing Latest Version');
+ expect(wrapper.find(GlDeprecatedDropdown).attributes('text')).toBe(
+ 'Showing Latest Version',
+ );
});
});
@@ -91,7 +95,7 @@ describe('Design management design version dropdown component', () => {
createComponent({ $route: designRouteFactory(PREVIOUS_VERSION_ID) });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlNewDropdown).attributes('text')).toBe(`Showing Version #1`);
+ expect(wrapper.find(GlDeprecatedDropdown).attributes('text')).toBe(`Showing Version #1`);
});
});
@@ -99,7 +103,9 @@ describe('Design management design version dropdown component', () => {
createComponent({ $route: designRouteFactory(LATEST_VERSION_ID) });
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(GlNewDropdown).attributes('text')).toBe('Showing Latest Version');
+ expect(wrapper.find(GlDeprecatedDropdown).attributes('text')).toBe(
+ 'Showing Latest Version',
+ );
});
});
@@ -107,7 +113,9 @@ describe('Design management design version dropdown component', () => {
createComponent();
return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.findAll(GlNewDropdownItem)).toHaveLength(wrapper.vm.allVersions.length);
+ expect(wrapper.findAll(GlDeprecatedDropdownItem)).toHaveLength(
+ wrapper.vm.allVersions.length,
+ );
});
});
});
diff --git a/spec/frontend/design_management_new/components/upload/mock_data/all_versions.js b/spec/frontend/design_management_legacy/components/upload/mock_data/all_versions.js
index e76bbd261bd..e76bbd261bd 100644
--- a/spec/frontend/design_management_new/components/upload/mock_data/all_versions.js
+++ b/spec/frontend/design_management_legacy/components/upload/mock_data/all_versions.js
diff --git a/spec/frontend/design_management_new/mock_data/all_versions.js b/spec/frontend/design_management_legacy/mock_data/all_versions.js
index c389fdb8747..c389fdb8747 100644
--- a/spec/frontend/design_management_new/mock_data/all_versions.js
+++ b/spec/frontend/design_management_legacy/mock_data/all_versions.js
diff --git a/spec/frontend/design_management_new/mock_data/design.js b/spec/frontend/design_management_legacy/mock_data/design.js
index 675198b9408..675198b9408 100644
--- a/spec/frontend/design_management_new/mock_data/design.js
+++ b/spec/frontend/design_management_legacy/mock_data/design.js
diff --git a/spec/frontend/design_management_new/mock_data/designs.js b/spec/frontend/design_management_legacy/mock_data/designs.js
index 07f5c1b7457..07f5c1b7457 100644
--- a/spec/frontend/design_management_new/mock_data/designs.js
+++ b/spec/frontend/design_management_legacy/mock_data/designs.js
diff --git a/spec/frontend/design_management_new/mock_data/no_designs.js b/spec/frontend/design_management_legacy/mock_data/no_designs.js
index 9db0ffcade2..9db0ffcade2 100644
--- a/spec/frontend/design_management_new/mock_data/no_designs.js
+++ b/spec/frontend/design_management_legacy/mock_data/no_designs.js
diff --git a/spec/frontend/design_management_new/mock_data/notes.js b/spec/frontend/design_management_legacy/mock_data/notes.js
index 80cb3944786..80cb3944786 100644
--- a/spec/frontend/design_management_new/mock_data/notes.js
+++ b/spec/frontend/design_management_legacy/mock_data/notes.js
diff --git a/spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_legacy/pages/__snapshots__/index_spec.js.snap
index 902803b0ad1..3ba63fd14f0 100644
--- a/spec/frontend/design_management_new/pages/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management_legacy/pages/__snapshots__/index_spec.js.snap
@@ -1,10 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Design management index page designs does not render toolbar when there is no permission 1`] = `
-<div
- class="gl-mt-5 designs-root"
- data-testid="designs-root"
->
+<div>
<!---->
<div
@@ -13,24 +10,18 @@ exports[`Design management index page designs does not render toolbar when there
<ol
class="list-unstyled row"
>
- <!---->
-
<li
- class="gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3"
- data-testid="design-dropzone-wrapper"
+ class="col-md-6 col-lg-4 mb-3"
>
<design-dropzone-stub
- class="design-list-item design-list-item-new"
- hasdesigns="true"
+ class="design-list-item"
/>
</li>
<li
- class="col-md-6 col-lg-3 gl-mb-3"
+ class="col-md-6 col-lg-4 mb-3"
>
- <design-dropzone-stub
- hasdesigns="true"
- >
+ <design-dropzone-stub>
<design-stub
event="NONE"
filename="design-1-name"
@@ -43,11 +34,9 @@ exports[`Design management index page designs does not render toolbar when there
<!---->
</li>
<li
- class="col-md-6 col-lg-3 gl-mb-3"
+ class="col-md-6 col-lg-4 mb-3"
>
- <design-dropzone-stub
- hasdesigns="true"
- >
+ <design-dropzone-stub>
<design-stub
event="NONE"
filename="design-2-name"
@@ -60,11 +49,9 @@ exports[`Design management index page designs does not render toolbar when there
<!---->
</li>
<li
- class="col-md-6 col-lg-3 gl-mb-3"
+ class="col-md-6 col-lg-4 mb-3"
>
- <design-dropzone-stub
- hasdesigns="true"
- >
+ <design-dropzone-stub>
<design-stub
event="NONE"
filename="design-3-name"
@@ -86,45 +73,30 @@ exports[`Design management index page designs does not render toolbar when there
`;
exports[`Design management index page designs renders designs list and header with upload button 1`] = `
-<div
- class="gl-mt-5 designs-root"
- data-testid="designs-root"
->
+<div>
<header
class="row-content-block border-top-0 p-2 d-flex"
>
<div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-w-full"
+ class="d-flex justify-content-between align-items-center w-100"
>
- <div>
- <span
- class="gl-font-weight-bold gl-mr-3"
- >
- Designs
- </span>
-
- <design-version-dropdown-stub />
- </div>
+ <design-version-dropdown-stub />
<div
- class="qa-selector-toolbar gl-display-flex"
+ class="qa-selector-toolbar d-flex"
>
- <gl-button-stub
- category="tertiary"
- class="gl-mr-2 js-select-all"
- icon=""
- size="small"
+ <gl-deprecated-button-stub
+ class="mr-2 js-select-all"
+ size="md"
variant="link"
>
Select all
-
- </gl-button-stub>
+ </gl-deprecated-button-stub>
<div>
<delete-button-stub
- buttonclass="gl-mr-4"
- buttonsize="small"
- buttonvariant="danger"
+ buttonclass="btn-danger btn-inverted mr-2"
+ buttonvariant=""
>
Delete selected
@@ -144,24 +116,18 @@ exports[`Design management index page designs renders designs list and header wi
<ol
class="list-unstyled row"
>
- <!---->
-
<li
- class="gl-flex-direction-column col-md-6 col-lg-3 gl-mb-3"
- data-testid="design-dropzone-wrapper"
+ class="col-md-6 col-lg-4 mb-3"
>
<design-dropzone-stub
- class="design-list-item design-list-item-new"
- hasdesigns="true"
+ class="design-list-item"
/>
</li>
<li
- class="col-md-6 col-lg-3 gl-mb-3"
+ class="col-md-6 col-lg-4 mb-3"
>
- <design-dropzone-stub
- hasdesigns="true"
- >
+ <design-dropzone-stub>
<design-stub
event="NONE"
filename="design-1-name"
@@ -177,11 +143,9 @@ exports[`Design management index page designs renders designs list and header wi
/>
</li>
<li
- class="col-md-6 col-lg-3 gl-mb-3"
+ class="col-md-6 col-lg-4 mb-3"
>
- <design-dropzone-stub
- hasdesigns="true"
- >
+ <design-dropzone-stub>
<design-stub
event="NONE"
filename="design-2-name"
@@ -197,11 +161,9 @@ exports[`Design management index page designs renders designs list and header wi
/>
</li>
<li
- class="col-md-6 col-lg-3 gl-mb-3"
+ class="col-md-6 col-lg-4 mb-3"
>
- <design-dropzone-stub
- hasdesigns="true"
- >
+ <design-dropzone-stub>
<design-stub
event="NONE"
filename="design-3-name"
@@ -226,10 +188,7 @@ exports[`Design management index page designs renders designs list and header wi
`;
exports[`Design management index page designs renders error 1`] = `
-<div
- class="gl-mt-5"
- data-testid="designs-root"
->
+<div>
<!---->
<div
@@ -257,10 +216,7 @@ exports[`Design management index page designs renders error 1`] = `
`;
exports[`Design management index page designs renders loading icon 1`] = `
-<div
- class="gl-mt-5"
- data-testid="designs-root"
->
+<div>
<!---->
<div
@@ -279,11 +235,8 @@ exports[`Design management index page designs renders loading icon 1`] = `
</div>
`;
-exports[`Design management index page when has no designs renders design dropzone 1`] = `
-<div
- class="gl-mt-5"
- data-testid="designs-root"
->
+exports[`Design management index page when has no designs renders empty text 1`] = `
+<div>
<!---->
<div
@@ -292,18 +245,11 @@ exports[`Design management index page when has no designs renders design dropzon
<ol
class="list-unstyled row"
>
- <span
- class="gl-font-weight-bold gl-font-weight-bold gl-ml-5 gl-mb-4"
- >
- Designs
- </span>
-
<li
- class="col-12"
- data-testid="design-dropzone-wrapper"
+ class="col-md-6 col-lg-4 mb-3"
>
<design-dropzone-stub
- class=""
+ class="design-list-item"
/>
</li>
diff --git a/spec/frontend/design_management_new/pages/design/__snapshots__/index_spec.js.snap b/spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap
index 83bcebd513e..dc5baf37fc6 100644
--- a/spec/frontend/design_management_new/pages/design/__snapshots__/index_spec.js.snap
+++ b/spec/frontend/design_management_legacy/pages/design/__snapshots__/index_spec.js.snap
@@ -10,7 +10,7 @@ exports[`Design management design index page renders design index 1`] = `
<design-destroyer-stub
filenames="test.jpg"
iid="1"
- project-path="project-path"
+ projectpath=""
/>
<!---->
@@ -41,7 +41,7 @@ exports[`Design management design index page renders design index 1`] = `
</h2>
<a
- class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
+ class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
href="full-issue-url"
>
ull-issue-path
@@ -60,13 +60,13 @@ exports[`Design management design index page renders design index 1`] = `
designid="test"
discussion="[object Object]"
discussionwithopenform=""
- markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
+ markdownpreviewpath="//preview_markdown?target_type=Issue"
noteableid="design-id"
/>
<gl-button-stub
- category="tertiary"
- class="link-inherit-color gl-text-black-normal gl-text-decoration-none gl-font-weight-bold gl-mb-4"
+ category="primary"
+ class="link-inherit-color gl-text-body gl-text-decoration-none gl-font-weight-bold gl-mb-4"
data-testid="resolved-comments"
icon="chevron-right"
id="resolved-comments"
@@ -108,7 +108,7 @@ exports[`Design management design index page renders design index 1`] = `
designid="test"
discussion="[object Object]"
discussionwithopenform=""
- markdownpreviewpath="/project-path/preview_markdown?target_type=Issue"
+ markdownpreviewpath="//preview_markdown?target_type=Issue"
noteableid="design-id"
/>
</gl-collapse-stub>
@@ -140,7 +140,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
<design-destroyer-stub
filenames="test.jpg"
iid="1"
- project-path="project-path"
+ projectpath=""
/>
<div
@@ -188,7 +188,7 @@ exports[`Design management design index page with error GlAlert is rendered in c
</h2>
<a
- class="gl-text-gray-600 gl-text-decoration-none gl-mb-6 gl-display-block"
+ class="gl-text-gray-400 gl-text-decoration-none gl-mb-6 gl-display-block"
href="full-issue-url"
>
ull-issue-path
diff --git a/spec/frontend/design_management_new/pages/design/index_spec.js b/spec/frontend/design_management_legacy/pages/design/index_spec.js
index 3822b0b3b71..5eb4158c715 100644
--- a/spec/frontend/design_management_new/pages/design/index_spec.js
+++ b/spec/frontend/design_management_legacy/pages/design/index_spec.js
@@ -2,11 +2,11 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueRouter from 'vue-router';
import { GlAlert } from '@gitlab/ui';
import { ApolloMutation } from 'vue-apollo';
-import createFlash from '~/flash';
-import DesignIndex from '~/design_management_new/pages/design/index.vue';
-import DesignSidebar from '~/design_management_new/components/design_sidebar.vue';
-import DesignPresentation from '~/design_management_new/components/design_presentation.vue';
-import createImageDiffNoteMutation from '~/design_management_new/graphql/mutations/create_image_diff_note.mutation.graphql';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import DesignIndex from '~/design_management_legacy/pages/design/index.vue';
+import DesignSidebar from '~/design_management_legacy/components/design_sidebar.vue';
+import DesignPresentation from '~/design_management_legacy/components/design_presentation.vue';
+import createImageDiffNoteMutation from '~/design_management_legacy/graphql/mutations/create_image_diff_note.mutation.graphql';
import design from '../../mock_data/design';
import mockResponseWithDesigns from '../../mock_data/designs';
import mockResponseNoDesigns from '../../mock_data/no_designs';
@@ -14,11 +14,11 @@ import mockAllVersions from '../../mock_data/all_versions';
import {
DESIGN_NOT_FOUND_ERROR,
DESIGN_VERSION_NOT_EXIST_ERROR,
-} from '~/design_management_new/utils/error_messages';
-import { DESIGNS_ROUTE_NAME } from '~/design_management_new/router/constants';
-import createRouter from '~/design_management_new/router';
-import * as utils from '~/design_management_new/utils/design_management_utils';
-import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management_new/constants';
+} from '~/design_management_legacy/utils/error_messages';
+import { DESIGNS_ROUTE_NAME } from '~/design_management_legacy/router/constants';
+import createRouter from '~/design_management_legacy/router';
+import * as utils from '~/design_management_legacy/utils/design_management_utils';
+import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management_legacy/constants';
jest.mock('~/flash');
jest.mock('mousetrap', () => ({
@@ -95,12 +95,9 @@ describe('Design management design index page', () => {
DesignSidebar,
DesignReplyForm,
},
- provide: {
- issueIid: '1',
- projectPath: 'project-path',
- },
data() {
return {
+ issueIid: '1',
activeDiscussion: {
id: null,
source: null,
@@ -152,7 +149,7 @@ describe('Design management design index page', () => {
expect(findSidebar().props()).toEqual({
design,
- markdownPreviewPath: '/project-path/preview_markdown?target_type=Issue',
+ markdownPreviewPath: '//preview_markdown?target_type=Issue',
resolvedDiscussionsExpanded: false,
});
});
diff --git a/spec/frontend/design_management_new/pages/index_spec.js b/spec/frontend/design_management_legacy/pages/index_spec.js
index 40a462eabb8..5b7512aab7b 100644
--- a/spec/frontend/design_management_new/pages/index_spec.js
+++ b/spec/frontend/design_management_legacy/pages/index_spec.js
@@ -2,20 +2,20 @@ import { createLocalVue, shallowMount } from '@vue/test-utils';
import { ApolloMutation } from 'vue-apollo';
import VueRouter from 'vue-router';
import { GlEmptyState } from '@gitlab/ui';
-import Index from '~/design_management_new/pages/index.vue';
-import uploadDesignQuery from '~/design_management_new/graphql/mutations/upload_design.mutation.graphql';
-import DesignDestroyer from '~/design_management_new/components/design_destroyer.vue';
-import DesignDropzone from '~/design_management_new/components/upload/design_dropzone.vue';
-import DeleteButton from '~/design_management_new/components/delete_button.vue';
-import { DESIGNS_ROUTE_NAME } from '~/design_management_new/router/constants';
+import Index from '~/design_management_legacy/pages/index.vue';
+import uploadDesignQuery from '~/design_management_legacy/graphql/mutations/upload_design.mutation.graphql';
+import DesignDestroyer from '~/design_management_legacy/components/design_destroyer.vue';
+import DesignDropzone from '~/design_management_legacy/components/upload/design_dropzone.vue';
+import DeleteButton from '~/design_management_legacy/components/delete_button.vue';
+import { DESIGNS_ROUTE_NAME } from '~/design_management_legacy/router/constants';
import {
EXISTING_DESIGN_DROP_MANY_FILES_MESSAGE,
EXISTING_DESIGN_DROP_INVALID_FILENAME_MESSAGE,
-} from '~/design_management_new/utils/error_messages';
-import createFlash from '~/flash';
-import createRouter from '~/design_management_new/router';
-import * as utils from '~/design_management_new/utils/design_management_utils';
-import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management_new/constants';
+} from '~/design_management_legacy/utils/error_messages';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import createRouter from '~/design_management_legacy/router';
+import * as utils from '~/design_management_legacy/utils/design_management_utils';
+import { DESIGN_DETAIL_LAYOUT_CLASSLIST } from '~/design_management_legacy/constants';
jest.mock('~/flash.js');
const mockPageEl = {
@@ -68,8 +68,6 @@ describe('Design management index page', () => {
const findToolbar = () => wrapper.find('.qa-selector-toolbar');
const findDeleteButton = () => wrapper.find(DeleteButton);
const findDropzone = () => wrapper.findAll(DesignDropzone).at(0);
- const dropzoneClasses = () => findDropzone().classes();
- const findDropzoneWrapper = () => wrapper.find('[data-testid="design-dropzone-wrapper"]');
const findFirstDropzoneWithDesign = () => wrapper.findAll(DesignDropzone).at(1);
function createComponent({
@@ -94,23 +92,19 @@ describe('Design management index page', () => {
};
wrapper = shallowMount(Index, {
- data() {
- return {
- designs,
- allVersions,
- permissions: {
- createDesign,
- },
- };
- },
mocks: { $apollo },
localVue,
router,
stubs: { DesignDestroyer, ApolloMutation, ...stubs },
attachToDocument: true,
- provide: {
- projectPath: 'project-path',
- issueIid: '1',
+ });
+
+ wrapper.setData({
+ designs,
+ allVersions,
+ issueIid: '1',
+ permissions: {
+ createDesign,
},
});
}
@@ -123,7 +117,9 @@ describe('Design management index page', () => {
it('renders loading icon', () => {
createComponent({ loading: true });
- expect(wrapper.element).toMatchSnapshot();
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
});
it('renders error', () => {
@@ -139,35 +135,25 @@ describe('Design management index page', () => {
it('renders a toolbar with buttons when there are designs', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
- expect(findToolbar().exists()).toBe(true);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findToolbar().exists()).toBe(true);
+ });
});
it('renders designs list and header with upload button', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
- expect(wrapper.element).toMatchSnapshot();
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
});
it('does not render toolbar when there is no permission', () => {
createComponent({ designs: mockDesigns, allVersions: [mockVersion], createDesign: false });
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('has correct classes applied to design dropzone', () => {
- createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
- expect(dropzoneClasses()).toContain('design-list-item');
- expect(dropzoneClasses()).toContain('design-list-item-new');
- });
-
- it('has correct classes applied to dropzone wrapper', () => {
- createComponent({ designs: mockDesigns, allVersions: [mockVersion] });
- expect(findDropzoneWrapper().classes()).toEqual([
- 'gl-flex-direction-column',
- 'col-md-6',
- 'col-lg-3',
- 'gl-mb-3',
- ]);
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
});
});
@@ -176,20 +162,11 @@ describe('Design management index page', () => {
createComponent();
});
- it('renders design dropzone', () =>
+ it('renders empty text', () =>
wrapper.vm.$nextTick().then(() => {
expect(wrapper.element).toMatchSnapshot();
}));
- it('has correct classes applied to design dropzone', () => {
- expect(dropzoneClasses()).not.toContain('design-list-item');
- expect(dropzoneClasses()).not.toContain('design-list-item-new');
- });
-
- it('has correct classes applied to dropzone wrapper', () => {
- expect(findDropzoneWrapper().classes()).toEqual(['col-12']);
- });
-
it('does not render a toolbar with buttons', () =>
wrapper.vm.$nextTick().then(() => {
expect(findToolbar().exists()).toBe(false);
@@ -208,7 +185,7 @@ describe('Design management index page', () => {
mutation: uploadDesignQuery,
variables: {
files: [{ name: 'test' }],
- projectPath: 'project-path',
+ projectPath: '',
iid: '1',
},
optimisticResponse: {
@@ -254,18 +231,12 @@ describe('Design management index page', () => {
},
};
- return wrapper.vm
- .$nextTick()
- .then(() => {
- findDropzone().vm.$emit('change', [{ name: 'test' }]);
- expect(mutate).toHaveBeenCalledWith(mutationVariables);
- expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]);
- expect(wrapper.vm.isSaving).toBeTruthy();
- })
- .then(() => {
- expect(dropzoneClasses()).toContain('design-list-item');
- expect(dropzoneClasses()).toContain('design-list-item-new');
- });
+ return wrapper.vm.$nextTick().then(() => {
+ findDropzone().vm.$emit('change', [{ name: 'test' }]);
+ expect(mutate).toHaveBeenCalledWith(mutationVariables);
+ expect(wrapper.vm.filesToBeSaved).toEqual([{ name: 'test' }]);
+ expect(wrapper.vm.isSaving).toBeTruthy();
+ });
});
it('sets isSaving', () => {
@@ -413,7 +384,8 @@ describe('Design management index page', () => {
it('renders toolbar buttons', () => {
expect(findToolbar().exists()).toBe(true);
- expect(findToolbar().isVisible()).toBe(true);
+ expect(findToolbar().classes()).toContain('d-flex');
+ expect(findToolbar().classes()).not.toContain('d-none');
});
it('adds two designs to selected designs when their checkboxes are checked', () => {
@@ -470,9 +442,9 @@ describe('Design management index page', () => {
});
});
- it('on latest version when has no designs toolbar buttons are invisible', () => {
+ it('on latest version when has no designs does not render toolbar buttons', () => {
createComponent({ designs: [], allVersions: [mockVersion] });
- expect(findToolbar().isVisible()).toBe(false);
+ expect(findToolbar().exists()).toBe(false);
});
describe('on non-latest version', () => {
@@ -563,7 +535,7 @@ describe('Design management index page', () => {
it('ensures fullscreen layout is not applied', () => {
createComponent(true);
- wrapper.vm.$router.push('/');
+ wrapper.vm.$router.push('/designs');
expect(mockPageEl.classList.remove).toHaveBeenCalledTimes(1);
expect(mockPageEl.classList.remove).toHaveBeenCalledWith(...DESIGN_DETAIL_LAYOUT_CLASSLIST);
});
diff --git a/spec/frontend/design_management_new/router_spec.js b/spec/frontend/design_management_legacy/router_spec.js
index 4d63e622724..5f62793a243 100644
--- a/spec/frontend/design_management_new/router_spec.js
+++ b/spec/frontend/design_management_legacy/router_spec.js
@@ -1,11 +1,15 @@
import { mount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import VueRouter from 'vue-router';
-import App from '~/design_management_new/components/app.vue';
-import Designs from '~/design_management_new/pages/index.vue';
-import DesignDetail from '~/design_management_new/pages/design/index.vue';
-import createRouter from '~/design_management_new/router';
-import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management_new/router/constants';
+import App from '~/design_management_legacy/components/app.vue';
+import Designs from '~/design_management_legacy/pages/index.vue';
+import DesignDetail from '~/design_management_legacy/pages/design/index.vue';
+import createRouter from '~/design_management_legacy/router';
+import {
+ ROOT_ROUTE_NAME,
+ DESIGNS_ROUTE_NAME,
+ DESIGN_ROUTE_NAME,
+} from '~/design_management_legacy/router/constants';
import '~/commons/bootstrap';
function factory(routeArg) {
@@ -45,7 +49,7 @@ describe('Design management router', () => {
window.location.hash = '';
});
- describe.each([['/'], [{ name: DESIGNS_ROUTE_NAME }]])('root route', routeArg => {
+ describe.each([['/'], [{ name: ROOT_ROUTE_NAME }]])('root route', routeArg => {
it('pushes home component', () => {
const wrapper = factory(routeArg);
@@ -53,6 +57,14 @@ describe('Design management router', () => {
});
});
+ describe.each([['/designs'], [{ name: DESIGNS_ROUTE_NAME }]])('designs route', routeArg => {
+ it('pushes designs root component', () => {
+ const wrapper = factory(routeArg);
+
+ expect(wrapper.find(Designs).exists()).toBe(true);
+ });
+ });
+
describe.each([['/designs/1'], [{ name: DESIGN_ROUTE_NAME, params: { id: '1' } }]])(
'designs detail route',
routeArg => {
diff --git a/spec/frontend/design_management_new/utils/cache_update_spec.js b/spec/frontend/design_management_legacy/utils/cache_update_spec.js
index 611716d5aa7..dce91b5e59b 100644
--- a/spec/frontend/design_management_new/utils/cache_update_spec.js
+++ b/spec/frontend/design_management_legacy/utils/cache_update_spec.js
@@ -5,15 +5,15 @@ import {
updateStoreAfterAddImageDiffNote,
updateStoreAfterUploadDesign,
updateStoreAfterUpdateImageDiffNote,
-} from '~/design_management_new/utils/cache_update';
+} from '~/design_management_legacy/utils/cache_update';
import {
designDeletionError,
ADD_DISCUSSION_COMMENT_ERROR,
ADD_IMAGE_DIFF_NOTE_ERROR,
UPDATE_IMAGE_DIFF_NOTE_ERROR,
-} from '~/design_management_new/utils/error_messages';
+} from '~/design_management_legacy/utils/error_messages';
import design from '../mock_data/design';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash.js');
diff --git a/spec/frontend/design_management_new/utils/design_management_utils_spec.js b/spec/frontend/design_management_legacy/utils/design_management_utils_spec.js
index 8bc33e214be..97e85a24a35 100644
--- a/spec/frontend/design_management_new/utils/design_management_utils_spec.js
+++ b/spec/frontend/design_management_legacy/utils/design_management_utils_spec.js
@@ -6,7 +6,7 @@ import {
updateImageDiffNoteOptimisticResponse,
isValidDesignFile,
extractDesign,
-} from '~/design_management_new/utils/design_management_utils';
+} from '~/design_management_legacy/utils/design_management_utils';
import mockResponseNoDesigns from '../mock_data/no_designs';
import mockResponseWithDesigns from '../mock_data/designs';
import mockDesign from '../mock_data/design';
diff --git a/spec/frontend/design_management_new/utils/error_messages_spec.js b/spec/frontend/design_management_legacy/utils/error_messages_spec.js
index eb5dc0fad20..489ac23da4e 100644
--- a/spec/frontend/design_management_new/utils/error_messages_spec.js
+++ b/spec/frontend/design_management_legacy/utils/error_messages_spec.js
@@ -1,7 +1,7 @@
import {
designDeletionError,
designUploadSkippedWarning,
-} from '~/design_management_new/utils/error_messages';
+} from '~/design_management_legacy/utils/error_messages';
const mockFilenames = n =>
Array(n)
@@ -55,7 +55,7 @@ describe('Error message', () => {
'Upload skipped. Some of the designs you tried uploading did not change: 1.jpg, 2.jpg, 3.jpg, 4.jpg, 5.jpg, and 2 more.',
],
])('designUploadSkippedWarning', (uploadedFiles, skippedFiles, expected) => {
- test('returns expected warning message', () => {
+ it('returns expected warning message', () => {
expect(designUploadSkippedWarning(uploadedFiles, skippedFiles)).toBe(expected);
});
});
diff --git a/spec/frontend/design_management_new/utils/tracking_spec.js b/spec/frontend/design_management_legacy/utils/tracking_spec.js
index ac7267642cb..a59cf80c906 100644
--- a/spec/frontend/design_management_new/utils/tracking_spec.js
+++ b/spec/frontend/design_management_legacy/utils/tracking_spec.js
@@ -1,5 +1,5 @@
import { mockTracking } from 'helpers/tracking_helper';
-import { trackDesignDetailView } from '~/design_management_new/utils/tracking';
+import { trackDesignDetailView } from '~/design_management_legacy/utils/tracking';
function getTrackingSpy(key) {
return mockTracking(key, undefined, jest.spyOn);
diff --git a/spec/frontend/design_management_new/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management_new/components/list/__snapshots__/item_spec.js.snap
deleted file mode 100644
index 8c6e20cb54c..00000000000
--- a/spec/frontend/design_management_new/components/list/__snapshots__/item_spec.js.snap
+++ /dev/null
@@ -1,472 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management list item component when item appears in view after image is loaded renders media broken icon when image onerror triggered 1`] = `
-<gl-icon-stub
- class="text-secondary"
- name="media-broken"
- size="32"
-/>
-`;
-
-exports[`Design management list item component with no notes renders item with correct status icon for creation event 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <div
- class="design-event position-absolute"
- >
- <span
- aria-label="Added in this version"
- title="Added in this version"
- >
- <icon-stub
- class="text-success-500"
- name="file-addition-solid"
- size="18"
- />
- </span>
- </div>
-
- <gl-intersection-observer-stub
- options="[object Object]"
- >
- <!---->
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <!---->
- </div>
-</router-link-stub>
-`;
-
-exports[`Design management list item component with no notes renders item with correct status icon for deletion event 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <div
- class="design-event position-absolute"
- >
- <span
- aria-label="Deleted in this version"
- title="Deleted in this version"
- >
- <icon-stub
- class="text-danger-500"
- name="file-deletion-solid"
- size="18"
- />
- </span>
- </div>
-
- <gl-intersection-observer-stub
- options="[object Object]"
- >
- <!---->
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <!---->
- </div>
-</router-link-stub>
-`;
-
-exports[`Design management list item component with no notes renders item with correct status icon for modification event 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <div
- class="design-event position-absolute"
- >
- <span
- aria-label="Modified in this version"
- title="Modified in this version"
- >
- <icon-stub
- class="text-primary-500"
- name="file-modified-solid"
- size="18"
- />
- </span>
- </div>
-
- <gl-intersection-observer-stub
- options="[object Object]"
- >
- <!---->
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <!---->
- </div>
-</router-link-stub>
-`;
-
-exports[`Design management list item component with no notes renders item with no status icon for none event 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <!---->
-
- <gl-intersection-observer-stub
- options="[object Object]"
- >
- <!---->
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <!---->
- </div>
-</router-link-stub>
-`;
-
-exports[`Design management list item component with no notes renders loading spinner when isUploading is true 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <!---->
-
- <gl-intersection-observer-stub
- options="[object Object]"
- >
- <gl-loading-icon-stub
- color="orange"
- label="Loading"
- size="md"
- />
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- style="display: none;"
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <!---->
- </div>
-</router-link-stub>
-`;
-
-exports[`Design management list item component with notes renders item with multiple comments 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <!---->
-
- <gl-intersection-observer-stub
- options="[object Object]"
- >
- <!---->
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <div
- class="ml-auto d-flex align-items-center text-secondary"
- >
- <icon-stub
- class="ml-1"
- name="comments"
- size="16"
- />
-
- <span
- aria-label="2 comments"
- class="ml-1"
- >
-
- 2
-
- </span>
- </div>
- </div>
-</router-link-stub>
-`;
-
-exports[`Design management list item component with notes renders item with single comment 1`] = `
-<router-link-stub
- class="card cursor-pointer text-plain js-design-list-item design-list-item design-list-item-new"
- to="[object Object]"
->
- <div
- class="card-body p-0 d-flex-center overflow-hidden position-relative"
- >
- <!---->
-
- <gl-intersection-observer-stub
- options="[object Object]"
- >
- <!---->
-
- <img
- alt="test"
- class="block mx-auto mw-100 mh-100 design-img"
- data-qa-selector="design_image"
- src=""
- />
- </gl-intersection-observer-stub>
- </div>
-
- <div
- class="card-footer d-flex w-100"
- >
- <div
- class="d-flex flex-column str-truncated-100"
- >
- <span
- class="bold str-truncated-100"
- data-qa-selector="design_file_name"
- >
- test
- </span>
-
- <span
- class="str-truncated-100"
- >
-
- Updated
- <timeago-stub
- cssclass=""
- time="01-01-2019"
- tooltipplacement="bottom"
- />
- </span>
- </div>
-
- <div
- class="ml-auto d-flex align-items-center text-secondary"
- >
- <icon-stub
- class="ml-1"
- name="comments"
- size="16"
- />
-
- <span
- aria-label="1 comment"
- class="ml-1"
- >
-
- 1
-
- </span>
- </div>
- </div>
-</router-link-stub>
-`;
diff --git a/spec/frontend/design_management_new/components/toolbar/__snapshots__/pagination_button_spec.js.snap b/spec/frontend/design_management_new/components/toolbar/__snapshots__/pagination_button_spec.js.snap
deleted file mode 100644
index 08662a04f15..00000000000
--- a/spec/frontend/design_management_new/components/toolbar/__snapshots__/pagination_button_spec.js.snap
+++ /dev/null
@@ -1,28 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management pagination button component disables button when no design is passed 1`] = `
-<router-link-stub
- aria-label="Test title"
- class="btn btn-default disabled"
- disabled="true"
- to="[object Object]"
->
- <icon-stub
- name="angle-right"
- size="16"
- />
-</router-link-stub>
-`;
-
-exports[`Design management pagination button component renders router-link 1`] = `
-<router-link-stub
- aria-label="Test title"
- class="btn btn-default"
- to="[object Object]"
->
- <icon-stub
- name="angle-right"
- size="16"
- />
-</router-link-stub>
-`;
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index b7f03f35dfb..ac046ddc203 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -41,6 +41,7 @@ describe('diffs/components/app', () => {
store = createDiffsStore();
store.state.diffs.isLoading = false;
+ store.state.diffs.isTreeLoaded = true;
extendStore(store);
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index 7f69a6344c1..7fdbc791589 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -30,7 +30,7 @@ describe('CompareVersions', () => {
store,
propsData: {
mergeRequestDiffs: diffsMockData,
- diffFilesLength: 0,
+ diffFilesCountText: null,
...props,
},
});
diff --git a/spec/frontend/diffs/components/diff_expansion_cell_spec.js b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
index ef2e0dfe59b..b8aca4ad86b 100644
--- a/spec/frontend/diffs/components/diff_expansion_cell_spec.js
+++ b/spec/frontend/diffs/components/diff_expansion_cell_spec.js
@@ -1,12 +1,12 @@
import Vue from 'vue';
import { cloneDeep } from 'lodash';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { getByText } from '@testing-library/dom';
import { createStore } from '~/mr_notes/stores';
import DiffExpansionCell from '~/diffs/components/diff_expansion_cell.vue';
import { getPreviousLineIndex } from '~/diffs/store/utils';
import { INLINE_DIFF_VIEW_TYPE, PARALLEL_DIFF_VIEW_TYPE } from '~/diffs/constants';
import diffFileMockData from '../mock_data/diff_file';
-import { getByText } from '@testing-library/dom';
const EXPAND_UP_CLASS = '.js-unfold';
const EXPAND_DOWN_CLASS = '.js-unfold-down';
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index 7e154d76f45..ead8bd79cdb 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
-import { createStore } from '~/mr_notes/stores';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
+import { createStore } from '~/mr_notes/stores';
import DiffFileComponent from '~/diffs/components/diff_file.vue';
import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
import diffFileMockDataReadable from '../mock_data/diff_file';
@@ -128,6 +128,26 @@ describe('DiffFile', () => {
});
});
+ it('should auto-expand collapsed files when viewDiffsFileByFile is true', done => {
+ vm.$destroy();
+ window.gon = {
+ features: { autoExpandCollapsedDiffs: true },
+ };
+ vm = createComponentWithStore(Vue.extend(DiffFileComponent), createStore(), {
+ file: JSON.parse(JSON.stringify(diffFileMockDataUnreadable)),
+ canCurrentUserFork: false,
+ viewDiffsFileByFile: true,
+ }).$mount();
+
+ vm.$nextTick(() => {
+ expect(vm.$el.innerText).not.toContain('This diff is collapsed');
+
+ window.gon = {};
+
+ done();
+ });
+ });
+
it('should be collapsed for renamed files', done => {
vm.renderIt = true;
vm.isCollapsed = false;
diff --git a/spec/frontend/diffs/components/diff_stats_spec.js b/spec/frontend/diffs/components/diff_stats_spec.js
index 5956b478019..7a083fb6bde 100644
--- a/spec/frontend/diffs/components/diff_stats_spec.js
+++ b/spec/frontend/diffs/components/diff_stats_spec.js
@@ -2,53 +2,97 @@ import { shallowMount } from '@vue/test-utils';
import DiffStats from '~/diffs/components/diff_stats.vue';
import Icon from '~/vue_shared/components/icon.vue';
+const TEST_ADDED_LINES = 100;
+const TEST_REMOVED_LINES = 200;
+const DIFF_FILES_COUNT = '300';
+const DIFF_FILES_COUNT_TRUNCATED = '300+';
+
describe('diff_stats', () => {
- it('does not render a group if diffFileLengths is empty', () => {
- const wrapper = shallowMount(DiffStats, {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(DiffStats, {
propsData: {
- addedLines: 1,
- removedLines: 2,
+ addedLines: TEST_ADDED_LINES,
+ removedLines: TEST_REMOVED_LINES,
+ ...props,
},
});
- const groups = wrapper.findAll('.diff-stats-group');
+ };
- expect(groups.length).toBe(2);
- });
+ describe('diff stats group', () => {
+ const findDiffStatsGroup = () => wrapper.findAll('.diff-stats-group');
- it('does not render a group if diffFileLengths is not a number', () => {
- const wrapper = shallowMount(DiffStats, {
- propsData: {
- addedLines: 1,
- removedLines: 2,
- diffFilesLength: Number.NaN,
- },
+ it('is not rendered if diffFilesCountText is empty', () => {
+ createComponent();
+
+ expect(findDiffStatsGroup().length).toBe(2);
});
- const groups = wrapper.findAll('.diff-stats-group');
- expect(groups.length).toBe(2);
- });
+ it('is not rendered if diffFilesCountText is not a number', () => {
+ createComponent({
+ diffFilesCountText: null,
+ });
- it('shows amount of files changed, lines added and lines removed when passed all props', () => {
- const wrapper = shallowMount(DiffStats, {
- propsData: {
- addedLines: 100,
- removedLines: 200,
- diffFilesLength: 300,
- },
+ expect(findDiffStatsGroup().length).toBe(2);
});
+ });
+ describe('line changes', () => {
const findFileLine = name => wrapper.find(name);
+
+ it('shows the amount of lines added', () => {
+ expect(findFileLine('.js-file-addition-line').text()).toBe(TEST_ADDED_LINES.toString());
+ });
+
+ it('shows the amount of lines removed', () => {
+ expect(findFileLine('.js-file-deletion-line').text()).toBe(TEST_REMOVED_LINES.toString());
+ });
+ });
+
+ describe('files changes', () => {
const findIcon = name =>
wrapper
.findAll(Icon)
.filter(c => c.attributes('name') === name)
.at(0).element.parentNode;
- const additions = findFileLine('.js-file-addition-line');
- const deletions = findFileLine('.js-file-deletion-line');
- const filesChanged = findIcon('doc-code');
- expect(additions.text()).toBe('100');
- expect(deletions.text()).toBe('200');
- expect(filesChanged.textContent).toContain('300');
+ it('shows amount of file changed with plural "files" when 0 files has changed', () => {
+ const oneFileChanged = '0';
+
+ createComponent({
+ diffFilesCountText: oneFileChanged,
+ });
+
+ expect(findIcon('doc-code').textContent.trim()).toBe(`${oneFileChanged} files`);
+ });
+
+ it('shows amount of file changed with singular "file" when 1 file is changed', () => {
+ const oneFileChanged = '1';
+
+ createComponent({
+ diffFilesCountText: oneFileChanged,
+ });
+
+ expect(findIcon('doc-code').textContent.trim()).toBe(`${oneFileChanged} file`);
+ });
+
+ it('shows amount of files change with plural "files" when multiple files are changed', () => {
+ createComponent({
+ diffFilesCountText: DIFF_FILES_COUNT,
+ });
+
+ expect(findIcon('doc-code').textContent.trim()).toContain(`${DIFF_FILES_COUNT} files`);
+ });
+
+ it('shows amount of files change with plural "files" when files changed is truncated', () => {
+ createComponent({
+ diffFilesCountText: DIFF_FILES_COUNT_TRUNCATED,
+ });
+
+ expect(findIcon('doc-code').textContent.trim()).toContain(
+ `${DIFF_FILES_COUNT_TRUNCATED} files`,
+ );
+ });
});
});
diff --git a/spec/frontend/diffs/components/diff_table_cell_spec.js b/spec/frontend/diffs/components/diff_table_cell_spec.js
index 9693fe68b57..02f5c27eecb 100644
--- a/spec/frontend/diffs/components/diff_table_cell_spec.js
+++ b/spec/frontend/diffs/components/diff_table_cell_spec.js
@@ -1,10 +1,10 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
+import { TEST_HOST } from 'helpers/test_constants';
import DiffTableCell from '~/diffs/components/diff_table_cell.vue';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
import { LINE_POSITION_RIGHT } from '~/diffs/constants';
import { createStore } from '~/mr_notes/stores';
-import { TEST_HOST } from 'helpers/test_constants';
import discussionsMockData from '../mock_data/diff_discussions';
import diffFileMockData from '../mock_data/diff_file';
@@ -18,6 +18,12 @@ const TEST_LINE_CODE = 'LC_42';
const TEST_FILE_HASH = diffFileMockData.file_hash;
describe('DiffTableCell', () => {
+ const symlinkishFileTooltip =
+ 'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
+ const realishFileTooltip =
+ 'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
+ const otherFileTooltip = 'Add a comment to this line';
+
let wrapper;
let line;
let store;
@@ -67,6 +73,7 @@ describe('DiffTableCell', () => {
const findTd = () => wrapper.find({ ref: 'td' });
const findNoteButton = () => wrapper.find({ ref: 'addDiffNoteButton' });
const findLineNumber = () => wrapper.find({ ref: 'lineNumberRef' });
+ const findTooltip = () => wrapper.find({ ref: 'addNoteTooltip' });
const findAvatars = () => wrapper.find(DiffGutterAvatars);
describe('td', () => {
@@ -134,6 +141,53 @@ describe('DiffTableCell', () => {
});
},
);
+
+ it.each`
+ disabled | commentsDisabled
+ ${'disabled'} | ${true}
+ ${undefined} | ${false}
+ `(
+ 'has attribute disabled=$disabled when the outer component has prop commentsDisabled=$commentsDisabled',
+ ({ disabled, commentsDisabled }) => {
+ line.commentsDisabled = commentsDisabled;
+
+ createComponent({
+ showCommentButton: true,
+ isHover: true,
+ });
+
+ wrapper.setData({ isCommentButtonRendered: true });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findNoteButton().attributes('disabled')).toBe(disabled);
+ });
+ },
+ );
+
+ it.each`
+ tooltip | commentsDisabled
+ ${symlinkishFileTooltip} | ${{ wasSymbolic: true }}
+ ${symlinkishFileTooltip} | ${{ isSymbolic: true }}
+ ${realishFileTooltip} | ${{ wasReal: true }}
+ ${realishFileTooltip} | ${{ isReal: true }}
+ ${otherFileTooltip} | ${false}
+ `(
+ 'has the correct tooltip when commentsDisabled=$commentsDisabled',
+ ({ tooltip, commentsDisabled }) => {
+ line.commentsDisabled = commentsDisabled;
+
+ createComponent({
+ showCommentButton: true,
+ isHover: true,
+ });
+
+ wrapper.setData({ isCommentButtonRendered: true });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findTooltip().attributes('title')).toBe(tooltip);
+ });
+ },
+ );
});
describe('line number', () => {
diff --git a/spec/frontend/diffs/components/inline_diff_view_spec.js b/spec/frontend/diffs/components/inline_diff_view_spec.js
index eeef8e5a7b0..6c37f86658e 100644
--- a/spec/frontend/diffs/components/inline_diff_view_spec.js
+++ b/spec/frontend/diffs/components/inline_diff_view_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import '~/behaviors/markdown/render_gfm';
-import { createStore } from '~/mr_notes/stores';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { createStore } from '~/mr_notes/stores';
import InlineDiffView from '~/diffs/components/inline_diff_view.vue';
import diffFileMockData from '../mock_data/diff_file';
import discussionsMockData from '../mock_data/diff_discussions';
diff --git a/spec/frontend/diffs/components/no_changes_spec.js b/spec/frontend/diffs/components/no_changes_spec.js
index 2eca97a47fd..2795c68b4ee 100644
--- a/spec/frontend/diffs/components/no_changes_spec.js
+++ b/spec/frontend/diffs/components/no_changes_spec.js
@@ -1,8 +1,8 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
+import { GlButton } from '@gitlab/ui';
import { createStore } from '~/mr_notes/stores';
import NoChanges from '~/diffs/components/no_changes.vue';
-import { GlButton } from '@gitlab/ui';
describe('Diff no changes empty state', () => {
let vm;
diff --git a/spec/frontend/diffs/components/parallel_diff_view_spec.js b/spec/frontend/diffs/components/parallel_diff_view_spec.js
index 30231f0ba71..cb1a47f60d5 100644
--- a/spec/frontend/diffs/components/parallel_diff_view_spec.js
+++ b/spec/frontend/diffs/components/parallel_diff_view_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import { createStore } from '~/mr_notes/stores';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { createStore } from '~/mr_notes/stores';
import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
import * as constants from '~/diffs/constants';
import diffFileMockData from '../mock_data/diff_file';
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index f78c5f25ee7..14cb2a17aec 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -17,6 +17,7 @@ describe('Diffs tree list component', () => {
});
// Setup initial state
+ store.state.diffs.isTreeLoaded = true;
store.state.diffs.diffFiles.push('test');
store.state.diffs = {
addedLines: 10,
diff --git a/spec/frontend/diffs/diff_file_spec.js b/spec/frontend/diffs/diff_file_spec.js
new file mode 100644
index 00000000000..5d74760ef66
--- /dev/null
+++ b/spec/frontend/diffs/diff_file_spec.js
@@ -0,0 +1,60 @@
+import { prepareRawDiffFile } from '~/diffs/diff_file';
+
+const DIFF_FILES = [
+ {
+ file_hash: 'ABC', // This file is just a normal file
+ },
+ {
+ file_hash: 'DEF', // This file replaces a symlink
+ a_mode: '0',
+ b_mode: '0755',
+ },
+ {
+ file_hash: 'DEF', // This symlink is replaced by a file
+ a_mode: '120000',
+ b_mode: '0',
+ },
+ {
+ file_hash: 'GHI', // This symlink replaces a file
+ a_mode: '0',
+ b_mode: '120000',
+ },
+ {
+ file_hash: 'GHI', // This file is replaced by a symlink
+ a_mode: '0755',
+ b_mode: '0',
+ },
+];
+
+function makeBrokenSymlinkObject(replaced, wasSymbolic, isSymbolic, wasReal, isReal) {
+ return {
+ replaced,
+ wasSymbolic,
+ isSymbolic,
+ wasReal,
+ isReal,
+ };
+}
+
+describe('diff_file utilities', () => {
+ describe('prepareRawDiffFile', () => {
+ it.each`
+ fileIndex | description | brokenSymlink
+ ${0} | ${'a file that is not symlink-adjacent'} | ${false}
+ ${1} | ${'a file that replaces a symlink'} | ${makeBrokenSymlinkObject(false, false, false, false, true)}
+ ${2} | ${'a symlink that is replaced by a file'} | ${makeBrokenSymlinkObject(true, true, false, false, false)}
+ ${3} | ${'a symlink that replaces a file'} | ${makeBrokenSymlinkObject(false, false, true, false, false)}
+ ${4} | ${'a file that is replaced by a symlink'} | ${makeBrokenSymlinkObject(true, false, false, true, false)}
+ `(
+ 'properly marks $description with the correct .brokenSymlink value',
+ ({ fileIndex, brokenSymlink }) => {
+ const preppedRaw = prepareRawDiffFile({
+ file: DIFF_FILES[fileIndex],
+ allFiles: DIFF_FILES,
+ });
+
+ expect(preppedRaw.brokenSymlink).toStrictEqual(brokenSymlink);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index fc5e39357ca..5fef35d6c5b 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -1,6 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
import Cookies from 'js-cookie';
import mockDiffFile from 'jest/diffs/mock_data/diff_file';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { TEST_HOST } from 'jest/helpers/test_constants';
import {
DIFF_VIEW_COOKIE_NAME,
INLINE_DIFF_VIEW_TYPE,
@@ -56,12 +58,10 @@ import testAction from '../../helpers/vuex_action_helper';
import * as utils from '~/diffs/store/utils';
import * as commonUtils from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { diffMetadata } from '../mock_data/diff_metadata';
-import createFlash from '~/flash';
-import { TEST_HOST } from 'jest/helpers/test_constants';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
-jest.mock('~/flash', () => jest.fn());
+jest.mock('~/flash');
describe('DiffsStoreActions', () => {
useLocalStorageSpy();
@@ -1594,24 +1594,39 @@ describe('DiffsStoreActions', () => {
describe('setCurrentDiffFileIdFromNote', () => {
it('commits UPDATE_CURRENT_DIFF_FILE_ID', () => {
const commit = jest.fn();
+ const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
getDiscussion: () => ({ diff_file: { file_hash: '123' } }),
notesById: { '1': { discussion_id: '2' } },
};
- setCurrentDiffFileIdFromNote({ commit, rootGetters }, '1');
+ setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
expect(commit).toHaveBeenCalledWith(types.UPDATE_CURRENT_DIFF_FILE_ID, '123');
});
it('does not commit UPDATE_CURRENT_DIFF_FILE_ID when discussion has no diff_file', () => {
const commit = jest.fn();
+ const state = { diffFiles: [{ file_hash: '123' }] };
const rootGetters = {
getDiscussion: () => ({ id: '1' }),
notesById: { '1': { discussion_id: '2' } },
};
- setCurrentDiffFileIdFromNote({ commit, rootGetters }, '1');
+ setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
+
+ expect(commit).not.toHaveBeenCalled();
+ });
+
+ it('does not commit UPDATE_CURRENT_DIFF_FILE_ID when diff file does not exist', () => {
+ const commit = jest.fn();
+ const state = { diffFiles: [{ file_hash: '123' }] };
+ const rootGetters = {
+ getDiscussion: () => ({ diff_file: { file_hash: '124' } }),
+ notesById: { '1': { discussion_id: '2' } },
+ };
+
+ setCurrentDiffFileIdFromNote({ commit, state, rootGetters }, '1');
expect(commit).not.toHaveBeenCalled();
});
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index c24d406fef3..70047899612 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -830,6 +830,7 @@ describe('DiffsStoreMutations', () => {
const state = {
treeEntries: {},
tree: [],
+ isTreeLoaded: false,
};
mutations[types.SET_TREE_DATA](state, {
@@ -844,6 +845,7 @@ describe('DiffsStoreMutations', () => {
});
expect(state.tree).toEqual(['tree']);
+ expect(state.isTreeLoaded).toEqual(true);
});
});
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index d87619e1e3c..62c82468ea0 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -20,6 +20,14 @@ import { noteableDataMock } from '../../notes/mock_data';
const getDiffFileMock = () => JSON.parse(JSON.stringify(diffFileMockData));
const getDiffMetadataMock = () => JSON.parse(JSON.stringify(diffMetadata));
+function extractLinesFromFile(file) {
+ const unpackedParallel = file.parallel_diff_lines
+ .flatMap(({ left, right }) => [left, right])
+ .filter(Boolean);
+
+ return [...file.highlighted_diff_lines, ...unpackedParallel];
+}
+
describe('DiffsStoreUtils', () => {
describe('findDiffFile', () => {
const files = [{ file_hash: 1, name: 'one' }];
@@ -429,6 +437,28 @@ describe('DiffsStoreUtils', () => {
expect(preppedLine.right).toEqual(correctLine);
expect(preppedLine.line_code).toEqual(correctLine.line_code);
});
+
+ it.each`
+ brokenSymlink
+ ${false}
+ ${{}}
+ ${'anything except `false`'}
+ `(
+ "properly assigns each line's `commentsDisabled` as the same value as the parent file's `brokenSymlink` value (`$brokenSymlink`)",
+ ({ brokenSymlink }) => {
+ preppedLine = utils.prepareLineForRenamedFile({
+ diffViewType: INLINE_DIFF_VIEW_TYPE,
+ line: sourceLine,
+ index: lineIndex,
+ diffFile: {
+ ...diffFile,
+ brokenSymlink,
+ },
+ });
+
+ expect(preppedLine.commentsDisabled).toStrictEqual(brokenSymlink);
+ },
+ );
});
describe('prepareDiffData', () => {
@@ -541,6 +571,25 @@ describe('DiffsStoreUtils', () => {
}),
]);
});
+
+ it('adds the `.brokenSymlink` property to each diff file', () => {
+ preparedDiff.diff_files.forEach(file => {
+ expect(file).toEqual(expect.objectContaining({ brokenSymlink: false }));
+ });
+ });
+
+ it("copies the diff file's `.brokenSymlink` value to each of that file's child lines", () => {
+ const lines = [
+ ...preparedDiff.diff_files,
+ ...splitInlineDiff.diff_files,
+ ...splitParallelDiff.diff_files,
+ ...completedDiff.diff_files,
+ ].flatMap(file => extractLinesFromFile(file));
+
+ lines.forEach(line => {
+ expect(line.commentsDisabled).toBe(false);
+ });
+ });
});
describe('for diff metadata', () => {
@@ -603,6 +652,12 @@ describe('DiffsStoreUtils', () => {
},
]);
});
+
+ it('adds the `.brokenSymlink` property to each meta diff file', () => {
+ preparedDiffFiles.forEach(file => {
+ expect(file).toMatchObject({ brokenSymlink: false });
+ });
+ });
});
});
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index 688b9164e5f..4cfc6478bd2 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
import mock from 'xhr-mock';
import { TEST_HOST } from 'spec/test_constants';
+import waitForPromises from 'helpers/wait_for_promises';
import dropzoneInput from '~/dropzone_input';
import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
-import waitForPromises from 'helpers/wait_for_promises';
const TEST_FILE = new File([], 'somefile.jpg');
TEST_FILE.upload = {};
diff --git a/spec/frontend/editor/editor_lite_spec.js b/spec/frontend/editor/editor_lite_spec.js
index 92a136835bf..e4edeab172b 100644
--- a/spec/frontend/editor/editor_lite_spec.js
+++ b/spec/frontend/editor/editor_lite_spec.js
@@ -2,13 +2,15 @@ import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monac
import Editor from '~/editor/editor_lite';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
+const URI_PREFIX = 'gitlab';
+
describe('Base editor', () => {
let editorEl;
let editor;
const blobContent = 'Foo Bar';
const blobPath = 'test.md';
- const uri = new Uri('gitlab', false, blobPath);
- const fakeModel = { foo: 'bar' };
+ const blobGlobalId = 'snippet_777';
+ const fakeModel = { foo: 'bar', dispose: jest.fn() };
beforeEach(() => {
setFixtures('<div id="editor" data-editor-loading></div>');
@@ -21,6 +23,8 @@ describe('Base editor', () => {
editorEl.remove();
});
+ const createUri = (...paths) => Uri.file([URI_PREFIX, ...paths].join('/'));
+
it('initializes Editor with basic properties', () => {
expect(editor).toBeDefined();
expect(editor.editorEl).toBe(null);
@@ -65,7 +69,7 @@ describe('Base editor', () => {
it('creates model to be supplied to Monaco editor', () => {
editor.createInstance({ el: editorEl, blobPath, blobContent });
- expect(modelSpy).toHaveBeenCalledWith(blobContent, undefined, uri);
+ expect(modelSpy).toHaveBeenCalledWith(blobContent, undefined, createUri(blobPath));
expect(setModel).toHaveBeenCalledWith(fakeModel);
});
@@ -75,6 +79,16 @@ describe('Base editor', () => {
expect(editor.editorEl).not.toBe(null);
expect(instanceSpy).toHaveBeenCalledWith(editorEl, expect.anything());
});
+
+ it('with blobGlobalId, creates model with id in uri', () => {
+ editor.createInstance({ el: editorEl, blobPath, blobContent, blobGlobalId });
+
+ expect(modelSpy).toHaveBeenCalledWith(
+ blobContent,
+ undefined,
+ createUri(blobGlobalId, blobPath),
+ );
+ });
});
describe('implementation', () => {
@@ -82,10 +96,6 @@ describe('Base editor', () => {
editor.createInstance({ el: editorEl, blobPath, blobContent });
});
- afterEach(() => {
- editor.model.dispose();
- });
-
it('correctly proxies value from the model', () => {
expect(editor.getValue()).toEqual(blobContent);
});
@@ -132,10 +142,6 @@ describe('Base editor', () => {
editor.createInstance({ el: editorEl, blobPath, blobContent });
});
- afterEach(() => {
- editor.model.dispose();
- });
-
it('is extensible with the extensions', () => {
expect(editor.foo).toBeUndefined();
diff --git a/spec/frontend/editor/editor_markdown_ext_spec.js b/spec/frontend/editor/editor_markdown_ext_spec.js
index aad2400c0f0..b0fabad8542 100644
--- a/spec/frontend/editor/editor_markdown_ext_spec.js
+++ b/spec/frontend/editor/editor_markdown_ext_spec.js
@@ -1,5 +1,5 @@
-import EditorLite from '~/editor/editor_lite';
import { Range, Position } from 'monaco-editor';
+import EditorLite from '~/editor/editor_lite';
import EditorMarkdownExtension from '~/editor/editor_markdown_ext';
describe('Markdown Extension for Editor Lite', () => {
diff --git a/spec/frontend/emoji/emoji_spec.js b/spec/frontend/emoji/emoji_spec.js
index c6a15d5976a..9b49c8b8ab5 100644
--- a/spec/frontend/emoji/emoji_spec.js
+++ b/spec/frontend/emoji/emoji_spec.js
@@ -1,4 +1,5 @@
import MockAdapter from 'axios-mock-adapter';
+import { trimText } from 'helpers/text_helper';
import axios from '~/lib/utils/axios_utils';
import { initEmojiMap, glEmojiTag, EMOJI_VERSION } from '~/emoji';
import isEmojiUnicodeSupported, {
@@ -9,7 +10,6 @@ import isEmojiUnicodeSupported, {
isHorceRacingSkinToneComboEmoji,
isPersonZwjEmoji,
} from '~/emoji/support/is_emoji_unicode_supported';
-import { trimText } from 'helpers/text_helper';
const emptySupportMap = {
personZwj: false,
diff --git a/spec/frontend/emoji/support/unicode_support_map_spec.js b/spec/frontend/emoji/support/unicode_support_map_spec.js
index aaee9c30cac..945e804a9fa 100644
--- a/spec/frontend/emoji/support/unicode_support_map_spec.js
+++ b/spec/frontend/emoji/support/unicode_support_map_spec.js
@@ -1,6 +1,6 @@
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import getUnicodeSupportMap from '~/emoji/support/unicode_support_map';
import AccessorUtilities from '~/lib/utils/accessor';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
describe('Unicode Support Map', () => {
useLocalStorageSpy();
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index c9d77a34595..35ca323f5a9 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -50,18 +50,14 @@ class CustomEnvironment extends JSDOMEnvironment {
*/
this.global.fetch = () => {};
- // Not yet supported by JSDOM: https://github.com/jsdom/jsdom/issues/317
- this.global.document.createRange = () => ({
- setStart: () => {},
- setEnd: () => {},
- commonAncestorContainer: {
- nodeName: 'BODY',
- ownerDocument: this.global.document,
- },
- });
-
// Expose the jsdom (created in super class) to the global so that we can call reconfigure({ url: '' }) to properly set `window.location`
- this.global.dom = this.dom;
+ this.global.jsdom = this.dom;
+
+ Object.assign(this.global.performance, {
+ mark: () => null,
+ measure: () => null,
+ getEntriesByName: () => [],
+ });
}
async teardown() {
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index 4c06e19cec0..e7f5ee4bc4d 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -1,9 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
+import { GlLoadingIcon } from '@gitlab/ui';
import eventHub from '~/environments/event_hub';
import EnvironmentActions from '~/environments/components/environment_actions.vue';
import Icon from '~/vue_shared/components/icon.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
describe('EnvironmentActions Component', () => {
let vm;
diff --git a/spec/frontend/environments/environment_external_url_spec.js b/spec/frontend/environments/environment_external_url_spec.js
index 9997ea94941..4c133665979 100644
--- a/spec/frontend/environments/environment_external_url_spec.js
+++ b/spec/frontend/environments/environment_external_url_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import ExternalUrlComp from '~/environments/components/environment_external_url.vue';
describe('External URL Component', () => {
@@ -6,7 +6,7 @@ describe('External URL Component', () => {
const externalUrl = 'https://gitlab.com';
beforeEach(() => {
- wrapper = shallowMount(ExternalUrlComp, { propsData: { externalUrl } });
+ wrapper = mount(ExternalUrlComp, { propsData: { externalUrl } });
});
it('should link to the provided externalUrl prop', () => {
diff --git a/spec/frontend/environments/environment_stop_spec.js b/spec/frontend/environments/environment_stop_spec.js
index f971cf56b65..1865403cdc4 100644
--- a/spec/frontend/environments/environment_stop_spec.js
+++ b/spec/frontend/environments/environment_stop_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
import StopComponent from '~/environments/components/environment_stop.vue';
-import LoadingButton from '~/vue_shared/components/loading_button.vue';
import eventHub from '~/environments/event_hub';
$.fn.tooltip = () => {};
@@ -17,7 +17,7 @@ describe('Stop Component', () => {
});
};
- const findButton = () => wrapper.find(LoadingButton);
+ const findButton = () => wrapper.find(GlButton);
beforeEach(() => {
jest.spyOn(window, 'confirm');
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index c0bf0dca176..d440bf73e15 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -1,6 +1,6 @@
import { mount, shallowMount } from '@vue/test-utils';
-import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import Container from '~/environments/components/container.vue';
import EmptyState from '~/environments/components/empty_state.vue';
import EnvironmentsApp from '~/environments/components/environments_app.vue';
diff --git a/spec/frontend/environments/folder/environments_folder_view_spec.js b/spec/frontend/environments/folder/environments_folder_view_spec.js
index 740225ddd9d..f33c8de0094 100644
--- a/spec/frontend/environments/folder/environments_folder_view_spec.js
+++ b/spec/frontend/environments/folder/environments_folder_view_spec.js
@@ -1,11 +1,11 @@
import { mount } from '@vue/test-utils';
-import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
+import { removeBreakLine, removeWhitespace } from 'helpers/text_helper';
+import { GlPagination } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
import EnvironmentTable from '~/environments/components/environments_table.vue';
import { environmentsList } from '../mock_data';
-import { removeBreakLine, removeWhitespace } from 'helpers/text_helper';
-import { GlPagination } from '@gitlab/ui';
describe('Environments Folder View', () => {
let mock;
diff --git a/spec/frontend/error_tracking/components/error_details_spec.js b/spec/frontend/error_tracking/components/error_details_spec.js
index 6124602e038..ef3eeb8c7e4 100644
--- a/spec/frontend/error_tracking/components/error_details_spec.js
+++ b/spec/frontend/error_tracking/components/error_details_spec.js
@@ -1,7 +1,5 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
-import { __ } from '~/locale';
-import createFlash from '~/flash';
import {
GlButton,
GlLoadingIcon,
@@ -11,6 +9,8 @@ import {
GlAlert,
GlSprintf,
} from '@gitlab/ui';
+import { __ } from '~/locale';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import Stacktrace from '~/error_tracking/components/stacktrace.vue';
import ErrorDetails from '~/error_tracking/components/error_details.vue';
import {
diff --git a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
index 1ea92883e54..b22805f5227 100644
--- a/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_actions_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
describe('Error Tracking Actions', () => {
@@ -20,7 +20,7 @@ describe('Error Tracking Actions', () => {
},
...props,
},
- stubs: { GlDeprecatedButton },
+ stubs: { GlButton },
});
}
@@ -34,7 +34,7 @@ describe('Error Tracking Actions', () => {
}
});
- const findButtons = () => wrapper.findAll(GlDeprecatedButton);
+ const findButtons = () => wrapper.findAll(GlButton);
describe('when error status is unresolved', () => {
it('renders the correct actions buttons to allow ignore and resolve', () => {
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index d88a412fb50..bad70a31599 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -1,6 +1,12 @@
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
-import { GlEmptyState, GlLoadingIcon, GlFormInput, GlPagination, GlDropdown } from '@gitlab/ui';
+import {
+ GlEmptyState,
+ GlLoadingIcon,
+ GlFormInput,
+ GlPagination,
+ GlDeprecatedDropdown,
+} from '@gitlab/ui';
import stubChildren from 'helpers/stub_children';
import ErrorTrackingList from '~/error_tracking/components/error_tracking_list.vue';
import ErrorTrackingActions from '~/error_tracking/components/error_tracking_actions.vue';
@@ -18,19 +24,19 @@ describe('ErrorTrackingList', () => {
const findErrorListTable = () => wrapper.find('table');
const findErrorListRows = () => wrapper.findAll('tbody tr');
- const dropdownsArray = () => wrapper.findAll(GlDropdown);
+ const dropdownsArray = () => wrapper.findAll(GlDeprecatedDropdown);
const findRecentSearchesDropdown = () =>
dropdownsArray()
.at(0)
- .find(GlDropdown);
+ .find(GlDeprecatedDropdown);
const findStatusFilterDropdown = () =>
dropdownsArray()
.at(1)
- .find(GlDropdown);
+ .find(GlDeprecatedDropdown);
const findSortDropdown = () =>
dropdownsArray()
.at(2)
- .find(GlDropdown);
+ .find(GlDeprecatedDropdown);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findPagination = () => wrapper.find(GlPagination);
const findErrorActions = () => wrapper.find(ErrorTrackingActions);
@@ -128,8 +134,8 @@ describe('ErrorTrackingList', () => {
mountComponent({
stubs: {
GlTable: false,
- GlDropdown: false,
- GlDropdownItem: false,
+ GlDeprecatedDropdown: false,
+ GlDeprecatedDropdownItem: false,
GlLink: false,
},
});
@@ -199,8 +205,8 @@ describe('ErrorTrackingList', () => {
mountComponent({
stubs: {
GlTable: false,
- GlDropdown: false,
- GlDropdownItem: false,
+ GlDeprecatedDropdown: false,
+ GlDeprecatedDropdownItem: false,
},
});
});
@@ -335,8 +341,8 @@ describe('ErrorTrackingList', () => {
beforeEach(() => {
mountComponent({
stubs: {
- GlDropdown: false,
- GlDropdownItem: false,
+ GlDeprecatedDropdown: false,
+ GlDeprecatedDropdownItem: false,
},
});
});
diff --git a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
index de746b8ac84..df7bff201f1 100644
--- a/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
+++ b/spec/frontend/error_tracking/components/stacktrace_entry_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
+import { trimText } from 'helpers/text_helper';
import StackTraceEntry from '~/error_tracking/components/stacktrace_entry.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
import Icon from '~/vue_shared/components/icon.vue';
-import { trimText } from 'helpers/text_helper';
describe('Stacktrace Entry', () => {
let wrapper;
diff --git a/spec/frontend/error_tracking/store/actions_spec.js b/spec/frontend/error_tracking/store/actions_spec.js
index e4a895902b3..43037473a61 100644
--- a/spec/frontend/error_tracking/store/actions_spec.js
+++ b/spec/frontend/error_tracking/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import * as actions from '~/error_tracking/store/actions';
import * as types from '~/error_tracking/store/mutation_types';
import { visitUrl } from '~/lib/utils/url_utility';
diff --git a/spec/frontend/error_tracking/store/details/actions_spec.js b/spec/frontend/error_tracking/store/details/actions_spec.js
index 6802300b0f5..58e77c46e02 100644
--- a/spec/frontend/error_tracking/store/details/actions_spec.js
+++ b/spec/frontend/error_tracking/store/details/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import * as actions from '~/error_tracking/store/details/actions';
import * as types from '~/error_tracking/store/details/mutation_types';
import Poll from '~/lib/utils/poll';
diff --git a/spec/frontend/error_tracking/store/list/actions_spec.js b/spec/frontend/error_tracking/store/list/actions_spec.js
index 3cb740bf05d..7326472e1dd 100644
--- a/spec/frontend/error_tracking/store/list/actions_spec.js
+++ b/spec/frontend/error_tracking/store/list/actions_spec.js
@@ -1,8 +1,8 @@
-import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import * as actions from '~/error_tracking/store/list/actions';
import * as types from '~/error_tracking/store/list/mutation_types';
diff --git a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
index d924f895da8..023a3e26781 100644
--- a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
+++ b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
@@ -1,7 +1,7 @@
import { pick, clone } from 'lodash';
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
import ProjectDropdown from '~/error_tracking_settings/components/project_dropdown.vue';
import { defaultProps, projectList, staleProject } from '../mock';
@@ -43,7 +43,7 @@ describe('error tracking settings project dropdown', () => {
describe('empty project list', () => {
it('renders the dropdown', () => {
expect(wrapper.find('#project-dropdown').exists()).toBeTruthy();
- expect(wrapper.find(GlDropdown).exists()).toBeTruthy();
+ expect(wrapper.find(GlDeprecatedDropdown).exists()).toBeTruthy();
});
it('shows helper text', () => {
@@ -58,8 +58,8 @@ describe('error tracking settings project dropdown', () => {
});
it('does not contain any dropdown items', () => {
- expect(wrapper.find(GlDropdownItem).exists()).toBeFalsy();
- expect(wrapper.find(GlDropdown).props('text')).toBe('No projects available');
+ expect(wrapper.find(GlDeprecatedDropdownItem).exists()).toBeFalsy();
+ expect(wrapper.find(GlDeprecatedDropdown).props('text')).toBe('No projects available');
});
});
@@ -72,12 +72,12 @@ describe('error tracking settings project dropdown', () => {
it('renders the dropdown', () => {
expect(wrapper.find('#project-dropdown').exists()).toBeTruthy();
- expect(wrapper.find(GlDropdown).exists()).toBeTruthy();
+ expect(wrapper.find(GlDeprecatedDropdown).exists()).toBeTruthy();
});
it('contains a number of dropdown items', () => {
- expect(wrapper.find(GlDropdownItem).exists()).toBeTruthy();
- expect(wrapper.findAll(GlDropdownItem).length).toBe(2);
+ expect(wrapper.find(GlDeprecatedDropdownItem).exists()).toBeTruthy();
+ expect(wrapper.findAll(GlDeprecatedDropdownItem).length).toBe(2);
});
});
diff --git a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
index c0851096d8e..158f70f7d47 100644
--- a/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/frontend/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -57,7 +57,11 @@ describe('Recent Searches Dropdown Content', () => {
beforeEach(() => {
createComponent({
- items: ['foo', 'author:@root label:~foo bar'],
+ items: [
+ 'foo',
+ 'author:@root label:~foo bar',
+ [{ type: 'author_username', value: { data: 'toby', operator: '=' } }],
+ ],
isLocalStorageAvailable: true,
});
});
@@ -76,7 +80,7 @@ describe('Recent Searches Dropdown Content', () => {
});
it('renders a correct amount of dropdown items', () => {
- expect(findDropdownItems()).toHaveLength(2);
+ expect(findDropdownItems()).toHaveLength(2); // Ignore non-string recent item
});
it('expect second dropdown to have 2 tokens', () => {
diff --git a/spec/frontend/filtered_search/filtered_search_manager_spec.js b/spec/frontend/filtered_search/filtered_search_manager_spec.js
index 70e8b339d4b..53c726a6cea 100644
--- a/spec/frontend/filtered_search/filtered_search_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_manager_spec.js
@@ -77,7 +77,7 @@ describe('Filtered Search Manager', () => {
jest.spyOn(FilteredSearchDropdownManager.prototype, 'setDropdown').mockImplementation();
});
- const initializeManager = () => {
+ const initializeManager = ({ useDefaultState } = {}) => {
jest.spyOn(FilteredSearchManager.prototype, 'loadSearchParamsFromURL').mockImplementation();
jest.spyOn(FilteredSearchManager.prototype, 'tokenChange').mockImplementation();
jest
@@ -88,7 +88,7 @@ describe('Filtered Search Manager', () => {
input = document.querySelector('.filtered-search');
tokensContainer = document.querySelector('.tokens-container');
- manager = new FilteredSearchManager({ page });
+ manager = new FilteredSearchManager({ page, useDefaultState });
manager.setup();
};
@@ -184,17 +184,27 @@ describe('Filtered Search Manager', () => {
});
describe('search', () => {
- const defaultParams = '?scope=all&utf8=%E2%9C%93&state=opened';
+ const defaultParams = '?scope=all&utf8=%E2%9C%93';
+ const defaultState = '&state=opened';
- beforeEach(() => {
+ it('should search with a single word', done => {
initializeManager();
+ input.value = 'searchTerm';
+
+ visitUrl.mockImplementation(url => {
+ expect(url).toEqual(`${defaultParams}&search=searchTerm`);
+ done();
+ });
+
+ manager.search();
});
- it('should search with a single word', done => {
+ it('sets default state', done => {
+ initializeManager({ useDefaultState: true });
input.value = 'searchTerm';
visitUrl.mockImplementation(url => {
- expect(url).toEqual(`${defaultParams}&search=searchTerm`);
+ expect(url).toEqual(`${defaultParams}${defaultState}&search=searchTerm`);
done();
});
@@ -202,6 +212,7 @@ describe('Filtered Search Manager', () => {
});
it('should search with multiple words', done => {
+ initializeManager();
input.value = 'awesome search terms';
visitUrl.mockImplementation(url => {
@@ -213,6 +224,7 @@ describe('Filtered Search Manager', () => {
});
it('should search with special characters', done => {
+ initializeManager();
input.value = '~!@#$%^&*()_+{}:<>,.?/';
visitUrl.mockImplementation(url => {
@@ -225,7 +237,29 @@ describe('Filtered Search Manager', () => {
manager.search();
});
+ it('should use replacement URL for condition', done => {
+ initializeManager();
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', '=', '13', true),
+ );
+
+ visitUrl.mockImplementation(url => {
+ expect(url).toEqual(`${defaultParams}&milestone_title=replaced`);
+ done();
+ });
+
+ manager.filteredSearchTokenKeys.conditions.push({
+ url: 'milestone_title=13',
+ replacementUrl: 'milestone_title=replaced',
+ tokenKey: 'milestone',
+ value: '13',
+ operator: '=',
+ });
+ manager.search();
+ });
+
it('removes duplicated tokens', done => {
+ initializeManager();
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '=', '~bug')}
diff --git a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
index e59ee925cc7..6a00065c9fe 100644
--- a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
@@ -280,8 +280,8 @@ describe('Filtered Search Visual Tokens', () => {
);
});
- it('contains fa-close icon', () => {
- expect(tokenElement.querySelector('.remove-token .fa-close')).toEqual(expect.anything());
+ it('contains close icon', () => {
+ expect(tokenElement.querySelector('.remove-token .close-icon')).toEqual(expect.anything());
});
});
});
diff --git a/spec/frontend/filtered_search/services/recent_searches_service_spec.js b/spec/frontend/filtered_search/services/recent_searches_service_spec.js
index a89d38b7a20..afeca54b949 100644
--- a/spec/frontend/filtered_search/services/recent_searches_service_spec.js
+++ b/spec/frontend/filtered_search/services/recent_searches_service_spec.js
@@ -1,7 +1,7 @@
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import RecentSearchesService from '~/filtered_search/services/recent_searches_service';
import RecentSearchesServiceError from '~/filtered_search/services/recent_searches_service_error';
import AccessorUtilities from '~/lib/utils/accessor';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
useLocalStorageSpy();
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
index 3a64b688c7a..e2855b29b70 100644
--- a/spec/frontend/filtered_search/visual_token_value_spec.js
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -1,10 +1,10 @@
import { escape } from 'lodash';
+import { TEST_HOST } from 'jest/helpers/test_constants';
import VisualTokenValue from '~/filtered_search/visual_token_value';
import AjaxCache from '~/lib/utils/ajax_cache';
import UsersCache from '~/lib/utils/users_cache';
import DropdownUtils from '~/filtered_search//dropdown_utils';
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('Filtered Search Visual Tokens', () => {
const findElements = tokenElement => {
diff --git a/spec/frontend/fixtures/api_merge_requests.rb b/spec/frontend/fixtures/api_merge_requests.rb
new file mode 100644
index 00000000000..f3280e216ff
--- /dev/null
+++ b/spec/frontend/fixtures/api_merge_requests.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::MergeRequests, '(JavaScript fixtures)', type: :request do
+ include ApiHelpers
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin, name: 'root') }
+ let(:namespace) { create(:namespace, name: 'gitlab-test' )}
+ let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
+
+ before(:all) do
+ clean_frontend_fixtures('api/merge_requests')
+ end
+
+ it 'api/merge_requests/get.json' do
+ 4.times { |i| create(:merge_request, source_project: project, source_branch: "branch-#{i}") }
+
+ get api("/projects/#{project.id}/merge_requests", admin)
+
+ expect(response).to be_successful
+ end
+end
diff --git a/spec/frontend/fixtures/api_projects.rb b/spec/frontend/fixtures/api_projects.rb
new file mode 100644
index 00000000000..fa77ca1c0cf
--- /dev/null
+++ b/spec/frontend/fixtures/api_projects.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe API::Projects, '(JavaScript fixtures)', type: :request do
+ include ApiHelpers
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin, name: 'root') }
+ let(:namespace) { create(:namespace, name: 'gitlab-test' )}
+ let(:project) { create(:project, :repository, namespace: namespace, path: 'lorem-ipsum') }
+ let(:project_empty) { create(:project_empty_repo, namespace: namespace, path: 'lorem-ipsum-empty') }
+
+ before(:all) do
+ clean_frontend_fixtures('api/projects')
+ end
+
+ it 'api/projects/get.json' do
+ get api("/projects/#{project.id}", admin)
+
+ expect(response).to be_successful
+ end
+
+ it 'api/projects/get_empty.json' do
+ get api("/projects/#{project_empty.id}", admin)
+
+ expect(response).to be_successful
+ end
+
+ it 'api/projects/branches/get.json' do
+ get api("/projects/#{project.id}/repository/branches/#{project.default_branch}", admin)
+
+ expect(response).to be_successful
+ end
+end
diff --git a/spec/frontend/fixtures/freeze_period.rb b/spec/frontend/fixtures/freeze_period.rb
new file mode 100644
index 00000000000..7695dbc2e8f
--- /dev/null
+++ b/spec/frontend/fixtures/freeze_period.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Freeze Periods (JavaScript fixtures)' do
+ include JavaScriptFixturesHelpers
+ include Ci::PipelineSchedulesHelper
+
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:project) { create(:project, :repository, path: 'freeze-periods-project') }
+
+ before(:all) do
+ clean_frontend_fixtures('api/freeze-periods/')
+ end
+
+ after(:all) do
+ remove_repository(project)
+ end
+
+ describe API::FreezePeriods, '(JavaScript fixtures)', type: :request do
+ include ApiHelpers
+
+ it 'api/freeze-periods/freeze_periods.json' do
+ create(:ci_freeze_period, project: project, freeze_start: '5 4 * * *', freeze_end: '5 9 * 8 *', cron_timezone: 'America/New_York')
+ create(:ci_freeze_period, project: project, freeze_start: '0 12 * * 1-5', freeze_end: '0 1 5 * *', cron_timezone: 'Etc/UTC')
+ create(:ci_freeze_period, project: project, freeze_start: '0 12 * * 1-5', freeze_end: '0 16 * * 6', cron_timezone: 'Europe/Berlin')
+
+ get api("/projects/#{project.id}/freeze_periods", admin)
+
+ expect(response).to be_successful
+ end
+ end
+
+ describe Ci::PipelineSchedulesHelper, '(JavaScript fixtures)' do
+ let(:response) { timezone_data.to_json }
+
+ it 'api/freeze-periods/timezone_data.json' do
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index 7801eb27ce8..6f281b26e6d 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -38,6 +38,7 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
sha: merge_request.diff_head_sha
)
end
+
let(:path) { "files/ruby/popen.rb" }
let(:position) do
build(:text_diff_position, :added,
diff --git a/spec/frontend/fixtures/metrics_dashboard.rb b/spec/frontend/fixtures/metrics_dashboard.rb
index 6ee730f5c3d..eef79825ae7 100644
--- a/spec/frontend/fixtures/metrics_dashboard.rb
+++ b/spec/frontend/fixtures/metrics_dashboard.rb
@@ -8,7 +8,7 @@ RSpec.describe MetricsDashboard, '(JavaScript fixtures)', type: :controller do
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', namespace: namespace) }
+ let_it_be(:project) { project_with_dashboard_namespace('.gitlab/dashboards/test.yml', nil, namespace: namespace) }
let_it_be(:environment) { create(:environment, id: 1, project: project) }
let_it_be(:params) { { environment: environment } }
diff --git a/spec/frontend/fixtures/projects_json.rb b/spec/frontend/fixtures/projects_json.rb
new file mode 100644
index 00000000000..c081d4f08dc
--- /dev/null
+++ b/spec/frontend/fixtures/projects_json.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe 'Projects JSON endpoints (JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin, name: 'root') }
+ let(:project) { create(:project, :repository) }
+
+ before(:all) do
+ clean_frontend_fixtures('projects_json/')
+ end
+
+ before do
+ project.add_maintainer(admin)
+ sign_in(admin)
+ end
+
+ describe Projects::FindFileController, '(JavaScript fixtures)', type: :controller do
+ it 'projects_json/files.json' do
+ get :list,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: project.default_branch
+ },
+ format: 'json'
+
+ expect(response).to be_successful
+ end
+ end
+
+ describe Projects::CommitController, '(JavaScript fixtures)', type: :controller do
+ it 'projects_json/pipelines_empty.json' do
+ get :pipelines,
+ params: {
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: project.commit(project.default_branch).id,
+ format: 'json'
+ }
+
+ expect(response).to be_successful
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/test_report.rb b/spec/frontend/fixtures/test_report.rb
index 16496aa901b..3d09078ba68 100644
--- a/spec/frontend/fixtures/test_report.rb
+++ b/spec/frontend/fixtures/test_report.rb
@@ -15,7 +15,6 @@ RSpec.describe Projects::PipelinesController, "(JavaScript fixtures)", type: :co
before do
sign_in(user)
- stub_feature_flags(junit_pipeline_view: project)
end
it "pipelines/test_report.json" do
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index fa7c1904339..a37d57b03fd 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -1,4 +1,10 @@
-import flash, { createFlashEl, createAction, hideFlash, removeFlashClickListener } from '~/flash';
+import createFlash, {
+ deprecatedCreateFlash,
+ createFlashEl,
+ createAction,
+ hideFlash,
+ removeFlashClickListener,
+} from '~/flash';
describe('Flash', () => {
describe('createFlashEl', () => {
@@ -119,10 +125,10 @@ describe('Flash', () => {
});
});
- describe('createFlash', () => {
+ describe('deprecatedCreateFlash', () => {
describe('no flash-container', () => {
it('does not add to the DOM', () => {
- const flashEl = flash('testing');
+ const flashEl = deprecatedCreateFlash('testing');
expect(flashEl).toBeNull();
@@ -144,7 +150,7 @@ describe('Flash', () => {
});
it('adds flash element into container', () => {
- flash('test', 'alert', document, null, false, true);
+ deprecatedCreateFlash('test', 'alert', document, null, false, true);
expect(document.querySelector('.flash-alert')).not.toBeNull();
@@ -152,26 +158,26 @@ describe('Flash', () => {
});
it('adds flash into specified parent', () => {
- flash('test', 'alert', document.querySelector('.content-wrapper'));
+ deprecatedCreateFlash('test', 'alert', document.querySelector('.content-wrapper'));
expect(document.querySelector('.content-wrapper .flash-alert')).not.toBeNull();
});
it('adds container classes when inside content-wrapper', () => {
- flash('test');
+ deprecatedCreateFlash('test');
expect(document.querySelector('.flash-text').className).toBe('flash-text');
});
it('does not add container when outside of content-wrapper', () => {
document.querySelector('.content-wrapper').className = 'js-content-wrapper';
- flash('test');
+ deprecatedCreateFlash('test');
expect(document.querySelector('.flash-text').className.trim()).toContain('flash-text');
});
it('removes element after clicking', () => {
- flash('test', 'alert', document, null, false, true);
+ deprecatedCreateFlash('test', 'alert', document, null, false, true);
document.querySelector('.flash-alert .js-close-icon').click();
@@ -182,8 +188,111 @@ describe('Flash', () => {
describe('with actionConfig', () => {
it('adds action link', () => {
- flash('test', 'alert', document, {
+ deprecatedCreateFlash('test', 'alert', document, {
+ title: 'test',
+ });
+
+ expect(document.querySelector('.flash-action')).not.toBeNull();
+ });
+
+ it('calls actionConfig clickHandler on click', () => {
+ const actionConfig = {
title: 'test',
+ clickHandler: jest.fn(),
+ };
+
+ deprecatedCreateFlash('test', 'alert', document, actionConfig);
+
+ document.querySelector('.flash-action').click();
+
+ expect(actionConfig.clickHandler).toHaveBeenCalled();
+ });
+ });
+ });
+ });
+
+ describe('createFlash', () => {
+ const message = 'test';
+ const type = 'alert';
+ const parent = document;
+ const fadeTransition = false;
+ const addBodyClass = true;
+ const defaultParams = {
+ message,
+ type,
+ parent,
+ actionConfig: null,
+ fadeTransition,
+ addBodyClass,
+ };
+
+ describe('no flash-container', () => {
+ it('does not add to the DOM', () => {
+ const flashEl = createFlash({ message });
+
+ expect(flashEl).toBeNull();
+
+ expect(document.querySelector('.flash-alert')).toBeNull();
+ });
+ });
+
+ describe('with flash-container', () => {
+ beforeEach(() => {
+ setFixtures(
+ '<div class="content-wrapper js-content-wrapper"><div class="flash-container"></div></div>',
+ );
+ });
+
+ afterEach(() => {
+ document.querySelector('.js-content-wrapper').remove();
+ });
+
+ it('adds flash element into container', () => {
+ createFlash({ ...defaultParams });
+
+ expect(document.querySelector('.flash-alert')).not.toBeNull();
+
+ expect(document.body.className).toContain('flash-shown');
+ });
+
+ it('adds flash into specified parent', () => {
+ createFlash({ ...defaultParams, parent: document.querySelector('.content-wrapper') });
+
+ expect(document.querySelector('.content-wrapper .flash-alert')).not.toBeNull();
+ expect(document.querySelector('.content-wrapper').innerText.trim()).toEqual(message);
+ });
+
+ it('adds container classes when inside content-wrapper', () => {
+ createFlash(defaultParams);
+
+ expect(document.querySelector('.flash-text').className).toBe('flash-text');
+ expect(document.querySelector('.content-wrapper').innerText.trim()).toEqual(message);
+ });
+
+ it('does not add container when outside of content-wrapper', () => {
+ document.querySelector('.content-wrapper').className = 'js-content-wrapper';
+ createFlash(defaultParams);
+
+ expect(document.querySelector('.flash-text').className.trim()).toContain('flash-text');
+ });
+
+ it('removes element after clicking', () => {
+ createFlash({ ...defaultParams });
+
+ document.querySelector('.flash-alert .js-close-icon').click();
+
+ expect(document.querySelector('.flash-alert')).toBeNull();
+
+ expect(document.body.className).not.toContain('flash-shown');
+ });
+
+ describe('with actionConfig', () => {
+ it('adds action link', () => {
+ createFlash({
+ ...defaultParams,
+ actionConfig: {
+ title: 'test',
+ },
});
expect(document.querySelector('.flash-action')).not.toBeNull();
@@ -195,7 +304,7 @@ describe('Flash', () => {
clickHandler: jest.fn(),
};
- flash('test', 'alert', document, actionConfig);
+ createFlash({ ...defaultParams, actionConfig });
document.querySelector('.flash-action').click();
diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js
index 7c54a48aa41..b4f36b82385 100644
--- a/spec/frontend/frequent_items/components/app_spec.js
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -1,6 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import appComponent from '~/frequent_items/components/app.vue';
import eventHub from '~/frequent_items/event_hub';
@@ -8,8 +10,6 @@ import store from '~/frequent_items/store';
import { FREQUENT_ITEMS, HOUR_IN_MS } from '~/frequent_items/constants';
import { getTopFrequentItems } from '~/frequent_items/utils';
import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import waitForPromises from 'helpers/wait_for_promises';
useLocalStorageSpy();
diff --git a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
index 1595f6c9fff..0e16b726c4b 100644
--- a/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
+++ b/spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap
@@ -12,17 +12,19 @@ exports[`grafana integration component default state to match the default snapsh
class="js-section-header h4"
>
- Grafana Authentication
+ Grafana authentication
</h3>
- <gl-deprecated-button-stub
+ <gl-button-stub
+ category="primary"
class="js-settings-toggle"
- size="md"
- variant="secondary"
+ icon=""
+ size="medium"
+ variant="default"
>
Expand
- </gl-deprecated-button-stub>
+ </gl-button-stub>
<p
class="js-section-sub-header"
@@ -90,14 +92,20 @@ exports[`grafana integration component default state to match the default snapsh
</p>
</gl-form-group-stub>
- <gl-deprecated-button-stub
- size="md"
- variant="success"
+ <div
+ class="gl-display-flex gl-justify-content-end"
>
+ <gl-button-stub
+ category="primary"
+ icon=""
+ size="medium"
+ variant="success"
+ >
+
+ Save Changes
- Save Changes
-
- </gl-deprecated-button-stub>
+ </gl-button-stub>
+ </div>
</form>
</div>
</section>
diff --git a/spec/frontend/grafana_integration/components/grafana_integration_spec.js b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
index 3df200a98e4..df88a336c09 100644
--- a/spec/frontend/grafana_integration/components/grafana_integration_spec.js
+++ b/spec/frontend/grafana_integration/components/grafana_integration_spec.js
@@ -1,11 +1,11 @@
import { mount, shallowMount } from '@vue/test-utils';
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import GrafanaIntegration from '~/grafana_integration/components/grafana_integration.vue';
import { createStore } from '~/grafana_integration/store';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/flash');
@@ -44,14 +44,14 @@ describe('grafana integration component', () => {
it('renders header text', () => {
wrapper = shallowMount(GrafanaIntegration, { store });
- expect(wrapper.find('.js-section-header').text()).toBe('Grafana Authentication');
+ expect(wrapper.find('.js-section-header').text()).toBe('Grafana authentication');
});
describe('expand/collapse button', () => {
it('renders as an expand button by default', () => {
wrapper = shallowMount(GrafanaIntegration, { store });
- const button = wrapper.find(GlDeprecatedButton);
+ const button = wrapper.find(GlButton);
expect(button.text()).toBe('Expand');
});
@@ -77,8 +77,7 @@ describe('grafana integration component', () => {
});
describe('submit button', () => {
- const findSubmitButton = () =>
- wrapper.find('.settings-content form').find(GlDeprecatedButton);
+ const findSubmitButton = () => wrapper.find('.settings-content form').find(GlButton);
const endpointRequest = [
operationsSettingsEndpoint,
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 35eda21e047..5d34bc48ed5 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -2,8 +2,8 @@ import '~/flash';
import $ from 'jquery';
import Vue from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
import appComponent from '~/groups/components/app.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js
index 467d9678f69..59a8ca2ed23 100644
--- a/spec/frontend/header_spec.js
+++ b/spec/frontend/header_spec.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import initTodoToggle, { initNavUserDropdownTracking } from '~/header';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+import initTodoToggle, { initNavUserDropdownTracking } from '~/header';
describe('Header', () => {
describe('Todos notification', () => {
diff --git a/spec/frontend/helpers/backoff_helper.js b/spec/frontend/helpers/backoff_helper.js
new file mode 100644
index 00000000000..e5c0308d3fb
--- /dev/null
+++ b/spec/frontend/helpers/backoff_helper.js
@@ -0,0 +1,33 @@
+/**
+ * A mock version of a commonUtils `backOff` to test multiple
+ * retries.
+ *
+ * Usage:
+ *
+ * ```
+ * import * as commonUtils from '~/lib/utils/common_utils';
+ * import { backoffMockImplementation } from '../../helpers/backoff_helper';
+ *
+ * beforeEach(() => {
+ * // ...
+ * jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation);
+ * });
+ * ```
+ *
+ * @param {Function} callback
+ */
+export const backoffMockImplementation = callback => {
+ const q = new Promise((resolve, reject) => {
+ const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
+ const next = () => callback(next, stop);
+ // Define a timeout based on a mock timer
+ setTimeout(() => {
+ callback(next, stop);
+ });
+ });
+ // Run all resolved promises in chain
+ jest.runOnlyPendingTimers();
+ return q;
+};
+
+export default { backoffMockImplementation };
diff --git a/spec/frontend/helpers/dom_events_helper.js b/spec/frontend/helpers/dom_events_helper.js
index b66c12daf4f..139e0813397 100644
--- a/spec/frontend/helpers/dom_events_helper.js
+++ b/spec/frontend/helpers/dom_events_helper.js
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/prefer-default-export
export const triggerDOMEvent = type => {
window.document.dispatchEvent(
new Event(type, {
@@ -6,5 +7,3 @@ export const triggerDOMEvent = type => {
}),
);
};
-
-export default () => {};
diff --git a/spec/frontend/helpers/dom_shims/index.js b/spec/frontend/helpers/dom_shims/index.js
index d18bb94c107..2ba5701fc77 100644
--- a/spec/frontend/helpers/dom_shims/index.js
+++ b/spec/frontend/helpers/dom_shims/index.js
@@ -4,7 +4,7 @@ import './element_scroll_to';
import './form_element';
import './get_client_rects';
import './inner_text';
-import './mutation_observer';
+import './range';
import './window_scroll_to';
import './scroll_by';
import './size_properties';
diff --git a/spec/frontend/helpers/dom_shims/mutation_observer.js b/spec/frontend/helpers/dom_shims/mutation_observer.js
deleted file mode 100644
index 68c494f19ea..00000000000
--- a/spec/frontend/helpers/dom_shims/mutation_observer.js
+++ /dev/null
@@ -1,7 +0,0 @@
-/* eslint-disable class-methods-use-this */
-class MutationObserverStub {
- disconnect() {}
- observe() {}
-}
-
-global.MutationObserver = MutationObserverStub;
diff --git a/spec/frontend/helpers/dom_shims/range.js b/spec/frontend/helpers/dom_shims/range.js
new file mode 100644
index 00000000000..4ffdf3280ad
--- /dev/null
+++ b/spec/frontend/helpers/dom_shims/range.js
@@ -0,0 +1,13 @@
+if (window.Range.prototype.getBoundingClientRect) {
+ throw new Error('window.Range.prototype.getBoundingClientRect already exists. Remove this stub!');
+}
+window.Range.prototype.getBoundingClientRect = function getBoundingClientRect() {
+ return { x: 0, y: 0, width: 0, height: 0, top: 0, right: 0, bottom: 0, left: 0 };
+};
+
+if (window.Range.prototype.getClientRects) {
+ throw new Error('window.Range.prototype.getClientRects already exists. Remove this stub!');
+}
+window.Range.prototype.getClientRects = function getClientRects() {
+ return [this.getBoundingClientRect()];
+};
diff --git a/spec/frontend/helpers/filtered_search_spec_helper.js b/spec/frontend/helpers/filtered_search_spec_helper.js
index ceb7982bbc3..ecf10694a16 100644
--- a/spec/frontend/helpers/filtered_search_spec_helper.js
+++ b/spec/frontend/helpers/filtered_search_spec_helper.js
@@ -15,7 +15,7 @@ export default class FilteredSearchSpecHelper {
<div class="value-container">
<div class="value">${value}</div>
<div class="remove-token" role="button">
- <i class="fa fa-close"></i>
+ <svg class="s16 close-icon"></svg>
</div>
</div>
</div>
diff --git a/spec/frontend/helpers/init_vue_mr_page_helper.js b/spec/frontend/helpers/init_vue_mr_page_helper.js
index c1d608cc5a0..b9aed63d0f6 100644
--- a/spec/frontend/helpers/init_vue_mr_page_helper.js
+++ b/spec/frontend/helpers/init_vue_mr_page_helper.js
@@ -22,6 +22,7 @@ export default function initVueMRPage() {
mrDiscussionsEl.setAttribute('data-noteable-data', JSON.stringify(noteableDataMock));
mrDiscussionsEl.setAttribute('data-notes-data', JSON.stringify(notesDataMock));
mrDiscussionsEl.setAttribute('data-noteable-type', 'merge-request');
+ mrDiscussionsEl.setAttribute('data-is-locked', 'false');
mrTestEl.appendChild(mrDiscussionsEl);
const discussionCounterEl = document.createElement('div');
diff --git a/spec/frontend/helpers/monitor_helper_spec.js b/spec/frontend/helpers/monitor_helper_spec.js
index 083b6404125..219b05e312b 100644
--- a/spec/frontend/helpers/monitor_helper_spec.js
+++ b/spec/frontend/helpers/monitor_helper_spec.js
@@ -1,12 +1,38 @@
-import * as monitorHelper from '~/helpers/monitor_helper';
+import { getSeriesLabel, makeDataSeries } from '~/helpers/monitor_helper';
describe('monitor helper', () => {
const defaultConfig = { default: true, name: 'default name' };
const name = 'data name';
const series = [[1, 1], [2, 2], [3, 3]];
- const data = ({ metric = { default_name: name }, values = series } = {}) => [{ metric, values }];
+
+ describe('getSeriesLabel', () => {
+ const metricAttributes = { __name__: 'up', app: 'prometheus' };
+
+ it('gets a single attribute label', () => {
+ expect(getSeriesLabel('app', metricAttributes)).toBe('app: prometheus');
+ });
+
+ it('gets a templated label', () => {
+ expect(getSeriesLabel('{{__name__}}', metricAttributes)).toBe('up');
+ expect(getSeriesLabel('{{app}}', metricAttributes)).toBe('prometheus');
+ expect(getSeriesLabel('{{missing}}', metricAttributes)).toBe('{{missing}}');
+ });
+
+ it('gets a multiple label', () => {
+ expect(getSeriesLabel(null, metricAttributes)).toBe('__name__: up, app: prometheus');
+ expect(getSeriesLabel('', metricAttributes)).toBe('__name__: up, app: prometheus');
+ });
+
+ it('gets a simple label', () => {
+ expect(getSeriesLabel('A label', {})).toBe('A label');
+ });
+ });
describe('makeDataSeries', () => {
+ const data = ({ metric = { default_name: name }, values = series } = {}) => [
+ { metric, values },
+ ];
+
const expectedDataSeries = [
{
...defaultConfig,
@@ -15,19 +41,17 @@ describe('monitor helper', () => {
];
it('converts query results to data series', () => {
- expect(monitorHelper.makeDataSeries(data({ metric: {} }), defaultConfig)).toEqual(
- expectedDataSeries,
- );
+ expect(makeDataSeries(data({ metric: {} }), defaultConfig)).toEqual(expectedDataSeries);
});
it('returns an empty array if no query results exist', () => {
- expect(monitorHelper.makeDataSeries([], defaultConfig)).toEqual([]);
+ expect(makeDataSeries([], defaultConfig)).toEqual([]);
});
it('handles multi-series query results', () => {
const expectedData = { ...expectedDataSeries[0], name: 'default name: data name' };
- expect(monitorHelper.makeDataSeries([...data(), ...data()], defaultConfig)).toEqual([
+ expect(makeDataSeries([...data(), ...data()], defaultConfig)).toEqual([
expectedData,
expectedData,
]);
@@ -39,10 +63,7 @@ describe('monitor helper', () => {
name: '{{cmd}}',
};
- const [result] = monitorHelper.makeDataSeries(
- [{ metric: { cmd: 'brpop' }, values: series }],
- config,
- );
+ const [result] = makeDataSeries([{ metric: { cmd: 'brpop' }, values: series }], config);
expect(result.name).toEqual('brpop');
});
@@ -53,7 +74,7 @@ describe('monitor helper', () => {
name: '',
};
- const [result] = monitorHelper.makeDataSeries(
+ const [result] = makeDataSeries(
[
{
metric: {
@@ -79,7 +100,7 @@ describe('monitor helper', () => {
name: 'backend: {{ backend }}',
};
- const [result] = monitorHelper.makeDataSeries(
+ const [result] = makeDataSeries(
[{ metric: { backend: 'HA Server' }, values: series }],
config,
);
@@ -90,10 +111,7 @@ describe('monitor helper', () => {
it('supports repeated template variables', () => {
const config = { ...defaultConfig, name: '{{cmd}}, {{cmd}}' };
- const [result] = monitorHelper.makeDataSeries(
- [{ metric: { cmd: 'brpop' }, values: series }],
- config,
- );
+ const [result] = makeDataSeries([{ metric: { cmd: 'brpop' }, values: series }], config);
expect(result.name).toEqual('brpop, brpop');
});
@@ -101,7 +119,7 @@ describe('monitor helper', () => {
it('supports hyphenated template variables', () => {
const config = { ...defaultConfig, name: 'expired - {{ test-attribute }}' };
- const [result] = monitorHelper.makeDataSeries(
+ const [result] = makeDataSeries(
[{ metric: { 'test-attribute': 'test-attribute-value' }, values: series }],
config,
);
@@ -115,7 +133,7 @@ describe('monitor helper', () => {
name: '{{job}}: {{cmd}}',
};
- const [result] = monitorHelper.makeDataSeries(
+ const [result] = makeDataSeries(
[{ metric: { cmd: 'brpop', job: 'redis' }, values: series }],
config,
);
@@ -129,7 +147,7 @@ describe('monitor helper', () => {
name: '{{cmd}}',
};
- const [firstSeries, secondSeries] = monitorHelper.makeDataSeries(
+ const [firstSeries, secondSeries] = makeDataSeries(
[
{ metric: { cmd: 'brpop' }, values: series },
{ metric: { cmd: 'zrangebyscore' }, values: series },
diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js
index 8b3853d4535..762f3c5dad1 100644
--- a/spec/frontend/ide/components/activity_bar_spec.js
+++ b/spec/frontend/ide/components/activity_bar_spec.js
@@ -1,15 +1,17 @@
import Vue from 'vue';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import { leftSidebarViews } from '~/ide/constants';
import ActivityBar from '~/ide/components/activity_bar.vue';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { resetStore } from '../helpers';
describe('IDE activity bar', () => {
const Component = Vue.extend(ActivityBar);
let vm;
+ let store;
beforeEach(() => {
+ store = createStore();
+
Vue.set(store.state.projects, 'abcproject', {
web_url: 'testing',
});
@@ -20,8 +22,6 @@ describe('IDE activity bar', () => {
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
describe('updateActivityBarView', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js
index 16d0b354a30..dbb43e43c19 100644
--- a/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/empty_state_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import emptyState from '~/ide/components/commit_sidebar/empty_state.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
-import { resetStore } from '../../helpers';
describe('IDE commit panel empty state', () => {
let vm;
+ let store;
beforeEach(() => {
+ store = createStore();
+
const Component = Vue.extend(emptyState);
Vue.set(store.state, 'noChangesStateSvgPath', 'no-changes');
@@ -19,8 +21,6 @@ describe('IDE commit panel empty state', () => {
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
it('renders no changes text when last commit message is empty', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index c62df4a3795..9245cefc183 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -1,19 +1,20 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { projectData } from 'jest/ide/mock_data';
-import store from '~/ide/stores';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createStore } from '~/ide/stores';
import CommitForm from '~/ide/components/commit_sidebar/form.vue';
import { leftSidebarViews } from '~/ide/constants';
-import { resetStore } from '../../helpers';
-import waitForPromises from 'helpers/wait_for_promises';
describe('IDE commit form', () => {
const Component = Vue.extend(CommitForm);
let vm;
+ let store;
const beginCommitButton = () => vm.$el.querySelector('[data-testid="begin-commit-button"]');
beforeEach(() => {
+ store = createStore();
store.state.changedFiles.push('test');
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
@@ -24,8 +25,6 @@ describe('IDE commit form', () => {
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
it('enables begin commit button when there are changes', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/list_collapsed_spec.js b/spec/frontend/ide/components/commit_sidebar/list_collapsed_spec.js
index 45372d18965..42e0a20bc7b 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_collapsed_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_collapsed_spec.js
@@ -1,14 +1,17 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
import { file } from '../../helpers';
import { removeWhitespace } from '../../../helpers/text_helper';
describe('Multi-file editor commit sidebar list collapsed', () => {
let vm;
+ let store;
beforeEach(() => {
+ store = createStore();
+
const Component = Vue.extend(listCollapsed);
vm = createComponentWithStore(Component, store, {
diff --git a/spec/frontend/ide/components/commit_sidebar/list_spec.js b/spec/frontend/ide/components/commit_sidebar/list_spec.js
index 2b5664ffc4e..2107ff96e95 100644
--- a/spec/frontend/ide/components/commit_sidebar/list_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/list_spec.js
@@ -1,13 +1,16 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
-import { file, resetStore } from '../../helpers';
+import { file } from '../../helpers';
describe('Multi-file editor commit sidebar list', () => {
+ let store;
let vm;
beforeEach(() => {
+ store = createStore();
+
const Component = Vue.extend(commitSidebarList);
vm = createComponentWithStore(Component, store, {
@@ -26,8 +29,6 @@ describe('Multi-file editor commit sidebar list', () => {
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
describe('with a list of files', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
index ac80ba58056..bf61f4bbe77 100644
--- a/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/radio_group_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import { resetStore } from 'jest/ide/helpers';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import radioGroup from '~/ide/components/commit_sidebar/radio_group.vue';
describe('IDE commit sidebar radio group', () => {
let vm;
+ let store;
beforeEach(done => {
+ store = createStore();
+
const Component = Vue.extend(radioGroup);
store.state.commit.commitAction = '2';
@@ -25,8 +27,6 @@ describe('IDE commit sidebar radio group', () => {
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
it('uses label if present', () => {
diff --git a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
index e1a432b81be..db13c90fbb9 100644
--- a/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/success_message_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import successMessage from '~/ide/components/commit_sidebar/success_message.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
-import { resetStore } from '../../helpers';
describe('IDE commit panel successful commit state', () => {
let vm;
+ let store;
beforeEach(() => {
+ store = createStore();
+
const Component = Vue.extend(successMessage);
vm = createComponentWithStore(Component, store, {
@@ -19,8 +21,6 @@ describe('IDE commit panel successful commit state', () => {
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
it('renders last commit message when it exists', done => {
diff --git a/spec/frontend/ide/components/file_row_extra_spec.js b/spec/frontend/ide/components/file_row_extra_spec.js
index e78bacadebb..4bd27d23f76 100644
--- a/spec/frontend/ide/components/file_row_extra_spec.js
+++ b/spec/frontend/ide/components/file_row_extra_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { createStore } from '~/ide/stores';
import FileRowExtra from '~/ide/components/file_row_extra.vue';
-import { file, resetStore } from '../helpers';
+import { file } from '../helpers';
describe('IDE extra file row component', () => {
let Component;
@@ -32,7 +32,6 @@ describe('IDE extra file row component', () => {
afterEach(() => {
vm.$destroy();
- resetStore(vm.$store);
stagedFilesCount = 0;
unstagedFilesCount = 0;
diff --git a/spec/frontend/ide/components/file_templates/bar_spec.js b/spec/frontend/ide/components/file_templates/bar_spec.js
index 21dbe18a223..5a33837fb14 100644
--- a/spec/frontend/ide/components/file_templates/bar_spec.js
+++ b/spec/frontend/ide/components/file_templates/bar_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
import { createStore } from '~/ide/stores';
import Bar from '~/ide/components/file_templates/bar.vue';
-import { resetStore, file } from '../../helpers';
+import { file } from '../../helpers';
describe('IDE file templates bar component', () => {
let Component;
@@ -26,7 +26,6 @@ describe('IDE file templates bar component', () => {
afterEach(() => {
vm.$destroy();
- resetStore(vm.$store);
});
describe('template type dropdown', () => {
diff --git a/spec/frontend/ide/components/ide_review_spec.js b/spec/frontend/ide/components/ide_review_spec.js
index b56957e1f6d..c9ac2ac423d 100644
--- a/spec/frontend/ide/components/ide_review_spec.js
+++ b/spec/frontend/ide/components/ide_review_spec.js
@@ -3,7 +3,7 @@ import IdeReview from '~/ide/components/ide_review.vue';
import { createStore } from '~/ide/stores';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { trimText } from '../../helpers/text_helper';
-import { resetStore, file } from '../helpers';
+import { file } from '../helpers';
import { projectData } from '../mock_data';
describe('IDE review mode', () => {
@@ -26,8 +26,6 @@ describe('IDE review mode', () => {
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
it('renders list of files', () => {
diff --git a/spec/frontend/ide/components/ide_side_bar_spec.js b/spec/frontend/ide/components/ide_side_bar_spec.js
index 65cad2e7eb0..67257b40879 100644
--- a/spec/frontend/ide/components/ide_side_bar_spec.js
+++ b/spec/frontend/ide/components/ide_side_bar_spec.js
@@ -1,15 +1,17 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import ideSidebar from '~/ide/components/ide_side_bar.vue';
import { leftSidebarViews } from '~/ide/constants';
-import { resetStore } from '../helpers';
import { projectData } from '../mock_data';
describe('IdeSidebar', () => {
let vm;
+ let store;
beforeEach(() => {
+ store = createStore();
+
const Component = Vue.extend(ideSidebar);
store.state.currentProjectId = 'abcproject';
@@ -20,8 +22,6 @@ describe('IdeSidebar', () => {
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
it('renders a sidebar', () => {
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index efc1d984dec..a7b07a9f0e2 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { createStore } from '~/ide/stores';
import ide from '~/ide/components/ide.vue';
-import { file, resetStore } from '../helpers';
+import { file } from '../helpers';
import { projectData } from '../mock_data';
import extendStore from '~/ide/stores/extend';
@@ -41,8 +41,6 @@ describe('ide component, empty repo', () => {
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
it('renders "New file" button in empty repo', done => {
@@ -63,8 +61,6 @@ describe('ide component, non-empty repo', () => {
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
it('shows error message when set', done => {
diff --git a/spec/frontend/ide/components/ide_tree_list_spec.js b/spec/frontend/ide/components/ide_tree_list_spec.js
index 30f11db3153..4593ef6049b 100644
--- a/spec/frontend/ide/components/ide_tree_list_spec.js
+++ b/spec/frontend/ide/components/ide_tree_list_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import IdeTreeList from '~/ide/components/ide_tree_list.vue';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { resetStore, file } from '../helpers';
+import { file } from '../helpers';
import { projectData } from '../mock_data';
describe('IDE tree list', () => {
@@ -10,6 +10,7 @@ describe('IDE tree list', () => {
const normalBranchTree = [file('fileName')];
const emptyBranchTree = [];
let vm;
+ let store;
const bootstrapWithTree = (tree = normalBranchTree) => {
store.state.currentProjectId = 'abcproject';
@@ -25,10 +26,12 @@ describe('IDE tree list', () => {
});
};
+ beforeEach(() => {
+ store = createStore();
+ });
+
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
describe('normal branch', () => {
diff --git a/spec/frontend/ide/components/ide_tree_spec.js b/spec/frontend/ide/components/ide_tree_spec.js
index 01f007f09c3..899daa0bf57 100644
--- a/spec/frontend/ide/components/ide_tree_spec.js
+++ b/spec/frontend/ide/components/ide_tree_spec.js
@@ -1,14 +1,17 @@
import Vue from 'vue';
import IdeTree from '~/ide/components/ide_tree.vue';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import { resetStore, file } from '../helpers';
+import { file } from '../helpers';
import { projectData } from '../mock_data';
describe('IdeRepoTree', () => {
+ let store;
let vm;
beforeEach(() => {
+ store = createStore();
+
const IdeRepoTree = Vue.extend(IdeTree);
store.state.currentProjectId = 'abcproject';
@@ -24,8 +27,6 @@ describe('IdeRepoTree', () => {
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
it('renders list of files', () => {
diff --git a/spec/frontend/ide/components/jobs/detail_spec.js b/spec/frontend/ide/components/jobs/detail_spec.js
index 8f3815d5aab..acd30dee718 100644
--- a/spec/frontend/ide/components/jobs/detail_spec.js
+++ b/spec/frontend/ide/components/jobs/detail_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
+import { TEST_HOST } from 'helpers/test_constants';
import JobDetail from '~/ide/components/jobs/detail.vue';
import { createStore } from '~/ide/stores';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { jobs } from '../../mock_data';
-import { TEST_HOST } from 'helpers/test_constants';
describe('IDE jobs detail view', () => {
let vm;
diff --git a/spec/frontend/ide/components/new_dropdown/index_spec.js b/spec/frontend/ide/components/new_dropdown/index_spec.js
index 00781c16609..c6cebf36de3 100644
--- a/spec/frontend/ide/components/new_dropdown/index_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/index_spec.js
@@ -1,13 +1,15 @@
import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
-import store from '~/ide/stores';
+import { createStore } from '~/ide/stores';
import newDropdown from '~/ide/components/new_dropdown/index.vue';
-import { resetStore } from '../../helpers';
describe('new dropdown component', () => {
+ let store;
let vm;
beforeEach(() => {
+ store = createStore();
+
const component = Vue.extend(newDropdown);
vm = createComponentWithStore(component, store, {
@@ -30,8 +32,6 @@ describe('new dropdown component', () => {
afterEach(() => {
vm.$destroy();
-
- resetStore(vm.$store);
});
it('renders new file, upload and new directory links', () => {
diff --git a/spec/frontend/ide/components/new_dropdown/modal_spec.js b/spec/frontend/ide/components/new_dropdown/modal_spec.js
index da17cc3601e..ea8ba24c9d0 100644
--- a/spec/frontend/ide/components/new_dropdown/modal_spec.js
+++ b/spec/frontend/ide/components/new_dropdown/modal_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { createStore } from '~/ide/stores';
import modal from '~/ide/components/new_dropdown/modal.vue';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
index e32abc98aae..bb9ba32a699 100644
--- a/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
+++ b/spec/frontend/ide/components/panes/collapsible_sidebar_spec.js
@@ -1,9 +1,9 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
import { createStore } from '~/ide/stores';
import paneModule from '~/ide/stores/modules/pane';
import CollapsibleSidebar from '~/ide/components/panes/collapsible_sidebar.vue';
import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue';
-import Vuex from 'vuex';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/ide/components/pipelines/list_spec.js b/spec/frontend/ide/components/pipelines/list_spec.js
index 795ded35d20..86cdbafaff9 100644
--- a/spec/frontend/ide/components/pipelines/list_spec.js
+++ b/spec/frontend/ide/components/pipelines/list_spec.js
@@ -2,11 +2,11 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { GlLoadingIcon } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
+import { pipelines } from 'jest/ide/mock_data';
import List from '~/ide/components/pipelines/list.vue';
import JobsList from '~/ide/components/jobs/list.vue';
import Tab from '~/vue_shared/components/tabs/tab.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
-import { pipelines } from 'jest/ide/mock_data';
import IDEServices from '~/ide/services';
const localVue = createLocalVue();
diff --git a/spec/frontend/ide/components/preview/navigator_spec.js b/spec/frontend/ide/components/preview/navigator_spec.js
index aa15f391e77..ba5ac3bbbea 100644
--- a/spec/frontend/ide/components/preview/navigator_spec.js
+++ b/spec/frontend/ide/components/preview/navigator_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { GlLoadingIcon } from '@gitlab/ui';
-import ClientsideNavigator from '~/ide/components/preview/navigator.vue';
import { listen } from 'codesandbox-api';
+import ClientsideNavigator from '~/ide/components/preview/navigator.vue';
jest.mock('codesandbox-api', () => ({
listen: jest.fn().mockReturnValue(jest.fn()),
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index a4336b8f2eb..f0ae2ba732b 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -3,6 +3,8 @@ import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import '~/behaviors/markdown/render_gfm';
import { Range } from 'monaco-editor';
+import waitForPromises from 'helpers/wait_for_promises';
+import waitUsingRealTimer from 'helpers/wait_using_real_timer';
import axios from '~/lib/utils/axios_utils';
import service from '~/ide/services';
import { createStoreOptions } from '~/ide/stores';
@@ -15,10 +17,8 @@ import {
viewerTypes,
} from '~/ide/constants';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
-import waitForPromises from 'helpers/wait_for_promises';
import { file } from '../helpers';
import { exampleConfigs, exampleFiles } from '../lib/editorconfig/mock_data';
-import waitUsingRealTimer from 'helpers/wait_using_real_timer';
describe('RepoEditor', () => {
let vm;
diff --git a/spec/frontend/ide/helpers.js b/spec/frontend/ide/helpers.js
index a9620d26313..8caa9c2b437 100644
--- a/spec/frontend/ide/helpers.js
+++ b/spec/frontend/ide/helpers.js
@@ -1,25 +1,5 @@
import * as pathUtils from 'path';
import { decorateData } from '~/ide/stores/utils';
-import state from '~/ide/stores/state';
-import commitState from '~/ide/stores/modules/commit/state';
-import mergeRequestsState from '~/ide/stores/modules/merge_requests/state';
-import pipelinesState from '~/ide/stores/modules/pipelines/state';
-import branchesState from '~/ide/stores/modules/branches/state';
-import fileTemplatesState from '~/ide/stores/modules/file_templates/state';
-import paneState from '~/ide/stores/modules/pane/state';
-
-export const resetStore = store => {
- const newState = {
- ...state(),
- commit: commitState(),
- mergeRequests: mergeRequestsState(),
- pipelines: pipelinesState(),
- branches: branchesState(),
- fileTemplates: fileTemplatesState(),
- rightPane: paneState(),
- };
- store.replaceState(newState);
-};
export const file = (name = 'name', id = name, type = '', parent = null) =>
decorateData({
diff --git a/spec/frontend/ide/ide_router_spec.js b/spec/frontend/ide/ide_router_spec.js
index b53e2019819..a4fe00883cf 100644
--- a/spec/frontend/ide/ide_router_spec.js
+++ b/spec/frontend/ide/ide_router_spec.js
@@ -1,6 +1,6 @@
+import waitForPromises from 'helpers/wait_for_promises';
import { createRouter } from '~/ide/ide_router';
import { createStore } from '~/ide/stores';
-import waitForPromises from 'helpers/wait_for_promises';
describe('IDE router', () => {
const PROJECT_NAMESPACE = 'my-group/sub-group';
diff --git a/spec/frontend/ide/lib/decorations/controller_spec.js b/spec/frontend/ide/lib/decorations/controller_spec.js
index 4556fc9d646..e9b7faaadfe 100644
--- a/spec/frontend/ide/lib/decorations/controller_spec.js
+++ b/spec/frontend/ide/lib/decorations/controller_spec.js
@@ -2,14 +2,17 @@ import Editor from '~/ide/lib/editor';
import DecorationsController from '~/ide/lib/decorations/controller';
import Model from '~/ide/lib/common/model';
import { file } from '../../helpers';
+import { createStore } from '~/ide/stores';
describe('Multi-file editor library decorations controller', () => {
let editorInstance;
let controller;
let model;
+ let store;
beforeEach(() => {
- editorInstance = Editor.create();
+ store = createStore();
+ editorInstance = Editor.create(store);
editorInstance.createInstance(document.createElement('div'));
controller = new DecorationsController(editorInstance);
diff --git a/spec/frontend/ide/lib/diff/controller_spec.js b/spec/frontend/ide/lib/diff/controller_spec.js
index 0b33a4c6ad6..8ee6388a760 100644
--- a/spec/frontend/ide/lib/diff/controller_spec.js
+++ b/spec/frontend/ide/lib/diff/controller_spec.js
@@ -4,6 +4,7 @@ import ModelManager from '~/ide/lib/common/model_manager';
import DecorationsController from '~/ide/lib/decorations/controller';
import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
import { computeDiff } from '~/ide/lib/diff/diff';
+import { createStore } from '~/ide/stores';
import { file } from '../../helpers';
describe('Multi-file editor library dirty diff controller', () => {
@@ -12,9 +13,12 @@ describe('Multi-file editor library dirty diff controller', () => {
let modelManager;
let decorationsController;
let model;
+ let store;
beforeEach(() => {
- editorInstance = Editor.create();
+ store = createStore();
+
+ editorInstance = Editor.create(store);
editorInstance.createInstance(document.createElement('div'));
modelManager = new ModelManager();
diff --git a/spec/frontend/ide/lib/editor_spec.js b/spec/frontend/ide/lib/editor_spec.js
index 5f28309422d..529f80e6f6f 100644
--- a/spec/frontend/ide/lib/editor_spec.js
+++ b/spec/frontend/ide/lib/editor_spec.js
@@ -5,6 +5,7 @@ import {
Selection,
} from 'monaco-editor';
import Editor from '~/ide/lib/editor';
+import { createStore } from '~/ide/stores';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
import { file } from '../helpers';
@@ -12,6 +13,7 @@ describe('Multi-file editor library', () => {
let instance;
let el;
let holder;
+ let store;
const setNodeOffsetWidth = val => {
Object.defineProperty(instance.instance.getDomNode(), 'offsetWidth', {
@@ -22,13 +24,14 @@ describe('Multi-file editor library', () => {
};
beforeEach(() => {
+ store = createStore();
el = document.createElement('div');
holder = document.createElement('div');
el.appendChild(holder);
document.body.appendChild(el);
- instance = Editor.create();
+ instance = Editor.create(store);
});
afterEach(() => {
@@ -44,7 +47,7 @@ describe('Multi-file editor library', () => {
});
it('creates instance returns cached instance', () => {
- expect(Editor.create()).toEqual(instance);
+ expect(Editor.create(store)).toEqual(instance);
});
describe('createInstance', () => {
diff --git a/spec/frontend/ide/lib/languages/vue_spec.js b/spec/frontend/ide/lib/languages/vue_spec.js
index 3d8784c1436..ba5c31bb101 100644
--- a/spec/frontend/ide/lib/languages/vue_spec.js
+++ b/spec/frontend/ide/lib/languages/vue_spec.js
@@ -9,7 +9,7 @@ describe('tokenization for .vue files', () => {
registerLanguages(vue);
});
- test.each([
+ it.each([
[
'<div v-if="something">content</div>',
[
diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js
index e5c4f346459..62971b9cad6 100644
--- a/spec/frontend/ide/stores/actions/merge_request_spec.js
+++ b/spec/frontend/ide/stores/actions/merge_request_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import store from '~/ide/stores';
-import createFlash from '~/flash';
+import { createStore } from '~/ide/stores';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
getMergeRequestData,
getMergeRequestChanges,
@@ -10,7 +10,6 @@ import {
} from '~/ide/stores/actions/merge_request';
import service from '~/ide/services';
import { leftSidebarViews, PERMISSION_READ_MR } from '~/ide/constants';
-import { resetStore } from '../../helpers';
const TEST_PROJECT = 'abcproject';
const TEST_PROJECT_ID = 17;
@@ -18,9 +17,12 @@ const TEST_PROJECT_ID = 17;
jest.mock('~/flash');
describe('IDE store merge request actions', () => {
+ let store;
let mock;
beforeEach(() => {
+ store = createStore();
+
mock = new MockAdapter(axios);
store.state.projects[TEST_PROJECT] = {
@@ -34,7 +36,6 @@ describe('IDE store merge request actions', () => {
afterEach(() => {
mock.restore();
- resetStore(store);
});
describe('getMergeRequestsForBranch', () => {
diff --git a/spec/frontend/ide/stores/actions/project_spec.js b/spec/frontend/ide/stores/actions/project_spec.js
index 64024c12903..ca3687307a9 100644
--- a/spec/frontend/ide/stores/actions/project_spec.js
+++ b/spec/frontend/ide/stores/actions/project_spec.js
@@ -1,4 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import axios from '~/lib/utils/axios_utils';
import { createStore } from '~/ide/stores';
import {
@@ -12,8 +14,6 @@ import {
} from '~/ide/stores/actions';
import service from '~/ide/services';
import api from '~/api';
-import testAction from 'helpers/vuex_action_helper';
-import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
const TEST_PROJECT_ID = 'abc/def';
diff --git a/spec/frontend/ide/stores/actions/tree_spec.js b/spec/frontend/ide/stores/actions/tree_spec.js
index c20941843c4..0eabd982d57 100644
--- a/spec/frontend/ide/stores/actions/tree_spec.js
+++ b/spec/frontend/ide/stores/actions/tree_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import { TEST_HOST } from 'jest/helpers/test_constants';
import { showTreeEntry, getFiles, setDirectoryData } from '~/ide/stores/actions/tree';
import * as types from '~/ide/stores/mutation_types';
import axios from '~/lib/utils/axios_utils';
@@ -7,7 +8,6 @@ import { createStore } from '~/ide/stores';
import service from '~/ide/services';
import { createRouter } from '~/ide/ide_router';
import { file, createEntriesFromPaths } from '../../helpers';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('Multi-file store tree actions', () => {
let projectTree;
diff --git a/spec/frontend/ide/stores/modules/file_templates/getters_spec.js b/spec/frontend/ide/stores/modules/file_templates/getters_spec.js
index 5855496a330..c9676b23fa1 100644
--- a/spec/frontend/ide/stores/modules/file_templates/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/file_templates/getters_spec.js
@@ -5,7 +5,7 @@ import * as getters from '~/ide/stores/modules/file_templates/getters';
describe('IDE file templates getters', () => {
describe('templateTypes', () => {
it('returns list of template types', () => {
- expect(getters.templateTypes().length).toBe(4);
+ expect(getters.templateTypes().length).toBe(5);
});
});
diff --git a/spec/frontend/ide/stores/modules/router/actions_spec.js b/spec/frontend/ide/stores/modules/router/actions_spec.js
index 4795eae2b79..1458a43da57 100644
--- a/spec/frontend/ide/stores/modules/router/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/router/actions_spec.js
@@ -1,6 +1,6 @@
+import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/ide/stores/modules/router/actions';
import * as types from '~/ide/stores/modules/router/mutation_types';
-import testAction from 'helpers/vuex_action_helper';
const TEST_PATH = 'test/path/abc';
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
index 4bc937b4784..d0ac2af3ffd 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
@@ -6,7 +6,7 @@ import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import * as actions from '~/ide/stores/modules/terminal/actions/session_controls';
import httpStatus from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
index 7909f828124..e25746e1dd1 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
@@ -5,7 +5,7 @@ import * as messages from '~/ide/stores/modules/terminal/messages';
import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import * as actions from '~/ide/stores/modules/terminal/actions/session_status';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/ide/stores/modules/terminal/messages_spec.js b/spec/frontend/ide/stores/modules/terminal/messages_spec.js
index 966158999da..1bb92a9dfa5 100644
--- a/spec/frontend/ide/stores/modules/terminal/messages_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/messages_spec.js
@@ -15,6 +15,8 @@ describe('IDE store terminal messages', () => {
sprintf(
messages.ERROR_CONFIG,
{
+ codeStart: `<code>`,
+ codeEnd: `</code>`,
helpStart: `<a href="${escape(TEST_HELP_URL)}" target="_blank">`,
helpEnd: '</a>',
},
diff --git a/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js b/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js
index ac976300ed0..3fa57bde415 100644
--- a/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal_sync/actions_spec.js
@@ -1,7 +1,7 @@
+import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/ide/stores/modules/terminal_sync/actions';
import mirror, { canConnect, SERVICE_NAME } from '~/ide/lib/mirror';
import * as types from '~/ide/stores/modules/terminal_sync/mutation_types';
-import testAction from 'helpers/vuex_action_helper';
jest.mock('~/ide/lib/mirror');
diff --git a/spec/frontend/ide/sync_router_and_store_spec.js b/spec/frontend/ide/sync_router_and_store_spec.js
index c4ce92b99cc..ccf6e200806 100644
--- a/spec/frontend/ide/sync_router_and_store_spec.js
+++ b/spec/frontend/ide/sync_router_and_store_spec.js
@@ -1,7 +1,7 @@
import VueRouter from 'vue-router';
+import waitForPromises from 'helpers/wait_for_promises';
import { createStore } from '~/ide/stores';
import { syncRouterAndStore } from '~/ide/sync_router_and_store';
-import waitForPromises from 'helpers/wait_for_promises';
const TEST_ROUTE = '/test/lorem/ipsum';
diff --git a/spec/frontend/ide/utils_spec.js b/spec/frontend/ide/utils_spec.js
index b6de576a0a4..e7ef0de45a0 100644
--- a/spec/frontend/ide/utils_spec.js
+++ b/spec/frontend/ide/utils_spec.js
@@ -1,3 +1,4 @@
+import { languages } from 'monaco-editor';
import {
isTextFile,
registerLanguages,
@@ -9,7 +10,6 @@ import {
getPathParent,
readFileAsDataURL,
} from '~/ide/utils';
-import { languages } from 'monaco-editor';
describe('WebIDE utils', () => {
describe('isTextFile', () => {
diff --git a/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js b/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js
index 2deb4be2b91..98c05d648b8 100644
--- a/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js
+++ b/spec/frontend/image_diff/helpers/comment_indicator_helper_spec.js
@@ -1,6 +1,6 @@
+import { TEST_HOST } from 'jest/helpers/test_constants';
import * as commentIndicatorHelper from '~/image_diff/helpers/comment_indicator_helper';
import * as mockData from '../mock_data';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('commentIndicatorHelper', () => {
const { coordinate } = mockData;
diff --git a/spec/frontend/image_diff/helpers/utils_helper_spec.js b/spec/frontend/image_diff/helpers/utils_helper_spec.js
index a47c681e775..7f2376826c2 100644
--- a/spec/frontend/image_diff/helpers/utils_helper_spec.js
+++ b/spec/frontend/image_diff/helpers/utils_helper_spec.js
@@ -1,7 +1,7 @@
+import { TEST_HOST } from 'jest/helpers/test_constants';
import * as utilsHelper from '~/image_diff/helpers/utils_helper';
import ImageBadge from '~/image_diff/image_badge';
import * as mockData from '../mock_data';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('utilsHelper', () => {
const { noteId, discussionId, image, imageProperties, imageMeta } = mockData;
diff --git a/spec/frontend/image_diff/image_diff_spec.js b/spec/frontend/image_diff/image_diff_spec.js
index 2b29a522193..d89e4312344 100644
--- a/spec/frontend/image_diff/image_diff_spec.js
+++ b/spec/frontend/image_diff/image_diff_spec.js
@@ -1,8 +1,8 @@
+import { TEST_HOST } from 'jest/helpers/test_constants';
import ImageDiff from '~/image_diff/image_diff';
import * as imageUtility from '~/lib/utils/image_utility';
import imageDiffHelper from '~/image_diff/helpers/index';
import * as mockData from './mock_data';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('ImageDiff', () => {
let element;
diff --git a/spec/frontend/image_diff/replaced_image_diff_spec.js b/spec/frontend/image_diff/replaced_image_diff_spec.js
index 38a43bfa858..10827d76e55 100644
--- a/spec/frontend/image_diff/replaced_image_diff_spec.js
+++ b/spec/frontend/image_diff/replaced_image_diff_spec.js
@@ -1,8 +1,8 @@
+import { TEST_HOST } from 'jest/helpers/test_constants';
import ReplacedImageDiff from '~/image_diff/replaced_image_diff';
import ImageDiff from '~/image_diff/image_diff';
import { viewTypes } from '~/image_diff/view_types';
import imageDiffHelper from '~/image_diff/helpers/index';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('ReplacedImageDiff', () => {
let element;
diff --git a/spec/frontend/import_projects/components/import_projects_table_spec.js b/spec/frontend/import_projects/components/import_projects_table_spec.js
index 419d67e239f..b217242968a 100644
--- a/spec/frontend/import_projects/components/import_projects_table_spec.js
+++ b/spec/frontend/import_projects/components/import_projects_table_spec.js
@@ -2,16 +2,14 @@ import { nextTick } from 'vue';
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlButton } from '@gitlab/ui';
-import { state, getters } from '~/import_projects/store';
-import eventHub from '~/import_projects/event_hub';
+import state from '~/import_projects/store/state';
+import * as getters from '~/import_projects/store/getters';
+import { STATUSES } from '~/import_projects/constants';
import ImportProjectsTable from '~/import_projects/components/import_projects_table.vue';
import ImportedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
import IncompatibleRepoTableRow from '~/import_projects/components/incompatible_repo_table_row.vue';
-
-jest.mock('~/import_projects/event_hub', () => ({
- $emit: jest.fn(),
-}));
+import PageQueryParamSync from '~/import_projects/components/page_query_param_sync.vue';
describe('ImportProjectsTable', () => {
let wrapper;
@@ -21,13 +19,6 @@ describe('ImportProjectsTable', () => {
const providerTitle = 'THE PROVIDER';
const providerRepo = { id: 10, sanitizedName: 'sanitizedName', fullName: 'fullName' };
- const importedProject = {
- id: 1,
- fullPath: 'fullPath',
- importStatus: 'started',
- providerLink: 'providerLink',
- importSource: 'importSource',
- };
const findImportAllButton = () =>
wrapper
@@ -35,11 +26,15 @@ describe('ImportProjectsTable', () => {
.filter(w => w.props().variant === 'success')
.at(0);
+ const importAllFn = jest.fn();
+ const setPageFn = jest.fn();
+
function createComponent({
state: initialState,
getters: customGetters,
slots,
filterable,
+ paginatable,
} = {}) {
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -52,11 +47,13 @@ describe('ImportProjectsTable', () => {
},
actions: {
fetchRepos: jest.fn(),
- fetchReposFiltered: jest.fn(),
fetchJobs: jest.fn(),
+ fetchNamespaces: jest.fn(),
+ importAll: importAllFn,
stopJobsPolling: jest.fn(),
clearJobsEtagPoll: jest.fn(),
setFilter: jest.fn(),
+ setPage: setPageFn,
},
});
@@ -66,6 +63,7 @@ describe('ImportProjectsTable', () => {
propsData: {
providerTitle,
filterable,
+ paginatable,
},
slots,
});
@@ -79,11 +77,13 @@ describe('ImportProjectsTable', () => {
});
it('renders a loading icon while repos are loading', () => {
- createComponent({
- state: {
- isLoadingRepos: true,
- },
- });
+ createComponent({ state: { isLoadingRepos: true } });
+
+ expect(wrapper.contains(GlLoadingIcon)).toBe(true);
+ });
+
+ it('renders a loading icon while namespaces are loading', () => {
+ createComponent({ state: { isLoadingNamespaces: true } });
expect(wrapper.contains(GlLoadingIcon)).toBe(true);
});
@@ -91,10 +91,16 @@ describe('ImportProjectsTable', () => {
it('renders a table with imported projects and provider repos', () => {
createComponent({
state: {
- importedProjects: [importedProject],
- providerRepos: [providerRepo],
- incompatibleRepos: [{ ...providerRepo, id: 11 }],
- namespaces: [{ path: 'path' }],
+ namespaces: [{ fullPath: 'path' }],
+ repositories: [
+ { importSource: { id: 1 }, importedProject: null, importStatus: STATUSES.NONE },
+ { importSource: { id: 2 }, importedProject: {}, importStatus: STATUSES.FINISHED },
+ {
+ importSource: { id: 3, incompatible: true },
+ importedProject: {},
+ importStatus: STATUSES.NONE,
+ },
+ ],
},
});
@@ -133,13 +139,7 @@ describe('ImportProjectsTable', () => {
);
it('renders an empty state if there are no projects available', () => {
- createComponent({
- state: {
- importedProjects: [],
- providerRepos: [],
- incompatibleProjects: [],
- },
- });
+ createComponent({ state: { repositories: [] } });
expect(wrapper.contains(ProviderRepoTableRow)).toBe(false);
expect(wrapper.contains(ImportedProjectTableRow)).toBe(false);
@@ -147,37 +147,63 @@ describe('ImportProjectsTable', () => {
});
it('sends importAll event when import button is clicked', async () => {
- createComponent({
- state: {
- providerRepos: [providerRepo],
- },
- });
+ createComponent({ state: { providerRepos: [providerRepo] } });
findImportAllButton().vm.$emit('click');
await nextTick();
- expect(eventHub.$emit).toHaveBeenCalledWith('importAll');
+
+ expect(importAllFn).toHaveBeenCalled();
});
it('shows loading spinner when import is in progress', () => {
- createComponent({
- getters: {
- isImportingAnyRepo: () => true,
- },
- });
+ createComponent({ getters: { isImportingAnyRepo: () => true } });
expect(findImportAllButton().props().loading).toBe(true);
});
it('renders filtering input field by default', () => {
createComponent();
+
expect(findFilterField().exists()).toBe(true);
});
it('does not render filtering input field when filterable is false', () => {
createComponent({ filterable: false });
+
expect(findFilterField().exists()).toBe(false);
});
+ describe('when paginatable is set to true', () => {
+ const pageInfo = { page: 1 };
+
+ beforeEach(() => {
+ createComponent({
+ state: {
+ namespaces: [{ fullPath: 'path' }],
+ pageInfo,
+ repositories: [
+ { importSource: { id: 1 }, importedProject: null, importStatus: STATUSES.NONE },
+ ],
+ },
+ paginatable: true,
+ });
+ });
+
+ it('passes current page to page-query-param-sync component', () => {
+ expect(wrapper.find(PageQueryParamSync).props().page).toBe(pageInfo.page);
+ });
+
+ it('dispatches setPage when page-query-param-sync emits popstate', () => {
+ const NEW_PAGE = 2;
+ wrapper.find(PageQueryParamSync).vm.$emit('popstate', NEW_PAGE);
+
+ const { calls } = setPageFn.mock;
+
+ expect(calls).toHaveLength(1);
+ expect(calls[0][1]).toBe(NEW_PAGE);
+ });
+ });
+
it.each`
hasIncompatibleRepos | shouldRenderSlot | action
${false} | ${false} | ${'does not render'}
diff --git a/spec/frontend/import_projects/components/imported_project_table_row_spec.js b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
index 700dd1e025a..8890c352826 100644
--- a/spec/frontend/import_projects/components/imported_project_table_row_spec.js
+++ b/spec/frontend/import_projects/components/imported_project_table_row_spec.js
@@ -1,57 +1,44 @@
-import Vuex from 'vuex';
-import { createLocalVue, mount } from '@vue/test-utils';
-import createStore from '~/import_projects/store';
-import importedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
-import STATUS_MAP from '~/import_projects/constants';
+import { mount } from '@vue/test-utils';
+import ImportedProjectTableRow from '~/import_projects/components/imported_project_table_row.vue';
+import ImportStatus from '~/import_projects/components/import_status.vue';
+import { STATUSES } from '~/import_projects/constants';
describe('ImportedProjectTableRow', () => {
- let vm;
+ let wrapper;
const project = {
- id: 1,
- fullPath: 'fullPath',
- importStatus: 'finished',
- providerLink: 'providerLink',
- importSource: 'importSource',
+ importSource: {
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ },
+ importedProject: {
+ id: 1,
+ fullPath: 'fullPath',
+ importSource: 'importSource',
+ },
+ importStatus: STATUSES.FINISHED,
};
function mountComponent() {
- const localVue = createLocalVue();
- localVue.use(Vuex);
-
- const component = mount(importedProjectTableRow, {
- localVue,
- store: createStore(),
- propsData: {
- project: {
- ...project,
- },
- },
- });
-
- return component.vm;
+ wrapper = mount(ImportedProjectTableRow, { propsData: { project } });
}
beforeEach(() => {
- vm = mountComponent();
+ mountComponent();
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders an imported project table row', () => {
- const providerLink = vm.$el.querySelector('.js-provider-link');
- const statusObject = STATUS_MAP[project.importStatus];
-
- expect(vm.$el.classList.contains('js-imported-project')).toBe(true);
- expect(providerLink.href).toMatch(project.providerLink);
- expect(providerLink.textContent).toMatch(project.importSource);
- expect(vm.$el.querySelector('.js-full-path').textContent).toMatch(project.fullPath);
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
+ const providerLink = wrapper.find('[data-testid=providerLink]');
+
+ expect(providerLink.attributes().href).toMatch(project.importSource.providerLink);
+ expect(providerLink.text()).toMatch(project.importSource.fullName);
+ expect(wrapper.find('[data-testid=fullPath]').text()).toMatch(project.importedProject.fullPath);
+ expect(wrapper.find(ImportStatus).props().status).toBe(project.importStatus);
+ expect(wrapper.find('[data-testid=goToProject').attributes().href).toMatch(
+ project.importedProject.fullPath,
);
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
- expect(vm.$el.querySelector('.js-go-to-project').href).toMatch(project.fullPath);
});
});
diff --git a/spec/frontend/import_projects/components/page_query_param_sync_spec.js b/spec/frontend/import_projects/components/page_query_param_sync_spec.js
new file mode 100644
index 00000000000..be19ecca1ba
--- /dev/null
+++ b/spec/frontend/import_projects/components/page_query_param_sync_spec.js
@@ -0,0 +1,87 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { TEST_HOST } from 'helpers/test_constants';
+
+import PageQueryParamSync from '~/import_projects/components/page_query_param_sync.vue';
+
+describe('PageQueryParamSync', () => {
+ let originalPushState;
+ let originalAddEventListener;
+ let originalRemoveEventListener;
+
+ const pushStateMock = jest.fn();
+ const addEventListenerMock = jest.fn();
+ const removeEventListenerMock = jest.fn();
+
+ beforeAll(() => {
+ window.location.search = '';
+ originalPushState = window.pushState;
+
+ window.history.pushState = pushStateMock;
+
+ originalAddEventListener = window.addEventListener;
+ window.addEventListener = addEventListenerMock;
+
+ originalRemoveEventListener = window.removeEventListener;
+ window.removeEventListener = removeEventListenerMock;
+ });
+
+ afterAll(() => {
+ window.history.pushState = originalPushState;
+ window.addEventListener = originalAddEventListener;
+ window.removeEventListener = originalRemoveEventListener;
+ });
+
+ let wrapper;
+ beforeEach(() => {
+ wrapper = shallowMount(PageQueryParamSync, {
+ propsData: { page: 3 },
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('calls push state with page number when page is updated and differs from 1', async () => {
+ wrapper.setProps({ page: 2 });
+
+ await nextTick();
+
+ const { calls } = pushStateMock.mock;
+ expect(calls).toHaveLength(1);
+ expect(calls[0][2]).toBe(`${TEST_HOST}/?page=2`);
+ });
+
+ it('calls push state without page number when page is updated and is 1', async () => {
+ wrapper.setProps({ page: 1 });
+
+ await nextTick();
+
+ const { calls } = pushStateMock.mock;
+ expect(calls).toHaveLength(1);
+ expect(calls[0][2]).toBe(`${TEST_HOST}/`);
+ });
+
+ it('subscribes to popstate event on create', () => {
+ expect(addEventListenerMock).toHaveBeenCalledWith('popstate', expect.any(Function));
+ });
+
+ it('unsubscribes from popstate event when destroyed', () => {
+ const [, fn] = addEventListenerMock.mock.calls[0];
+
+ wrapper.destroy();
+
+ expect(removeEventListenerMock).toHaveBeenCalledWith('popstate', fn);
+ });
+
+ it('emits popstate event when popstate is triggered', async () => {
+ const [, fn] = addEventListenerMock.mock.calls[0];
+
+ delete window.location;
+ window.location = new URL(`${TEST_HOST}/?page=5`);
+ fn();
+
+ expect(wrapper.emitted().popstate[0]).toStrictEqual([5]);
+ });
+});
diff --git a/spec/frontend/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
index f5e5141eac8..bd9cd07db78 100644
--- a/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_projects/components/provider_repo_table_row_spec.js
@@ -1,100 +1,100 @@
+import { nextTick } from 'vue';
import Vuex from 'vuex';
-import { createLocalVue, mount } from '@vue/test-utils';
-import { state, actions, getters, mutations } from '~/import_projects/store';
-import providerRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
-import STATUS_MAP, { STATUSES } from '~/import_projects/constants';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import ProviderRepoTableRow from '~/import_projects/components/provider_repo_table_row.vue';
+import ImportStatus from '~/import_projects/components/import_status.vue';
+import { STATUSES } from '~/import_projects/constants';
+import Select2Select from '~/vue_shared/components/select2_select.vue';
describe('ProviderRepoTableRow', () => {
- let vm;
+ let wrapper;
const fetchImport = jest.fn();
- const importPath = '/import-path';
- const defaultTargetNamespace = 'user';
- const ciCdOnly = true;
+ const setImportTarget = jest.fn();
+ const fakeImportTarget = {
+ targetNamespace: 'target',
+ newName: 'newName',
+ };
+ const ciCdOnly = false;
const repo = {
- id: 10,
- sanitizedName: 'sanitizedName',
- fullName: 'fullName',
- providerLink: 'providerLink',
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ },
+ importedProject: {
+ id: 1,
+ fullPath: 'fullPath',
+ importSource: 'importSource',
+ },
+ importStatus: STATUSES.FINISHED,
};
- function initStore(initialState) {
- const stubbedActions = { ...actions, fetchImport };
+ const availableNamespaces = [
+ { text: 'Groups', children: [{ id: 'test', text: 'test' }] },
+ { text: 'Users', children: [{ id: 'root', text: 'root' }] },
+ ];
+ function initStore(initialState) {
const store = new Vuex.Store({
- state: { ...state(), ...initialState },
- actions: stubbedActions,
- mutations,
- getters,
+ state: initialState,
+ getters: {
+ getImportTarget: () => () => fakeImportTarget,
+ },
+ actions: { fetchImport, setImportTarget },
});
return store;
}
+ const findImportButton = () =>
+ wrapper
+ .findAll('button')
+ .filter(node => node.text() === 'Import')
+ .at(0);
+
function mountComponent(initialState) {
const localVue = createLocalVue();
localVue.use(Vuex);
- const store = initStore({ importPath, defaultTargetNamespace, ciCdOnly, ...initialState });
+ const store = initStore({ ciCdOnly, ...initialState });
- const component = mount(providerRepoTableRow, {
+ wrapper = shallowMount(ProviderRepoTableRow, {
localVue,
store,
- propsData: {
- repo,
- },
+ propsData: { repo, availableNamespaces },
});
-
- return component.vm;
}
beforeEach(() => {
- vm = mountComponent();
+ mountComponent();
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders a provider repo table row', () => {
- const providerLink = vm.$el.querySelector('.js-provider-link');
- const statusObject = STATUS_MAP[STATUSES.NONE];
-
- expect(vm.$el.classList.contains('js-provider-repo')).toBe(true);
- expect(providerLink.href).toMatch(repo.providerLink);
- expect(providerLink.textContent).toMatch(repo.fullName);
- expect(vm.$el.querySelector(`.${statusObject.textClass}`).textContent).toMatch(
- statusObject.text,
- );
-
- expect(vm.$el.querySelector(`.ic-status_${statusObject.icon}`)).not.toBeNull();
- expect(vm.$el.querySelector('.js-import-button')).not.toBeNull();
+ const providerLink = wrapper.find('[data-testid=providerLink]');
+
+ expect(providerLink.attributes().href).toMatch(repo.importSource.providerLink);
+ expect(providerLink.text()).toMatch(repo.importSource.fullName);
+ expect(wrapper.find(ImportStatus).props().status).toBe(repo.importStatus);
+ expect(wrapper.contains('button')).toBe(true);
});
it('renders a select2 namespace select', () => {
- const dropdownTrigger = vm.$el.querySelector('.js-namespace-select');
-
- expect(dropdownTrigger).not.toBeNull();
- expect(dropdownTrigger.classList.contains('select2-container')).toBe(true);
-
- dropdownTrigger.click();
-
- expect(vm.$el.querySelector('.select2-drop')).not.toBeNull();
+ expect(wrapper.contains(Select2Select)).toBe(true);
+ expect(wrapper.find(Select2Select).props().options.data).toBe(availableNamespaces);
});
- it('imports repo when clicking import button', () => {
- vm.$el.querySelector('.js-import-button').click();
+ it('imports repo when clicking import button', async () => {
+ findImportButton().trigger('click');
- return vm.$nextTick().then(() => {
- const { calls } = fetchImport.mock;
+ await nextTick();
- // Not using .toBeCalledWith because it expects
- // an unmatchable and undefined 3rd argument.
- expect(calls.length).toBe(1);
- expect(calls[0][1]).toEqual({
- repo,
- newName: repo.sanitizedName,
- targetNamespace: defaultTargetNamespace,
- });
- });
+ const { calls } = fetchImport.mock;
+
+ expect(calls).toHaveLength(1);
+ expect(calls[0][1]).toBe(repo.importSource.id);
});
});
diff --git a/spec/frontend/import_projects/store/actions_spec.js b/spec/frontend/import_projects/store/actions_spec.js
index fd6fbcbfce0..45a59b3f6d6 100644
--- a/spec/frontend/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_projects/store/actions_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
-import createFlash from '~/flash';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'helpers/test_constants';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import {
@@ -12,41 +12,79 @@ import {
RECEIVE_IMPORT_SUCCESS,
RECEIVE_IMPORT_ERROR,
RECEIVE_JOBS_SUCCESS,
+ REQUEST_NAMESPACES,
+ RECEIVE_NAMESPACES_SUCCESS,
+ RECEIVE_NAMESPACES_ERROR,
+ SET_PAGE,
} from '~/import_projects/store/mutation_types';
-import {
- fetchRepos,
- fetchImport,
- receiveJobsSuccess,
- fetchJobs,
- clearJobsEtagPoll,
- stopJobsPolling,
-} from '~/import_projects/store/actions';
+import actionsFactory from '~/import_projects/store/actions';
+import { getImportTarget } from '~/import_projects/store/getters';
import state from '~/import_projects/store/state';
+import { STATUSES } from '~/import_projects/constants';
jest.mock('~/flash');
+const MOCK_ENDPOINT = `${TEST_HOST}/endpoint.json`;
+const endpoints = {
+ reposPath: MOCK_ENDPOINT,
+ importPath: MOCK_ENDPOINT,
+ jobsPath: MOCK_ENDPOINT,
+ namespacesPath: MOCK_ENDPOINT,
+};
+
+const {
+ clearJobsEtagPoll,
+ stopJobsPolling,
+ importAll,
+ fetchRepos,
+ fetchImport,
+ fetchJobs,
+ fetchNamespaces,
+ setPage,
+} = actionsFactory({
+ endpoints,
+});
+
describe('import_projects store actions', () => {
let localState;
- const repos = [{ id: 1 }, { id: 2 }];
- const importPayload = { newName: 'newName', targetNamespace: 'targetNamespace', repo: { id: 1 } };
+ const importRepoId = 1;
+ const otherImportRepoId = 2;
+ const defaultTargetNamespace = 'default';
+ const sanitizedName = 'sanitizedName';
+ const defaultImportTarget = { newName: sanitizedName, targetNamespace: defaultTargetNamespace };
beforeEach(() => {
- localState = state();
+ localState = {
+ ...state(),
+ defaultTargetNamespace,
+ repositories: [
+ { importSource: { id: importRepoId, sanitizedName }, importStatus: STATUSES.NONE },
+ {
+ importSource: { id: otherImportRepoId, sanitizedName: 's2' },
+ importStatus: STATUSES.NONE,
+ },
+ {
+ importSource: { id: 3, sanitizedName: 's3', incompatible: true },
+ importStatus: STATUSES.NONE,
+ },
+ ],
+ };
+
+ localState.getImportTarget = getImportTarget(localState);
});
describe('fetchRepos', () => {
let mock;
- const payload = { imported_projects: [{}], provider_repos: [{}], namespaces: [{}] };
+ const payload = { imported_projects: [{}], provider_repos: [{}] };
beforeEach(() => {
- localState.reposPath = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
});
afterEach(() => mock.restore());
it('dispatches stopJobsPolling actions and commits REQUEST_REPOS, RECEIVE_REPOS_SUCCESS mutations on a successful request', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, payload);
+ mock.onGet(MOCK_ENDPOINT).reply(200, payload);
return testAction(
fetchRepos,
@@ -64,7 +102,7 @@ describe('import_projects store actions', () => {
});
it('dispatches stopJobsPolling action and commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+ mock.onGet(MOCK_ENDPOINT).reply(500);
return testAction(
fetchRepos,
@@ -75,18 +113,39 @@ describe('import_projects store actions', () => {
);
});
- describe('when filtered', () => {
- beforeEach(() => {
- localState.filter = 'filter';
+ describe('when pagination is enabled', () => {
+ it('includes page in url query params', async () => {
+ const { fetchRepos: fetchReposWithPagination } = actionsFactory({
+ endpoints,
+ hasPagination: true,
+ });
+
+ let requestedUrl;
+ mock.onGet().reply(config => {
+ requestedUrl = config.url;
+ return [200, payload];
+ });
+
+ await testAction(
+ fetchReposWithPagination,
+ null,
+ localState,
+ expect.any(Array),
+ expect.any(Array),
+ );
+
+ expect(requestedUrl).toBe(`${MOCK_ENDPOINT}?page=${localState.pageInfo.page}`);
});
+ });
+ describe('when filtered', () => {
it('fetches repos with filter applied', () => {
mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, payload);
return testAction(
fetchRepos,
null,
- localState,
+ { ...localState, filter: 'filter' },
[
{ type: REQUEST_REPOS },
{
@@ -104,7 +163,6 @@ describe('import_projects store actions', () => {
let mock;
beforeEach(() => {
- localState.importPath = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
});
@@ -112,15 +170,17 @@ describe('import_projects store actions', () => {
it('commits REQUEST_IMPORT and REQUEST_IMPORT_SUCCESS mutations on a successful request', () => {
const importedProject = { name: 'imported/project' };
- const importRepoId = importPayload.repo.id;
- mock.onPost(`${TEST_HOST}/endpoint.json`).reply(200, importedProject);
+ mock.onPost(MOCK_ENDPOINT).reply(200, importedProject);
return testAction(
fetchImport,
- importPayload,
+ importRepoId,
localState,
[
- { type: REQUEST_IMPORT, payload: importRepoId },
+ {
+ type: REQUEST_IMPORT,
+ payload: { repoId: importRepoId, importTarget: defaultImportTarget },
+ },
{
type: RECEIVE_IMPORT_SUCCESS,
payload: {
@@ -134,15 +194,18 @@ describe('import_projects store actions', () => {
});
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows generic error message on an unsuccessful request', async () => {
- mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500);
+ mock.onPost(MOCK_ENDPOINT).reply(500);
await testAction(
fetchImport,
- importPayload,
+ importRepoId,
localState,
[
- { type: REQUEST_IMPORT, payload: importPayload.repo.id },
- { type: RECEIVE_IMPORT_ERROR, payload: importPayload.repo.id },
+ {
+ type: REQUEST_IMPORT,
+ payload: { repoId: importRepoId, importTarget: defaultImportTarget },
+ },
+ { type: RECEIVE_IMPORT_ERROR, payload: importRepoId },
],
[],
);
@@ -152,15 +215,18 @@ describe('import_projects store actions', () => {
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows detailed error message on an unsuccessful request with errors fields in response', async () => {
const ERROR_MESSAGE = 'dummy';
- mock.onPost(`${TEST_HOST}/endpoint.json`).reply(500, { errors: ERROR_MESSAGE });
+ mock.onPost(MOCK_ENDPOINT).reply(500, { errors: ERROR_MESSAGE });
await testAction(
fetchImport,
- importPayload,
+ importRepoId,
localState,
[
- { type: REQUEST_IMPORT, payload: importPayload.repo.id },
- { type: RECEIVE_IMPORT_ERROR, payload: importPayload.repo.id },
+ {
+ type: REQUEST_IMPORT,
+ payload: { repoId: importRepoId, importTarget: defaultImportTarget },
+ },
+ { type: RECEIVE_IMPORT_ERROR, payload: importRepoId },
],
[],
);
@@ -169,24 +235,11 @@ describe('import_projects store actions', () => {
});
});
- describe('receiveJobsSuccess', () => {
- it(`commits ${RECEIVE_JOBS_SUCCESS} mutation`, () => {
- return testAction(
- receiveJobsSuccess,
- repos,
- localState,
- [{ type: RECEIVE_JOBS_SUCCESS, payload: repos }],
- [],
- );
- });
- });
-
describe('fetchJobs', () => {
let mock;
const updatedProjects = [{ name: 'imported/project' }, { name: 'provider/repo' }];
beforeEach(() => {
- localState.jobsPath = `${TEST_HOST}/endpoint.json`;
mock = new MockAdapter(axios);
});
@@ -198,7 +251,7 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
it('commits RECEIVE_JOBS_SUCCESS mutation on a successful request', async () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(200, updatedProjects);
+ mock.onGet(MOCK_ENDPOINT).reply(200, updatedProjects);
await testAction(
fetchJobs,
@@ -237,4 +290,78 @@ describe('import_projects store actions', () => {
});
});
});
+
+ describe('fetchNamespaces', () => {
+ let mock;
+ const namespaces = [{ full_name: 'test/ns1' }, { full_name: 'test_ns2' }];
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => mock.restore());
+
+ it('commits REQUEST_NAMESPACES and RECEIVE_NAMESPACES_SUCCESS on success', async () => {
+ mock.onGet(MOCK_ENDPOINT).reply(200, namespaces);
+
+ await testAction(
+ fetchNamespaces,
+ null,
+ localState,
+ [
+ { type: REQUEST_NAMESPACES },
+ {
+ type: RECEIVE_NAMESPACES_SUCCESS,
+ payload: convertObjectPropsToCamelCase(namespaces, { deep: true }),
+ },
+ ],
+ [],
+ );
+ });
+
+ it('commits REQUEST_NAMESPACES and RECEIVE_NAMESPACES_ERROR and shows generic error message on an unsuccessful request', async () => {
+ mock.onGet(MOCK_ENDPOINT).reply(500);
+
+ await testAction(
+ fetchNamespaces,
+ null,
+ localState,
+ [{ type: REQUEST_NAMESPACES }, { type: RECEIVE_NAMESPACES_ERROR }],
+ [],
+ );
+
+ expect(createFlash).toHaveBeenCalledWith('Requesting namespaces failed');
+ });
+ });
+
+ describe('importAll', () => {
+ it('dispatches multiple fetchImport actions', async () => {
+ await testAction(
+ importAll,
+ null,
+ localState,
+ [],
+ [
+ { type: 'fetchImport', payload: importRepoId },
+ { type: 'fetchImport', payload: otherImportRepoId },
+ ],
+ );
+ });
+
+ describe('setPage', () => {
+ it('dispatches fetchRepos and commits setPage when page number differs from current one', async () => {
+ await testAction(
+ setPage,
+ 2,
+ { ...localState, pageInfo: { page: 1 } },
+ [{ type: SET_PAGE, payload: 2 }],
+ [{ type: 'fetchRepos' }],
+ );
+ });
+
+ it('does not perform any action if page equals to current one', async () => {
+ await testAction(setPage, 2, { ...localState, pageInfo: { page: 2 } }, [], []);
+ });
+ });
+ });
});
diff --git a/spec/frontend/import_projects/store/getters_spec.js b/spec/frontend/import_projects/store/getters_spec.js
index 93d1ed89783..5c1ea25a684 100644
--- a/spec/frontend/import_projects/store/getters_spec.js
+++ b/spec/frontend/import_projects/store/getters_spec.js
@@ -1,12 +1,28 @@
import {
- namespaceSelectOptions,
+ isLoading,
isImportingAnyRepo,
- hasProviderRepos,
hasIncompatibleRepos,
- hasImportedProjects,
+ hasImportableRepos,
+ getImportTarget,
} from '~/import_projects/store/getters';
+import { STATUSES } from '~/import_projects/constants';
import state from '~/import_projects/store/state';
+const IMPORTED_REPO = {
+ importSource: {},
+ importedProject: { fullPath: 'some/path' },
+};
+
+const IMPORTABLE_REPO = {
+ importSource: { id: 'some-id', sanitizedName: 'sanitized' },
+ importedProject: null,
+ importStatus: STATUSES.NONE,
+};
+
+const INCOMPATIBLE_REPO = {
+ importSource: { incompatible: true },
+};
+
describe('import_projects store getters', () => {
let localState;
@@ -14,85 +30,87 @@ describe('import_projects store getters', () => {
localState = state();
});
- describe('namespaceSelectOptions', () => {
- const namespaces = [{ fullPath: 'namespace-0' }, { fullPath: 'namespace-1' }];
- const defaultTargetNamespace = 'current-user';
-
- it('returns an options array with a "Users" and "Groups" optgroups', () => {
- localState.namespaces = namespaces;
- localState.defaultTargetNamespace = defaultTargetNamespace;
-
- const optionsArray = namespaceSelectOptions(localState);
- const groupsGroup = optionsArray[0];
- const usersGroup = optionsArray[1];
-
- expect(groupsGroup.text).toBe('Groups');
- expect(usersGroup.text).toBe('Users');
-
- groupsGroup.children.forEach((child, index) => {
- expect(child.id).toBe(namespaces[index].fullPath);
- expect(child.text).toBe(namespaces[index].fullPath);
+ it.each`
+ isLoadingRepos | isLoadingNamespaces | isLoadingValue
+ ${false} | ${false} | ${false}
+ ${true} | ${false} | ${true}
+ ${false} | ${true} | ${true}
+ ${true} | ${true} | ${true}
+ `(
+ 'isLoading returns $isLoadingValue when isLoadingRepos is $isLoadingRepos and isLoadingNamespaces is $isLoadingNamespaces',
+ ({ isLoadingRepos, isLoadingNamespaces, isLoadingValue }) => {
+ Object.assign(localState, {
+ isLoadingRepos,
+ isLoadingNamespaces,
});
- expect(usersGroup.children.length).toBe(1);
- expect(usersGroup.children[0].id).toBe(defaultTargetNamespace);
- expect(usersGroup.children[0].text).toBe(defaultTargetNamespace);
- });
- });
-
- describe('isImportingAnyRepo', () => {
- it('returns true if there are any reposBeingImported', () => {
- localState.reposBeingImported = new Array(1);
-
- expect(isImportingAnyRepo(localState)).toBe(true);
- });
+ expect(isLoading(localState)).toBe(isLoadingValue);
+ },
+ );
+
+ it.each`
+ importStatus | value
+ ${STATUSES.NONE} | ${false}
+ ${STATUSES.SCHEDULING} | ${true}
+ ${STATUSES.SCHEDULED} | ${true}
+ ${STATUSES.STARTED} | ${true}
+ ${STATUSES.FINISHED} | ${false}
+ `(
+ 'isImportingAnyRepo returns $value when repo with $importStatus status is available',
+ ({ importStatus, value }) => {
+ localState.repositories = [{ importStatus }];
+
+ expect(isImportingAnyRepo(localState)).toBe(value);
+ },
+ );
- it('returns false if there are no reposBeingImported', () => {
- localState.reposBeingImported = [];
-
- expect(isImportingAnyRepo(localState)).toBe(false);
- });
- });
-
- describe('hasProviderRepos', () => {
- it('returns true if there are any providerRepos', () => {
- localState.providerRepos = new Array(1);
+ describe('hasIncompatibleRepos', () => {
+ it('returns true if there are any incompatible projects', () => {
+ localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO, INCOMPATIBLE_REPO];
- expect(hasProviderRepos(localState)).toBe(true);
+ expect(hasIncompatibleRepos(localState)).toBe(true);
});
- it('returns false if there are no providerRepos', () => {
- localState.providerRepos = [];
+ it('returns false if there are no incompatible projects', () => {
+ localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO];
- expect(hasProviderRepos(localState)).toBe(false);
+ expect(hasIncompatibleRepos(localState)).toBe(false);
});
});
- describe('hasImportedProjects', () => {
- it('returns true if there are any importedProjects', () => {
- localState.importedProjects = new Array(1);
+ describe('hasImportableRepos', () => {
+ it('returns true if there are any importable projects ', () => {
+ localState.repositories = [IMPORTABLE_REPO, IMPORTED_REPO, INCOMPATIBLE_REPO];
- expect(hasImportedProjects(localState)).toBe(true);
+ expect(hasImportableRepos(localState)).toBe(true);
});
- it('returns false if there are no importedProjects', () => {
- localState.importedProjects = [];
+ it('returns false if there are no importable projects', () => {
+ localState.repositories = [IMPORTED_REPO, INCOMPATIBLE_REPO];
- expect(hasImportedProjects(localState)).toBe(false);
+ expect(hasImportableRepos(localState)).toBe(false);
});
});
- describe('hasIncompatibleRepos', () => {
- it('returns true if there are any incompatibleProjects', () => {
- localState.incompatibleRepos = new Array(1);
+ describe('getImportTarget', () => {
+ it('returns default value if no custom target available', () => {
+ localState.defaultTargetNamespace = 'default';
+ localState.repositories = [IMPORTABLE_REPO];
- expect(hasIncompatibleRepos(localState)).toBe(true);
+ expect(getImportTarget(localState)(IMPORTABLE_REPO.importSource.id)).toStrictEqual({
+ newName: IMPORTABLE_REPO.importSource.sanitizedName,
+ targetNamespace: localState.defaultTargetNamespace,
+ });
});
- it('returns false if there are no incompatibleProjects', () => {
- localState.incompatibleRepos = [];
+ it('returns custom import target if available', () => {
+ const fakeTarget = { newName: 'something', targetNamespace: 'ns' };
+ localState.repositories = [IMPORTABLE_REPO];
+ localState.customImportTargets[IMPORTABLE_REPO.importSource.id] = fakeTarget;
- expect(hasIncompatibleRepos(localState)).toBe(false);
+ expect(getImportTarget(localState)(IMPORTABLE_REPO.importSource.id)).toStrictEqual(
+ fakeTarget,
+ );
});
});
});
diff --git a/spec/frontend/import_projects/store/mutations_spec.js b/spec/frontend/import_projects/store/mutations_spec.js
index 505545f7aa5..3672ec9f2c0 100644
--- a/spec/frontend/import_projects/store/mutations_spec.js
+++ b/spec/frontend/import_projects/store/mutations_spec.js
@@ -1,34 +1,303 @@
import * as types from '~/import_projects/store/mutation_types';
import mutations from '~/import_projects/store/mutations';
+import { STATUSES } from '~/import_projects/constants';
describe('import_projects store mutations', () => {
- describe(`${types.RECEIVE_IMPORT_SUCCESS}`, () => {
- it('removes repoId from reposBeingImported and providerRepos, adds to importedProjects', () => {
- const repoId = 1;
- const state = {
- reposBeingImported: [repoId],
- providerRepos: [{ id: repoId }],
+ let state;
+ const SOURCE_PROJECT = {
+ id: 1,
+ full_name: 'full/name',
+ sanitized_name: 'name',
+ provider_link: 'https://demo.link/full/name',
+ };
+ const IMPORTED_PROJECT = {
+ name: 'demo',
+ importSource: 'something',
+ providerLink: 'custom-link',
+ importStatus: 'status',
+ fullName: 'fullName',
+ };
+
+ describe(`${types.SET_FILTER}`, () => {
+ it('overwrites current filter value', () => {
+ state = { filter: 'some-value' };
+ const NEW_VALUE = 'new-value';
+
+ mutations[types.SET_FILTER](state, NEW_VALUE);
+
+ expect(state.filter).toBe(NEW_VALUE);
+ });
+ });
+
+ describe(`${types.REQUEST_REPOS}`, () => {
+ it('sets repos loading flag to true', () => {
+ state = {};
+
+ mutations[types.REQUEST_REPOS](state);
+
+ expect(state.isLoadingRepos).toBe(true);
+ });
+ });
+
+ describe(`${types.RECEIVE_REPOS_SUCCESS}`, () => {
+ describe('for imported projects', () => {
+ const response = {
+ importedProjects: [IMPORTED_PROJECT],
+ providerRepos: [],
+ };
+
+ it('picks import status from response', () => {
+ state = {};
+
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+
+ expect(state.repositories[0].importStatus).toBe(IMPORTED_PROJECT.importStatus);
+ });
+
+ it('recreates importSource from response', () => {
+ state = {};
+
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+
+ expect(state.repositories[0].importSource).toStrictEqual(
+ expect.objectContaining({
+ fullName: IMPORTED_PROJECT.importSource,
+ sanitizedName: IMPORTED_PROJECT.name,
+ providerLink: IMPORTED_PROJECT.providerLink,
+ }),
+ );
+ });
+
+ it('passes project to importProject', () => {
+ state = {};
+
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+
+ expect(IMPORTED_PROJECT).toStrictEqual(
+ expect.objectContaining(state.repositories[0].importedProject),
+ );
+ });
+ });
+
+ describe('for importable projects', () => {
+ beforeEach(() => {
+ state = {};
+ const response = {
+ importedProjects: [],
+ providerRepos: [SOURCE_PROJECT],
+ };
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+ });
+
+ it('sets import status to none', () => {
+ expect(state.repositories[0].importStatus).toBe(STATUSES.NONE);
+ });
+
+ it('sets importSource to project', () => {
+ expect(state.repositories[0].importSource).toBe(SOURCE_PROJECT);
+ });
+ });
+
+ describe('for incompatible projects', () => {
+ const response = {
importedProjects: [],
+ providerRepos: [],
+ incompatibleRepos: [SOURCE_PROJECT],
};
- const importedProject = { id: repoId };
- mutations[types.RECEIVE_IMPORT_SUCCESS](state, { importedProject, repoId });
+ beforeEach(() => {
+ state = {};
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+ });
+
+ it('sets incompatible flag', () => {
+ expect(state.repositories[0].importSource.incompatible).toBe(true);
+ });
+
+ it('sets importSource to project', () => {
+ expect(state.repositories[0].importSource).toStrictEqual(
+ expect.objectContaining(SOURCE_PROJECT),
+ );
+ });
+ });
+
+ it('sets repos loading flag to false', () => {
+ const response = {
+ importedProjects: [],
+ providerRepos: [],
+ };
+ state = {};
+
+ mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
+
+ expect(state.isLoadingRepos).toBe(false);
+ });
+ });
+
+ describe(`${types.RECEIVE_REPOS_ERROR}`, () => {
+ it('sets repos loading flag to false', () => {
+ state = {};
+
+ mutations[types.RECEIVE_REPOS_ERROR](state);
+
+ expect(state.isLoadingRepos).toBe(false);
+ });
+ });
+
+ describe(`${types.REQUEST_IMPORT}`, () => {
+ beforeEach(() => {
+ const REPO_ID = 1;
+ const importTarget = { targetNamespace: 'ns', newName: 'name ' };
+ state = { repositories: [{ importSource: { id: REPO_ID } }] };
+
+ mutations[types.REQUEST_IMPORT](state, { repoId: REPO_ID, importTarget });
+ });
+
+ it(`sets status to ${STATUSES.SCHEDULING}`, () => {
+ expect(state.repositories[0].importStatus).toBe(STATUSES.SCHEDULING);
+ });
+ });
+
+ describe(`${types.RECEIVE_IMPORT_SUCCESS}`, () => {
+ beforeEach(() => {
+ const REPO_ID = 1;
+ state = { repositories: [{ importSource: { id: REPO_ID } }] };
+
+ mutations[types.RECEIVE_IMPORT_SUCCESS](state, {
+ repoId: REPO_ID,
+ importedProject: IMPORTED_PROJECT,
+ });
+ });
- expect(state.reposBeingImported.includes(repoId)).toBe(false);
- expect(state.providerRepos.some(repo => repo.id === repoId)).toBe(false);
- expect(state.importedProjects.some(repo => repo.id === repoId)).toBe(true);
+ it('sets import status', () => {
+ expect(state.repositories[0].importStatus).toBe(IMPORTED_PROJECT.importStatus);
+ });
+
+ it('sets imported project', () => {
+ expect(IMPORTED_PROJECT).toStrictEqual(
+ expect.objectContaining(state.repositories[0].importedProject),
+ );
+ });
+ });
+
+ describe(`${types.RECEIVE_IMPORT_ERROR}`, () => {
+ beforeEach(() => {
+ const REPO_ID = 1;
+ state = { repositories: [{ importSource: { id: REPO_ID } }] };
+
+ mutations[types.RECEIVE_IMPORT_ERROR](state, REPO_ID);
+ });
+
+ it(`resets import status to ${STATUSES.NONE}`, () => {
+ expect(state.repositories[0].importStatus).toBe(STATUSES.NONE);
});
});
describe(`${types.RECEIVE_JOBS_SUCCESS}`, () => {
- it('updates importStatus of existing importedProjects', () => {
+ it('updates import status of existing project', () => {
const repoId = 1;
- const state = { importedProjects: [{ id: repoId, importStatus: 'started' }] };
- const updatedProjects = [{ id: repoId, importStatus: 'finished' }];
+ state = {
+ repositories: [{ importedProject: { id: repoId }, importStatus: STATUSES.STARTED }],
+ };
+ const updatedProjects = [{ id: repoId, importStatus: STATUSES.FINISHED }];
mutations[types.RECEIVE_JOBS_SUCCESS](state, updatedProjects);
- expect(state.importedProjects[0].importStatus).toBe(updatedProjects[0].importStatus);
+ expect(state.repositories[0].importStatus).toBe(updatedProjects[0].importStatus);
+ });
+ });
+
+ describe(`${types.REQUEST_NAMESPACES}`, () => {
+ it('sets namespaces loading flag to true', () => {
+ state = {};
+
+ mutations[types.REQUEST_NAMESPACES](state);
+
+ expect(state.isLoadingNamespaces).toBe(true);
+ });
+ });
+
+ describe(`${types.RECEIVE_NAMESPACES_SUCCESS}`, () => {
+ const response = [{ fullPath: 'some/path' }];
+
+ beforeEach(() => {
+ state = {};
+ mutations[types.RECEIVE_NAMESPACES_SUCCESS](state, response);
+ });
+
+ it('stores namespaces to state', () => {
+ expect(state.namespaces).toStrictEqual(response);
+ });
+
+ it('sets namespaces loading flag to false', () => {
+ expect(state.isLoadingNamespaces).toBe(false);
+ });
+ });
+
+ describe(`${types.RECEIVE_NAMESPACES_ERROR}`, () => {
+ it('sets namespaces loading flag to false', () => {
+ state = {};
+
+ mutations[types.RECEIVE_NAMESPACES_ERROR](state);
+
+ expect(state.isLoadingNamespaces).toBe(false);
+ });
+ });
+
+ describe(`${types.SET_IMPORT_TARGET}`, () => {
+ const PROJECT = {
+ id: 2,
+ sanitizedName: 'sanitizedName',
+ };
+
+ it('stores custom target if it differs from defaults', () => {
+ state = { customImportTargets: {}, repositories: [{ importSource: PROJECT }] };
+ const importTarget = { targetNamespace: 'ns', newName: 'name ' };
+
+ mutations[types.SET_IMPORT_TARGET](state, { repoId: PROJECT.id, importTarget });
+ expect(state.customImportTargets[PROJECT.id]).toBe(importTarget);
+ });
+
+ it('removes custom target if it is equal to defaults', () => {
+ const importTarget = { targetNamespace: 'ns', newName: 'name ' };
+ state = {
+ defaultTargetNamespace: 'default',
+ customImportTargets: {
+ [PROJECT.id]: importTarget,
+ },
+ repositories: [{ importSource: PROJECT }],
+ };
+
+ mutations[types.SET_IMPORT_TARGET](state, {
+ repoId: PROJECT.id,
+ importTarget: {
+ targetNamespace: state.defaultTargetNamespace,
+ newName: PROJECT.sanitizedName,
+ },
+ });
+
+ expect(state.customImportTargets[SOURCE_PROJECT.id]).toBeUndefined();
+ });
+ });
+
+ describe(`${types.SET_PAGE_INFO}`, () => {
+ it('sets passed page info', () => {
+ state = {};
+ const pageInfo = { page: 1, total: 10 };
+
+ mutations[types.SET_PAGE_INFO](state, pageInfo);
+
+ expect(state.pageInfo).toBe(pageInfo);
+ });
+ });
+
+ describe(`${types.SET_PAGE}`, () => {
+ it('sets page number', () => {
+ const NEW_PAGE = 4;
+ state = { pageInfo: { page: 5 } };
+
+ mutations[types.SET_PAGE](state, NEW_PAGE);
+ expect(state.pageInfo.page).toBe(NEW_PAGE);
});
});
});
diff --git a/spec/frontend/import_projects/utils_spec.js b/spec/frontend/import_projects/utils_spec.js
new file mode 100644
index 00000000000..826b06d5a70
--- /dev/null
+++ b/spec/frontend/import_projects/utils_spec.js
@@ -0,0 +1,32 @@
+import { isProjectImportable } from '~/import_projects/utils';
+import { STATUSES } from '~/import_projects/constants';
+
+describe('import_projects utils', () => {
+ describe('isProjectImportable', () => {
+ it.each`
+ status | result
+ ${STATUSES.FINISHED} | ${false}
+ ${STATUSES.FAILED} | ${false}
+ ${STATUSES.SCHEDULED} | ${false}
+ ${STATUSES.STARTED} | ${false}
+ ${STATUSES.NONE} | ${true}
+ ${STATUSES.SCHEDULING} | ${false}
+ `('returns $result when project is compatible and status is $status', ({ status, result }) => {
+ expect(
+ isProjectImportable({
+ importStatus: status,
+ importSource: { incompatible: false },
+ }),
+ ).toBe(result);
+ });
+
+ it('returns false if project is not compatible', () => {
+ expect(
+ isProjectImportable({
+ importStatus: STATUSES.NONE,
+ importSource: { incompatible: true },
+ }),
+ ).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/incidents/components/incidents_list_spec.js b/spec/frontend/incidents/components/incidents_list_spec.js
new file mode 100644
index 00000000000..33ddd06d6d9
--- /dev/null
+++ b/spec/frontend/incidents/components/incidents_list_spec.js
@@ -0,0 +1,362 @@
+import { mount } from '@vue/test-utils';
+import {
+ GlAlert,
+ GlLoadingIcon,
+ GlTable,
+ GlAvatar,
+ GlPagination,
+ GlSearchBoxByType,
+ GlTab,
+ GlTabs,
+ GlBadge,
+ GlEmptyState,
+} from '@gitlab/ui';
+import { visitUrl, joinPaths, mergeUrlParams } from '~/lib/utils/url_utility';
+import IncidentsList from '~/incidents/components/incidents_list.vue';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { I18N, INCIDENT_STATUS_TABS } from '~/incidents/constants';
+import mockIncidents from '../mocks/incidents.json';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn().mockName('visitUrlMock'),
+ joinPaths: jest.fn().mockName('joinPaths'),
+ mergeUrlParams: jest.fn().mockName('mergeUrlParams'),
+}));
+
+describe('Incidents List', () => {
+ let wrapper;
+ const newIssuePath = 'namespace/project/-/issues/new';
+ const emptyListSvgPath = '/assets/empty.svg';
+ const incidentTemplateName = 'incident';
+ const incidentType = 'incident';
+ const incidentsCount = {
+ opened: 14,
+ closed: 1,
+ all: 16,
+ };
+
+ const findTable = () => wrapper.find(GlTable);
+ const findTableRows = () => wrapper.findAll('table tbody tr');
+ const findAlert = () => wrapper.find(GlAlert);
+ const findLoader = () => wrapper.find(GlLoadingIcon);
+ const findTimeAgo = () => wrapper.findAll(TimeAgoTooltip);
+ const findDateColumnHeader = () =>
+ wrapper.find('[data-testid="incident-management-created-at-sort"]');
+ const findSearch = () => wrapper.find(GlSearchBoxByType);
+ const findAssingees = () => wrapper.findAll('[data-testid="incident-assignees"]');
+ const findCreateIncidentBtn = () => wrapper.find('[data-testid="createIncidentBtn"]');
+ const findClosedIcon = () => wrapper.findAll("[data-testid='incident-closed']");
+ const findPagination = () => wrapper.find(GlPagination);
+ const findStatusFilterTabs = () => wrapper.findAll(GlTab);
+ const findStatusFilterBadge = () => wrapper.findAll(GlBadge);
+ const findStatusTabs = () => wrapper.find(GlTabs);
+ const findEmptyState = () => wrapper.find(GlEmptyState);
+
+ function mountComponent({ data = { incidents: [], incidentsCount: {} }, loading = false }) {
+ wrapper = mount(IncidentsList, {
+ data() {
+ return data;
+ },
+ mocks: {
+ $apollo: {
+ queries: {
+ incidents: {
+ loading,
+ },
+ },
+ },
+ },
+ provide: {
+ projectPath: '/project/path',
+ newIssuePath,
+ incidentTemplateName,
+ incidentType,
+ issuePath: '/project/isssues',
+ publishedAvailable: true,
+ emptyListSvgPath,
+ },
+ stubs: {
+ GlButton: true,
+ GlAvatar: true,
+ },
+ });
+ }
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ wrapper = null;
+ }
+ });
+
+ it('shows the loading state', () => {
+ mountComponent({
+ loading: true,
+ });
+ expect(findLoader().exists()).toBe(true);
+ });
+
+ it('shows empty state', () => {
+ mountComponent({
+ data: { incidents: { list: [] }, incidentsCount: {} },
+ loading: false,
+ });
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('shows error state', () => {
+ mountComponent({
+ data: { incidents: { list: [] }, incidentsCount: { all: 0 }, errored: true },
+ loading: false,
+ });
+ expect(findTable().text()).toContain(I18N.noIncidents);
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ describe('Incident Management list', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: { incidents: { list: mockIncidents }, incidentsCount },
+ loading: false,
+ });
+ });
+
+ it('renders rows based on provided data', () => {
+ expect(findTableRows().length).toBe(mockIncidents.length);
+ });
+
+ it('renders a createdAt with timeAgo component per row', () => {
+ expect(findTimeAgo().length).toBe(mockIncidents.length);
+ });
+
+ describe('Assignees', () => {
+ it('shows Unassigned when there are no assignees', () => {
+ expect(
+ findAssingees()
+ .at(0)
+ .text(),
+ ).toBe(I18N.unassigned);
+ });
+
+ it('renders an avatar component when there is an assignee', () => {
+ const avatar = findAssingees()
+ .at(1)
+ .find(GlAvatar);
+ const { src, label } = avatar.attributes();
+ const { name, avatarUrl } = mockIncidents[1].assignees.nodes[0];
+
+ expect(avatar.exists()).toBe(true);
+ expect(label).toBe(name);
+ expect(src).toBe(avatarUrl);
+ });
+
+ it('contains a link to the issue details', () => {
+ findTableRows()
+ .at(0)
+ .trigger('click');
+ expect(visitUrl).toHaveBeenCalledWith(joinPaths(`/project/isssues/`, mockIncidents[0].iid));
+ });
+
+ it('renders a closed icon for closed incidents', () => {
+ expect(findClosedIcon().length).toBe(
+ mockIncidents.filter(({ state }) => state === 'closed').length,
+ );
+ });
+ });
+ });
+
+ describe('Create Incident', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: { incidents: { list: mockIncidents }, incidentsCount: {} },
+ loading: false,
+ });
+ });
+
+ it('shows the button linking to new incidents page with prefilled incident template when clicked', () => {
+ expect(findCreateIncidentBtn().exists()).toBe(true);
+ findCreateIncidentBtn().trigger('click');
+ expect(mergeUrlParams).toHaveBeenCalledWith(
+ { issuable_template: incidentTemplateName, 'issue[issue_type]': incidentType },
+ newIssuePath,
+ );
+ });
+
+ it('sets button loading on click', () => {
+ findCreateIncidentBtn().vm.$emit('click');
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findCreateIncidentBtn().attributes('loading')).toBe('true');
+ });
+ });
+ });
+
+ describe('Pagination', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: {
+ incidents: {
+ list: mockIncidents,
+ pageInfo: { hasNextPage: true, hasPreviousPage: true },
+ },
+ incidentsCount,
+ errored: false,
+ },
+ loading: false,
+ });
+ });
+
+ it('should render pagination', () => {
+ expect(wrapper.find(GlPagination).exists()).toBe(true);
+ });
+
+ describe('prevPage', () => {
+ it('returns prevPage button', () => {
+ findPagination().vm.$emit('input', 3);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(
+ findPagination()
+ .findAll('.page-item')
+ .at(0)
+ .text(),
+ ).toBe('Prev');
+ });
+ });
+
+ it('returns prevPage number', () => {
+ findPagination().vm.$emit('input', 3);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.prevPage).toBe(2);
+ });
+ });
+
+ it('returns 0 when it is the first page', () => {
+ findPagination().vm.$emit('input', 1);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.prevPage).toBe(0);
+ });
+ });
+ });
+
+ describe('nextPage', () => {
+ it('returns nextPage button', () => {
+ findPagination().vm.$emit('input', 3);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(
+ findPagination()
+ .findAll('.page-item')
+ .at(1)
+ .text(),
+ ).toBe('Next');
+ });
+ });
+
+ it('returns nextPage number', () => {
+ mountComponent({
+ data: {
+ incidents: {
+ list: [...mockIncidents, ...mockIncidents, ...mockIncidents],
+ pageInfo: { hasNextPage: true, hasPreviousPage: true },
+ },
+ incidentsCount,
+ errored: false,
+ },
+ loading: false,
+ });
+ findPagination().vm.$emit('input', 1);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.nextPage).toBe(2);
+ });
+ });
+
+ it('returns `null` when currentPage is already last page', () => {
+ findStatusTabs().vm.$emit('input', 1);
+ findPagination().vm.$emit('input', 1);
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.vm.nextPage).toBeNull();
+ });
+ });
+ });
+
+ describe('Search', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: {
+ incidents: {
+ list: mockIncidents,
+ pageInfo: { hasNextPage: true, hasPreviousPage: true },
+ },
+ incidentsCount,
+ errored: false,
+ },
+ loading: false,
+ });
+ });
+
+ it('renders the search component for incidents', () => {
+ expect(findSearch().exists()).toBe(true);
+ });
+
+ it('sets the `searchTerm` graphql variable', () => {
+ const SEARCH_TERM = 'Simple Incident';
+
+ findSearch().vm.$emit('input', SEARCH_TERM);
+
+ expect(wrapper.vm.$data.searchTerm).toBe(SEARCH_TERM);
+ });
+ });
+
+ describe('Status Filter Tabs', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: { incidents: mockIncidents, incidentsCount },
+ loading: false,
+ stubs: {
+ GlTab: true,
+ },
+ });
+ });
+
+ it('should display filter tabs', () => {
+ const tabs = findStatusFilterTabs().wrappers;
+
+ tabs.forEach((tab, i) => {
+ expect(tab.attributes('data-testid')).toContain(INCIDENT_STATUS_TABS[i].status);
+ });
+ });
+
+ it('should display filter tabs with alerts count badge for each status', () => {
+ const tabs = findStatusFilterTabs().wrappers;
+ const badges = findStatusFilterBadge();
+
+ tabs.forEach((tab, i) => {
+ const status = INCIDENT_STATUS_TABS[i].status.toLowerCase();
+ expect(tab.attributes('data-testid')).toContain(INCIDENT_STATUS_TABS[i].status);
+ expect(badges.at(i).text()).toContain(incidentsCount[status]);
+ });
+ });
+ });
+ });
+
+ describe('sorting the incident list by column', () => {
+ beforeEach(() => {
+ mountComponent({
+ data: { incidents: mockIncidents, incidentsCount },
+ loading: false,
+ });
+ });
+
+ it('updates sort with new direction and column key', () => {
+ expect(findDateColumnHeader().attributes('aria-sort')).toBe('descending');
+
+ findDateColumnHeader().trigger('click');
+ return wrapper.vm.$nextTick(() => {
+ expect(findDateColumnHeader().attributes('aria-sort')).toBe('ascending');
+ });
+ });
+ });
+});
diff --git a/spec/frontend/incidents/mocks/incidents.json b/spec/frontend/incidents/mocks/incidents.json
new file mode 100644
index 00000000000..4eab709e53f
--- /dev/null
+++ b/spec/frontend/incidents/mocks/incidents.json
@@ -0,0 +1,39 @@
+[
+ {
+ "iid": "15",
+ "title": "New: Incident",
+ "createdAt": "2020-06-03T15:46:08Z",
+ "assignees": {},
+ "state": "opened"
+ },
+ {
+ "iid": "14",
+ "title": "Create issue4",
+ "createdAt": "2020-05-19T09:26:07Z",
+ "assignees": {
+ "nodes": [
+ {
+ "name": "Benjamin Braun",
+ "username": "kami.hegmann",
+ "avatarUrl": "https://invalid'",
+ "webUrl": "https://invalid"
+ }
+ ]
+ },
+ "state": "opened"
+ },
+ {
+ "iid": "13",
+ "title": "Create issue3",
+ "createdAt": "2020-05-19T08:53:55Z",
+ "assignees": {},
+ "state": "closed"
+ },
+ {
+ "iid": "12",
+ "title": "Create issue2",
+ "createdAt": "2020-05-18T17:13:35Z",
+ "assignees": {},
+ "state": "closed"
+ }
+]
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap
index dd3589e2951..f3f610e4bb7 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/alerts_form_spec.js.snap
@@ -81,19 +81,23 @@ exports[`Alert integration settings form default state should match the default
</gl-form-checkbox-stub>
</gl-form-group-stub>
- <gl-button-stub
- category="tertiary"
- class="js-no-auto-disable"
- data-qa-selector="save_changes_button"
- icon=""
- size="medium"
- type="submit"
- variant="success"
+ <div
+ class="gl-display-flex gl-justify-content-end"
>
+ <gl-button-stub
+ category="primary"
+ class="js-no-auto-disable"
+ data-qa-selector="save_changes_button"
+ icon=""
+ size="medium"
+ type="submit"
+ variant="success"
+ >
+
+ Save changes
- Save changes
-
- </gl-button-stub>
+ </gl-button-stub>
+ </div>
</form>
</div>
`;
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
index 5f355ee8261..3ad4c13382d 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap
@@ -9,16 +9,16 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
<div
class="settings-header"
>
- <h3
- class="h4"
+ <h4
+ class="gl-my-3! gl-py-1"
>
Incidents
- </h3>
+ </h4>
<gl-button-stub
- category="tertiary"
+ category="primary"
class="js-settings-toggle"
icon=""
size="medium"
diff --git a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
index 17ada722034..78bb238fcb6 100644
--- a/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
+++ b/spec/frontend/incidents_settings/components/__snapshots__/pagerduty_form_spec.js.snap
@@ -35,27 +35,31 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
/>
<div
- class="gl-text-gray-400 gl-pt-2"
+ class="gl-text-gray-200 gl-pt-2"
>
<gl-sprintf-stub
message="Create a GitLab issue for each PagerDuty incident by %{docsLink}"
/>
</div>
- <gl-button-stub
- category="tertiary"
- class="gl-mt-3"
- data-testid="webhook-reset-btn"
- icon=""
- role="button"
- size="medium"
- tabindex="0"
- variant="default"
+ <div
+ class="gl-display-flex gl-justify-content-end"
>
+ <gl-button-stub
+ category="primary"
+ class="gl-mt-3"
+ data-testid="webhook-reset-btn"
+ icon=""
+ role="button"
+ size="medium"
+ tabindex="0"
+ variant="default"
+ >
+
+ Reset webhook URL
- Reset webhook URL
-
- </gl-button-stub>
+ </gl-button-stub>
+ </div>
<gl-modal-stub
modalclass=""
@@ -72,18 +76,22 @@ exports[`Alert integration settings form should match the default snapshot 1`] =
</gl-modal-stub>
</gl-form-group-stub>
- <gl-button-stub
- category="tertiary"
- class="js-no-auto-disable"
- icon=""
- size="medium"
- type="submit"
- variant="success"
+ <div
+ class="gl-display-flex gl-justify-content-end"
>
+ <gl-button-stub
+ category="primary"
+ class="js-no-auto-disable"
+ icon=""
+ size="medium"
+ type="submit"
+ variant="success"
+ >
+
+ Save changes
- Save changes
-
- </gl-button-stub>
+ </gl-button-stub>
+ </div>
</form>
</div>
`;
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
index 58f9a318808..5010fc0bb5c 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
@@ -1,9 +1,9 @@
-import axios from '~/lib/utils/axios_utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service';
import { ERROR_MSG } from '~/incidents_settings/constants';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
jest.mock('~/flash');
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
index 47e2aecc108..c56b9ed2a69 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_tabs_spec.js
@@ -6,9 +6,7 @@ describe('IncidentsSettingTabs', () => {
let wrapper;
beforeEach(() => {
- wrapper = shallowMount(IncidentsSettingTabs, {
- provide: { glFeatures: { pagerdutyWebhook: true } },
- });
+ wrapper = shallowMount(IncidentsSettingTabs);
});
afterEach(() => {
diff --git a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
index 521094ad54c..50d0de8a753 100644
--- a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
+++ b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
-import PagerDutySettingsForm from '~/incidents_settings/components/pagerduty_form.vue';
import { GlAlert, GlModal } from '@gitlab/ui';
+import PagerDutySettingsForm from '~/incidents_settings/components/pagerduty_form.vue';
describe('Alert integration settings form', () => {
let wrapper;
diff --git a/spec/frontend/integrations/edit/components/dynamic_field_spec.js b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
index 3a7a0efcab7..53234419f5f 100644
--- a/spec/frontend/integrations/edit/components/dynamic_field_spec.js
+++ b/spec/frontend/integrations/edit/components/dynamic_field_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
-import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
import { GlFormGroup, GlFormCheckbox, GlFormInput, GlFormSelect, GlFormTextarea } from '@gitlab/ui';
+import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
describe('DynamicField', () => {
let wrapper;
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 482c6a439f2..f8e2eb5e7f4 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { mockIntegrationProps } from 'jest/integrations/edit/mock_data';
import { createStore } from '~/integrations/edit/store';
import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
@@ -7,7 +8,6 @@ import JiraTriggerFields from '~/integrations/edit/components/jira_trigger_field
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
-import { mockIntegrationProps } from 'jest/integrations/edit/mock_data';
describe('IntegrationForm', () => {
let wrapper;
diff --git a/spec/frontend/integrations/edit/components/trigger_fields_spec.js b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
index 41bccb8ada0..df12c70f9f5 100644
--- a/spec/frontend/integrations/edit/components/trigger_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/trigger_fields_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
-import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
import { GlFormGroup, GlFormCheckbox, GlFormInput } from '@gitlab/ui';
+import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
describe('TriggerFields', () => {
let wrapper;
diff --git a/spec/frontend/integrations/edit/store/actions_spec.js b/spec/frontend/integrations/edit/store/actions_spec.js
index c3ce6e51a3d..5356c0a411b 100644
--- a/spec/frontend/integrations/edit/store/actions_spec.js
+++ b/spec/frontend/integrations/edit/store/actions_spec.js
@@ -1,9 +1,8 @@
+import testAction from 'helpers/vuex_action_helper';
import createState from '~/integrations/edit/store/state';
import { setOverride } from '~/integrations/edit/store/actions';
import * as types from '~/integrations/edit/store/mutation_types';
-import testAction from 'helpers/vuex_action_helper';
-
describe('Integration form store actions', () => {
let state;
diff --git a/spec/frontend/issuable_form_spec.js b/spec/frontend/issuable_form_spec.js
new file mode 100644
index 00000000000..009ca28ff78
--- /dev/null
+++ b/spec/frontend/issuable_form_spec.js
@@ -0,0 +1,56 @@
+import $ from 'jquery';
+
+import IssuableForm from '~/issuable_form';
+
+function createIssuable() {
+ const instance = new IssuableForm($(document.createElement('form')));
+
+ instance.titleField = $(document.createElement('input'));
+
+ return instance;
+}
+
+describe('IssuableForm', () => {
+ let instance;
+
+ beforeEach(() => {
+ instance = createIssuable();
+ });
+
+ describe('removeWip', () => {
+ it.each`
+ prefix
+ ${'wip '}
+ ${' wIP: '}
+ ${'[WIp] '}
+ ${'wIP:'}
+ ${' [WIp]'}
+ ${'drAft '}
+ ${'draFT: '}
+ ${' [DRaft] '}
+ ${'drAft:'}
+ ${'[draFT]'}
+ ${' dRaFt - '}
+ ${'dRaFt - '}
+ ${'(draft) '}
+ ${' (DrafT)'}
+ ${'wip wip: [wip] draft draft - draft: [draft] (draft)'}
+ `('removes "$prefix" from the beginning of the title', ({ prefix }) => {
+ instance.titleField.val(`${prefix}The Issuable's Title Value`);
+
+ instance.removeWip();
+
+ expect(instance.titleField.val()).toBe("The Issuable's Title Value");
+ });
+ });
+
+ describe('addWip', () => {
+ it("properly adds the work in progress prefix to the Issuable's title", () => {
+ instance.titleField.val("The Issuable's Title Value");
+
+ instance.addWip();
+
+ expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value");
+ });
+ });
+});
diff --git a/spec/frontend/issuable_suggestions/components/item_spec.js b/spec/frontend/issuable_suggestions/components/item_spec.js
index 36799f4ee9f..ad37ccd2ca5 100644
--- a/spec/frontend/issuable_suggestions/components/item_spec.js
+++ b/spec/frontend/issuable_suggestions/components/item_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { GlTooltip, GlLink } from '@gitlab/ui';
+import { TEST_HOST } from 'jest/helpers/test_constants';
import Icon from '~/vue_shared/components/icon.vue';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
import Suggestion from '~/issuable_suggestions/components/item.vue';
import mockData from '../mock_data';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('Issuable suggestions suggestion component', () => {
let vm;
diff --git a/spec/frontend/issuables_list/components/__snapshots__/issuables_list_app_spec.js.snap b/spec/frontend/issuables_list/components/__snapshots__/issuables_list_app_spec.js.snap
index 3e445319746..c327b7de827 100644
--- a/spec/frontend/issuables_list/components/__snapshots__/issuables_list_app_spec.js.snap
+++ b/spec/frontend/issuables_list/components/__snapshots__/issuables_list_app_spec.js.snap
@@ -2,7 +2,6 @@
exports[`Issuables list component with empty issues response with all state should display a catch-all if there are no issues to show 1`] = `
<gl-empty-state-stub
- description="The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
svgpath="/emptySvg"
title="There are no issues to show"
/>
diff --git a/spec/frontend/issuables_list/components/issuable_spec.js b/spec/frontend/issuables_list/components/issuable_spec.js
index 87868b7eeff..6ede46a602a 100644
--- a/spec/frontend/issuables_list/components/issuable_spec.js
+++ b/spec/frontend/issuables_list/components/issuable_spec.js
@@ -76,8 +76,9 @@ describe('Issuable component', () => {
});
const checkExists = findFn => () => findFn().exists();
- const hasConfidentialIcon = () =>
- wrapper.findAll(GlIcon).wrappers.some(iconWrapper => iconWrapper.props('name') === 'eye-slash');
+ const hasIcon = (iconName, iconWrapper = wrapper) =>
+ iconWrapper.findAll(GlIcon).wrappers.some(icon => icon.props('name') === iconName);
+ const hasConfidentialIcon = () => hasIcon('eye-slash');
const findTaskStatus = () => wrapper.find('.task-status');
const findOpenedAgoContainer = () => wrapper.find('[data-testid="openedByMessage"]');
const findAuthor = () => wrapper.find({ ref: 'openedAgoByContainer' });
@@ -85,18 +86,20 @@ describe('Issuable component', () => {
const findMilestoneTooltip = () => findMilestone().attributes('title');
const findDueDate = () => wrapper.find('.js-due-date');
const findLabels = () => wrapper.findAll(GlLabel);
- const findWeight = () => wrapper.find('.js-weight');
+ const findWeight = () => wrapper.find('[data-testid="weight"]');
const findAssignees = () => wrapper.find(IssueAssignees);
- const findMergeRequestsCount = () => wrapper.find('.js-merge-requests');
- const findUpvotes = () => wrapper.find('.js-upvotes');
- const findDownvotes = () => wrapper.find('.js-downvotes');
- const findNotes = () => wrapper.find('.js-notes');
+ const findBlockingIssuesCount = () => wrapper.find('[data-testid="blocking-issues"]');
+ const findMergeRequestsCount = () => wrapper.find('[data-testid="merge-requests"]');
+ const findUpvotes = () => wrapper.find('[data-testid="upvotes"]');
+ const findDownvotes = () => wrapper.find('[data-testid="downvotes"]');
+ const findNotes = () => wrapper.find('[data-testid="notes-count"]');
const findBulkCheckbox = () => wrapper.find('input.selected-issuable');
const findScopedLabels = () => findLabels().filter(w => isScopedLabel({ title: w.text() }));
const findUnscopedLabels = () => findLabels().filter(w => !isScopedLabel({ title: w.text() }));
const findIssuableTitle = () => wrapper.find('[data-testid="issuable-title"]');
const findIssuableStatus = () => wrapper.find('[data-testid="issuable-status"]');
const containsJiraLogo = () => wrapper.contains('[data-testid="jira-logo"]');
+ const findHealthStatus = () => wrapper.find('.health-status');
describe('when mounted', () => {
it('initializes user popovers', () => {
@@ -181,6 +184,7 @@ describe('Issuable component', () => {
${'due date'} | ${checkExists(findDueDate)}
${'labels'} | ${checkExists(findLabels)}
${'weight'} | ${checkExists(findWeight)}
+ ${'blocking issues count'} | ${checkExists(findBlockingIssuesCount)}
${'merge request count'} | ${checkExists(findMergeRequestsCount)}
${'upvotes'} | ${checkExists(findUpvotes)}
${'downvotes'} | ${checkExists(findDownvotes)}
@@ -286,11 +290,7 @@ describe('Issuable component', () => {
it('renders milestone', () => {
expect(findMilestone().exists()).toBe(true);
- expect(
- findMilestone()
- .find('.fa-clock-o')
- .exists(),
- ).toBe(true);
+ expect(hasIcon('clock', findMilestone())).toBe(true);
expect(findMilestone().text()).toEqual(TEST_MILESTONE.title);
});
@@ -430,11 +430,12 @@ describe('Issuable component', () => {
});
describe.each`
- desc | key | finder
- ${'with merge requests count'} | ${'merge_requests_count'} | ${findMergeRequestsCount}
- ${'with upvote count'} | ${'upvotes'} | ${findUpvotes}
- ${'with downvote count'} | ${'downvotes'} | ${findDownvotes}
- ${'with notes count'} | ${'user_notes_count'} | ${findNotes}
+ desc | key | finder
+ ${'with blocking issues count'} | ${'blocking_issues_count'} | ${findBlockingIssuesCount}
+ ${'with merge requests count'} | ${'merge_requests_count'} | ${findMergeRequestsCount}
+ ${'with upvote count'} | ${'upvotes'} | ${findUpvotes}
+ ${'with downvote count'} | ${'downvotes'} | ${findDownvotes}
+ ${'with notes count'} | ${'user_notes_count'} | ${findNotes}
`('$desc', ({ key, finder }) => {
beforeEach(() => {
issuable[key] = TEST_META_COUNT;
@@ -442,7 +443,7 @@ describe('Issuable component', () => {
factory({ issuable });
});
- it('renders merge requests count', () => {
+ it('renders correct count', () => {
expect(finder().exists()).toBe(true);
expect(finder().text()).toBe(TEST_META_COUNT.toString());
expect(finder().classes('no-comments')).toBe(false);
@@ -474,4 +475,19 @@ describe('Issuable component', () => {
});
});
});
+
+ if (IS_EE) {
+ describe('with health status', () => {
+ it('renders health status tag', () => {
+ factory({ issuable });
+ expect(findHealthStatus().exists()).toBe(true);
+ });
+
+ it('does not render when health status is absent', () => {
+ issuable.health_status = null;
+ factory({ issuable });
+ expect(findHealthStatus().exists()).toBe(false);
+ });
+ });
+ }
});
diff --git a/spec/frontend/issuables_list/components/issuables_list_app_spec.js b/spec/frontend/issuables_list/components/issuables_list_app_spec.js
index 9f4995a54ee..65b87ddf6a6 100644
--- a/spec/frontend/issuables_list/components/issuables_list_app_spec.js
+++ b/spec/frontend/issuables_list/components/issuables_list_app_spec.js
@@ -4,14 +4,14 @@ import { shallowMount } from '@vue/test-utils';
import { GlEmptyState, GlPagination, GlSkeletonLoading } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'helpers/test_constants';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import IssuablesListApp from '~/issuables_list/components/issuables_list_app.vue';
import Issuable from '~/issuables_list/components/issuable.vue';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
import issueablesEventBus from '~/issuables_list/eventhub';
import { PAGE_SIZE, PAGE_SIZE_MANUAL, RELATIVE_POSITION } from '~/issuables_list/constants';
-jest.mock('~/flash', () => jest.fn());
+jest.mock('~/flash');
jest.mock('~/issuables_list/eventhub');
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
@@ -21,7 +21,7 @@ jest.mock('~/lib/utils/common_utils', () => ({
const TEST_LOCATION = `${TEST_HOST}/issues`;
const TEST_ENDPOINT = '/issues';
const TEST_CREATE_ISSUES_PATH = '/createIssue';
-const TEST_EMPTY_SVG_PATH = '/emptySvg';
+const TEST_SVG_PATH = '/emptySvg';
const setUrl = query => {
window.location.href = `${TEST_LOCATION}${query}`;
@@ -48,11 +48,15 @@ describe('Issuables list component', () => {
};
const factory = (props = { sortKey: 'priority' }) => {
+ const emptyStateMeta = {
+ createIssuePath: TEST_CREATE_ISSUES_PATH,
+ svgPath: TEST_SVG_PATH,
+ };
+
wrapper = shallowMount(IssuablesListApp, {
propsData: {
endpoint: TEST_ENDPOINT,
- createIssuePath: TEST_CREATE_ISSUES_PATH,
- emptySvgPath: TEST_EMPTY_SVG_PATH,
+ emptyStateMeta,
...props,
},
});
@@ -117,9 +121,10 @@ describe('Issuables list component', () => {
expect(wrapper.vm).toMatchObject({
// Props
canBulkEdit: false,
- createIssuePath: TEST_CREATE_ISSUES_PATH,
- emptySvgPath: TEST_EMPTY_SVG_PATH,
-
+ emptyStateMeta: {
+ createIssuePath: TEST_CREATE_ISSUES_PATH,
+ svgPath: TEST_SVG_PATH,
+ },
// Data
filters: {
state: 'opened',
diff --git a/spec/frontend/issuables_list/issuable_list_test_data.js b/spec/frontend/issuables_list/issuable_list_test_data.js
index 19d8ee7f71a..313aa15bd31 100644
--- a/spec/frontend/issuables_list/issuable_list_test_data.js
+++ b/spec/frontend/issuables_list/issuable_list_test_data.js
@@ -18,6 +18,7 @@ export const simpleIssue = {
},
assignee: null,
user_notes_count: 0,
+ blocking_issues_count: 0,
merge_requests_count: 0,
upvotes: 0,
downvotes: 0,
@@ -29,6 +30,7 @@ export const simpleIssue = {
references: {
relative: 'html-boilerplate#45',
},
+ health_status: 'on_track',
};
export const testLabels = [
diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js
index d970fd349e7..f76f42cb9ae 100644
--- a/spec/frontend/issue_show/components/app_spec.js
+++ b/spec/frontend/issue_show/components/app_spec.js
@@ -2,6 +2,7 @@ import { GlIntersectionObserver } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
+import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import axios from '~/lib/utils/axios_utils';
import { visitUrl } from '~/lib/utils/url_utility';
import '~/behaviors/markdown/render_gfm';
@@ -22,6 +23,8 @@ const zoomMeetingUrl = 'https://gitlab.zoom.us/j/95919234811';
const publishedIncidentUrl = 'https://status.com/';
describe('Issuable output', () => {
+ useMockIntersectionObserver();
+
let mock;
let realtimeRequestCount = 0;
let wrapper;
@@ -45,11 +48,6 @@ describe('Issuable output', () => {
</div>
`);
- window.IntersectionObserver = class {
- disconnect = jest.fn();
- observe = jest.fn();
- };
-
mock = new MockAdapter(axios);
mock
.onGet('/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes')
@@ -84,7 +82,6 @@ describe('Issuable output', () => {
});
afterEach(() => {
- delete window.IntersectionObserver;
mock.restore();
realtimeRequestCount = 0;
diff --git a/spec/frontend/issue_show/components/issuable_header_warnings_spec.js b/spec/frontend/issue_show/components/issuable_header_warnings_spec.js
deleted file mode 100644
index 5a166812d84..00000000000
--- a/spec/frontend/issue_show/components/issuable_header_warnings_spec.js
+++ /dev/null
@@ -1,79 +0,0 @@
-import { shallowMount, createLocalVue } from '@vue/test-utils';
-import Vuex from 'vuex';
-import IssuableHeaderWarnings from '~/issue_show/components/issuable_header_warnings.vue';
-import createStore from '~/notes/stores';
-
-const localVue = createLocalVue();
-localVue.use(Vuex);
-
-describe('IssuableHeaderWarnings', () => {
- let wrapper;
- let store;
-
- const findConfidential = () => wrapper.find('[data-testid="confidential"]');
- const findLocked = () => wrapper.find('[data-testid="locked"]');
- const confidentialIconName = () => findConfidential().attributes('name');
- const lockedIconName = () => findLocked().attributes('name');
-
- const createComponent = () => {
- wrapper = shallowMount(IssuableHeaderWarnings, { store, localVue });
- };
-
- beforeEach(() => {
- store = createStore();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- store = null;
- });
-
- describe('when confidential is on', () => {
- beforeEach(() => {
- store.state.noteableData.confidential = true;
-
- createComponent();
- });
-
- it('renders the confidential icon', () => {
- expect(confidentialIconName()).toBe('eye-slash');
- });
- });
-
- describe('when confidential is off', () => {
- beforeEach(() => {
- store.state.noteableData.confidential = false;
-
- createComponent();
- });
-
- it('does not find the component', () => {
- expect(findConfidential().exists()).toBe(false);
- });
- });
-
- describe('when discussion locked is on', () => {
- beforeEach(() => {
- store.state.noteableData.discussion_locked = true;
-
- createComponent();
- });
-
- it('renders the locked icon', () => {
- expect(lockedIconName()).toBe('lock');
- });
- });
-
- describe('when discussion locked is off', () => {
- beforeEach(() => {
- store.state.noteableData.discussion_locked = false;
-
- createComponent();
- });
-
- it('does not find the component', () => {
- expect(findLocked().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/jira_import/components/jira_import_app_spec.js b/spec/frontend/jira_import/components/jira_import_app_spec.js
index 64b4461d7b2..27314a0eb6e 100644
--- a/spec/frontend/jira_import/components/jira_import_app_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_app_spec.js
@@ -1,21 +1,26 @@
import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
-import AxiosMockAdapter from 'axios-mock-adapter';
+import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
-import axios from '~/lib/utils/axios_utils';
import JiraImportApp from '~/jira_import/components/jira_import_app.vue';
import JiraImportForm from '~/jira_import/components/jira_import_form.vue';
import JiraImportProgress from '~/jira_import/components/jira_import_progress.vue';
import JiraImportSetup from '~/jira_import/components/jira_import_setup.vue';
-import initiateJiraImportMutation from '~/jira_import/queries/initiate_jira_import.mutation.graphql';
-import getJiraUserMappingMutation from '~/jira_import/queries/get_jira_user_mapping.mutation.graphql';
-import { imports, issuesPath, jiraIntegrationPath, jiraProjects, userMappings } from '../mock_data';
+import {
+ imports,
+ issuesPath,
+ jiraIntegrationPath,
+ jiraProjects,
+ projectId,
+ projectPath,
+} from '../mock_data';
describe('JiraImportApp', () => {
- let axiosMock;
- let mutateSpy;
let wrapper;
+ const inProgressIllustration = 'in-progress-illustration.svg';
+
+ const setupIllustration = 'setup-illustration.svg';
+
const getFormComponent = () => wrapper.find(JiraImportForm);
const getProgressComponent = () => wrapper.find(JiraImportProgress);
@@ -29,28 +34,22 @@ describe('JiraImportApp', () => {
const mountComponent = ({
isJiraConfigured = true,
errorMessage = '',
- selectedProject = 'MTG',
showAlert = false,
isInProgress = false,
loading = false,
- mutate = mutateSpy,
- mountFunction = shallowMount,
} = {}) =>
- mountFunction(JiraImportApp, {
+ shallowMount(JiraImportApp, {
propsData: {
- inProgressIllustration: 'in-progress-illustration.svg',
+ inProgressIllustration,
isJiraConfigured,
issuesPath,
jiraIntegrationPath,
- projectId: '5',
- projectPath: 'gitlab-org/gitlab-test',
- setupIllustration: 'setup-illustration.svg',
+ projectId,
+ projectPath,
+ setupIllustration,
},
data() {
return {
- isSubmitting: false,
- selectedProject,
- userMappings,
errorMessage,
showAlert,
jiraImportDetails: {
@@ -64,26 +63,11 @@ describe('JiraImportApp', () => {
mocks: {
$apollo: {
loading,
- mutate,
},
},
});
- beforeEach(() => {
- axiosMock = new AxiosMockAdapter(axios);
- mutateSpy = jest.fn(() =>
- Promise.resolve({
- data: {
- jiraImportStart: { errors: [] },
- jiraImportUsers: { jiraUsers: [], errors: [] },
- },
- }),
- );
- });
-
afterEach(() => {
- axiosMock.restore();
- mutateSpy.mockRestore();
wrapper.destroy();
wrapper = null;
});
@@ -176,111 +160,84 @@ describe('JiraImportApp', () => {
});
});
- describe('import in progress screen', () => {
+ describe('import setup component', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({ isJiraConfigured: false });
+ });
+
+ it('receives the illustration', () => {
+ expect(getSetupComponent().props('illustration')).toBe(setupIllustration);
+ });
+
+ it('receives the path to the Jira integration page', () => {
+ expect(getSetupComponent().props('jiraIntegrationPath')).toBe(jiraIntegrationPath);
+ });
+ });
+
+ describe('import in progress component', () => {
beforeEach(() => {
wrapper = mountComponent({ isInProgress: true });
});
- it('shows the illustration', () => {
- expect(getProgressComponent().props('illustration')).toBe('in-progress-illustration.svg');
+ it('receives the illustration', () => {
+ expect(getProgressComponent().props('illustration')).toBe(inProgressIllustration);
});
- it('shows the name of the most recent import initiator', () => {
+ it('receives the name of the most recent import initiator', () => {
expect(getProgressComponent().props('importInitiator')).toBe('Jane Doe');
});
- it('shows the name of the most recent imported project', () => {
+ it('receives the name of the most recent imported project', () => {
expect(getProgressComponent().props('importProject')).toBe('MTG');
});
- it('shows the time of the most recent import', () => {
+ it('receives the time of the most recent import', () => {
expect(getProgressComponent().props('importTime')).toBe('2020-04-09T16:17:18+00:00');
});
- it('has the path to the issues page', () => {
+ it('receives the path to the issues page', () => {
expect(getProgressComponent().props('issuesPath')).toBe('gitlab-org/gitlab-test/-/issues');
});
});
- describe('jira import form screen', () => {
- describe('when selected project has been imported before', () => {
- it('shows jira-import::MTG-3 label since project MTG has been imported 2 time before', () => {
- wrapper = mountComponent();
-
- expect(getFormComponent().props('importLabel')).toBe('jira-import::MTG-3');
- });
-
- it('shows warning alert to explain project MTG has been imported 2 times before', () => {
- wrapper = mountComponent({ mountFunction: mount });
-
- expect(getAlert().text()).toBe(
- 'You have imported from this project 2 times before. Each new import will create duplicate issues.',
- );
- });
+ describe('import form component', () => {
+ beforeEach(() => {
+ wrapper = mountComponent();
});
- describe('when selected project has not been imported before', () => {
- beforeEach(() => {
- wrapper = mountComponent({ selectedProject: 'MJP' });
- });
-
- it('shows jira-import::MJP-1 label since project MJP has not been imported before', () => {
- expect(getFormComponent().props('importLabel')).toBe('jira-import::MJP-1');
- });
-
- it('does not show warning alert since project MJP has not been imported before', () => {
- expect(getAlert().exists()).toBe(false);
- });
+ it('receives the illustration', () => {
+ expect(getFormComponent().props('issuesPath')).toBe(issuesPath);
});
- });
- describe('initiating a Jira import', () => {
- it('calls the mutation with the expected arguments', () => {
- wrapper = mountComponent();
+ it('receives the name of the most recent import initiator', () => {
+ expect(getFormComponent().props('jiraImports')).toEqual(imports);
+ });
- const mutationArguments = {
- mutation: initiateJiraImportMutation,
- variables: {
- input: {
- jiraProjectKey: 'MTG',
- projectPath: 'gitlab-org/gitlab-test',
- usersMapping: [
- {
- jiraAccountId: 'aei23f98f-q23fj98qfj',
- gitlabId: 15,
- },
- {
- jiraAccountId: 'fu39y8t34w-rq3u289t3h4i',
- gitlabId: undefined,
- },
- ],
- },
- },
- };
+ it('receives the name of the most recent imported project', () => {
+ expect(getFormComponent().props('jiraProjects')).toEqual(jiraProjects);
+ });
- getFormComponent().vm.$emit('initiateJiraImport', 'MTG');
+ it('receives the project ID', () => {
+ expect(getFormComponent().props('projectId')).toBe(projectId);
+ });
- expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
+ it('receives the project path', () => {
+ expect(getFormComponent().props('projectPath')).toBe(projectPath);
});
- it('shows alert message with error message on error', () => {
- const mutate = jest.fn(() => Promise.reject());
+ it('shows an alert when it emits an error', async () => {
+ expect(getAlert().exists()).toBe(false);
- wrapper = mountComponent({ mutate });
+ getFormComponent().vm.$emit('error', 'There was an error');
- getFormComponent().vm.$emit('initiateJiraImport', 'MTG');
+ await Vue.nextTick();
- // One tick doesn't update the dom to the desired state so we have two ticks here
- return Vue.nextTick()
- .then(Vue.nextTick)
- .then(() => {
- expect(getAlert().text()).toBe('There was an error importing the Jira project.');
- });
+ expect(getAlert().exists()).toBe(true);
});
});
describe('alert', () => {
- it('can be dismissed', () => {
+ it('can be dismissed', async () => {
wrapper = mountComponent({
errorMessage: 'There was an error importing the Jira project.',
showAlert: true,
@@ -291,40 +248,9 @@ describe('JiraImportApp', () => {
getAlert().vm.$emit('dismiss');
- return Vue.nextTick().then(() => {
- expect(getAlert().exists()).toBe(false);
- });
- });
- });
-
- describe('on mount', () => {
- it('makes a GraphQL mutation call to get user mappings', () => {
- wrapper = mountComponent();
+ await Vue.nextTick();
- const mutationArguments = {
- mutation: getJiraUserMappingMutation,
- variables: {
- input: {
- projectPath: 'gitlab-org/gitlab-test',
- },
- },
- };
-
- expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
- });
-
- it('does not make a GraphQL mutation call to get user mappings when Jira is not configured', () => {
- wrapper = mountComponent({ isJiraConfigured: false });
-
- expect(mutateSpy).not.toHaveBeenCalled();
- });
-
- it('shows error message when there is an error with the GraphQL mutation call', () => {
- const mutate = jest.fn(() => Promise.reject());
-
- wrapper = mountComponent({ mutate });
-
- expect(getAlert().exists()).toBe(true);
+ expect(getAlert().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/jira_import/components/jira_import_form_spec.js b/spec/frontend/jira_import/components/jira_import_form_spec.js
index 685b0288e92..7cc7b40f4c8 100644
--- a/spec/frontend/jira_import/components/jira_import_form_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_form_spec.js
@@ -1,56 +1,97 @@
-import { GlButton, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui';
+import { GlAlert, GlButton, GlNewDropdown, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui';
import { getByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import JiraImportForm from '~/jira_import/components/jira_import_form.vue';
-import { issuesPath, jiraProjects, userMappings } from '../mock_data';
+import getJiraUserMappingMutation from '~/jira_import/queries/get_jira_user_mapping.mutation.graphql';
+import initiateJiraImportMutation from '~/jira_import/queries/initiate_jira_import.mutation.graphql';
+import {
+ imports,
+ issuesPath,
+ jiraProjects,
+ projectId,
+ projectPath,
+ userMappings as defaultUserMappings,
+} from '../mock_data';
describe('JiraImportForm', () => {
let axiosMock;
+ let mutateSpy;
let wrapper;
const currentUsername = 'mrgitlab';
- const importLabel = 'jira-import::MTG-1';
- const value = 'MTG';
+
+ const getAlert = () => wrapper.find(GlAlert);
const getSelectDropdown = () => wrapper.find(GlFormSelect);
+ const getContinueButton = () => wrapper.find(GlButton);
+
const getCancelButton = () => wrapper.findAll(GlButton).at(1);
+ const getLabel = () => wrapper.find(GlLabel);
+
+ const getTable = () => wrapper.find(GlTable);
+
+ const getUserDropdown = () => getTable().find(GlNewDropdown);
+
const getHeader = name => getByRole(wrapper.element, 'columnheader', { name });
- const mountComponent = ({ isSubmitting = false, mountFunction = shallowMount } = {}) =>
+ const mountComponent = ({
+ isSubmitting = false,
+ loading = false,
+ mutate = mutateSpy,
+ selectedProject = 'MTG',
+ userMappings = defaultUserMappings,
+ mountFunction = shallowMount,
+ } = {}) =>
mountFunction(JiraImportForm, {
propsData: {
- importLabel,
- isSubmitting,
issuesPath,
+ jiraImports: imports,
jiraProjects,
- projectId: '5',
- userMappings,
- value,
+ projectId,
+ projectPath,
},
data: () => ({
isFetching: false,
+ isSubmitting,
searchTerm: '',
+ selectedProject,
selectState: null,
users: [],
+ userMappings,
}),
+ mocks: {
+ $apollo: {
+ loading,
+ mutate,
+ },
+ },
currentUsername,
});
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
+ mutateSpy = jest.fn(() =>
+ Promise.resolve({
+ data: {
+ jiraImportStart: { errors: [] },
+ jiraImportUsers: { jiraUsers: [], errors: [] },
+ },
+ }),
+ );
});
afterEach(() => {
axiosMock.restore();
+ mutateSpy.mockRestore();
wrapper.destroy();
wrapper = null;
});
- describe('select dropdown', () => {
+ describe('select dropdown project selection', () => {
it('is shown', () => {
wrapper = mountComponent();
@@ -67,12 +108,34 @@ describe('JiraImportForm', () => {
});
});
- it('emits an "input" event when the input select value changes', () => {
- wrapper = mountComponent();
+ describe('when selected project has been imported before', () => {
+ it('shows jira-import::MTG-3 label since project MTG has been imported 2 time before', () => {
+ wrapper = mountComponent();
+
+ expect(getLabel().props('title')).toBe('jira-import::MTG-3');
+ });
+
+ it('shows warning alert to explain project MTG has been imported 2 times before', () => {
+ wrapper = mountComponent({ mountFunction: mount });
+
+ expect(getAlert().text()).toBe(
+ 'You have imported from this project 2 times before. Each new import will create duplicate issues.',
+ );
+ });
+ });
+
+ describe('when selected project has not been imported before', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({ selectedProject: 'MJP' });
+ });
- getSelectDropdown().vm.$emit('change', value);
+ it('shows jira-import::MJP-1 label since project MJP has not been imported before', () => {
+ expect(getLabel().props('title')).toBe('jira-import::MJP-1');
+ });
- expect(wrapper.emitted('input')[0]).toEqual([value]);
+ it('does not show warning alert since project MJP has not been imported before', () => {
+ expect(getAlert().exists()).toBe(false);
+ });
});
});
@@ -81,10 +144,6 @@ describe('JiraImportForm', () => {
wrapper = mountComponent();
});
- it('shows a label which will be applied to imported Jira projects', () => {
- expect(wrapper.find(GlLabel).props('title')).toBe(importLabel);
- });
-
it('shows a heading for the user mapping section', () => {
expect(
getByRole(wrapper.element, 'heading', { name: 'Jira-GitLab user mapping template' }),
@@ -93,7 +152,7 @@ describe('JiraImportForm', () => {
it('shows information to the user', () => {
expect(wrapper.find('p').text()).toBe(
- 'Jira users have been matched with similar GitLab users. This can be overwritten by selecting a GitLab user from the dropdown in the "GitLab username" column. If it wasn\'t possible to match a Jira user with a GitLab user, the dropdown defaults to the user conducting the import.',
+ 'Jira users have been imported from the configured Jira instance. They can be mapped by selecting a GitLab user from the dropdown in the "GitLab username" column. When the form appears, the dropdown defaults to the user conducting the import.',
);
});
});
@@ -121,13 +180,53 @@ describe('JiraImportForm', () => {
it('shows all user mappings', () => {
wrapper = mountComponent({ mountFunction: mount });
- expect(wrapper.find(GlTable).findAll('tbody tr').length).toBe(userMappings.length);
+ expect(getTable().findAll('tbody tr')).toHaveLength(2);
});
it('shows correct information in each cell', () => {
wrapper = mountComponent({ mountFunction: mount });
- expect(wrapper.find(GlTable).element).toMatchSnapshot();
+ expect(getTable().element).toMatchSnapshot();
+ });
+
+ describe('when there is no Jira->GitLab user mapping', () => {
+ it('shows the logged in user in the dropdown', () => {
+ wrapper = mountComponent({
+ mountFunction: mount,
+ userMappings: [
+ {
+ jiraAccountId: 'aei23f98f-q23fj98qfj',
+ jiraDisplayName: 'Jane Doe',
+ jiraEmail: 'janedoe@example.com',
+ gitlabId: undefined,
+ gitlabUsername: undefined,
+ },
+ ],
+ });
+
+ expect(getUserDropdown().text()).toContain(currentUsername);
+ });
+ });
+
+ describe('when there is a Jira->GitLab user mapping', () => {
+ it('shows the mapped user in the dropdown', () => {
+ const gitlabUsername = 'mai';
+
+ wrapper = mountComponent({
+ mountFunction: mount,
+ userMappings: [
+ {
+ jiraAccountId: 'aei23f98f-q23fj98qfj',
+ jiraDisplayName: 'Jane Doe',
+ jiraEmail: 'janedoe@example.com',
+ gitlabId: 14,
+ gitlabUsername,
+ },
+ ],
+ });
+
+ expect(getUserDropdown().text()).toContain(gitlabUsername);
+ });
});
});
});
@@ -137,13 +236,13 @@ describe('JiraImportForm', () => {
it('is shown', () => {
wrapper = mountComponent();
- expect(wrapper.find(GlButton).text()).toBe('Continue');
+ expect(getContinueButton().text()).toBe('Continue');
});
it('is in loading state when the form is submitting', async () => {
wrapper = mountComponent({ isSubmitting: true });
- expect(wrapper.find(GlButton).props('loading')).toBe(true);
+ expect(getContinueButton().props('loading')).toBe(true);
});
});
@@ -162,13 +261,61 @@ describe('JiraImportForm', () => {
});
});
- describe('form', () => {
- it('emits an "initiateJiraImport" event with the selected dropdown value when submitted', () => {
+ describe('submitting the form', () => {
+ it('initiates the Jira import mutation with the expected arguments', () => {
wrapper = mountComponent();
+ const mutationArguments = {
+ mutation: initiateJiraImportMutation,
+ variables: {
+ input: {
+ jiraProjectKey: 'MTG',
+ projectPath,
+ usersMapping: [
+ {
+ jiraAccountId: 'aei23f98f-q23fj98qfj',
+ gitlabId: 15,
+ },
+ {
+ jiraAccountId: 'fu39y8t34w-rq3u289t3h4i',
+ gitlabId: undefined,
+ },
+ ],
+ },
+ },
+ };
+
wrapper.find('form').trigger('submit');
- expect(wrapper.emitted('initiateJiraImport')[0]).toEqual([value]);
+ expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
+ });
+ });
+
+ describe('on mount GraphQL user mapping mutation', () => {
+ it('is called with the expected arguments', () => {
+ wrapper = mountComponent();
+
+ const mutationArguments = {
+ mutation: getJiraUserMappingMutation,
+ variables: {
+ input: {
+ projectPath,
+ },
+ },
+ };
+
+ expect(mutateSpy).toHaveBeenCalledWith(expect.objectContaining(mutationArguments));
+ });
+
+ describe('when there is an error when called', () => {
+ beforeEach(() => {
+ const mutate = jest.fn(() => Promise.reject());
+ wrapper = mountComponent({ mutate });
+ });
+
+ it('shows error message', () => {
+ expect(getAlert().exists()).toBe(true);
+ });
});
});
});
diff --git a/spec/frontend/jira_import/mock_data.js b/spec/frontend/jira_import/mock_data.js
index a7447221b15..8ea40080f32 100644
--- a/spec/frontend/jira_import/mock_data.js
+++ b/spec/frontend/jira_import/mock_data.js
@@ -3,6 +3,16 @@ import { IMPORT_STATE } from '~/jira_import/utils/jira_import_utils';
export const fullPath = 'gitlab-org/gitlab-test';
+export const issuesPath = 'gitlab-org/gitlab-test/-/issues';
+
+export const illustration = 'illustration.svg';
+
+export const jiraIntegrationPath = 'gitlab-org/gitlab-test/-/services/jira/edit';
+
+export const projectId = '5';
+
+export const projectPath = 'gitlab-org/gitlab-test';
+
export const queryDetails = {
query: getJiraImportDetailsQuery,
variables: {
@@ -71,12 +81,6 @@ export const jiraImportMutationResponse = {
},
};
-export const issuesPath = 'gitlab-org/gitlab-test/-/issues';
-
-export const jiraIntegrationPath = 'gitlab-org/gitlab-test/-/services/jira/edit';
-
-export const illustration = 'illustration.svg';
-
export const jiraProjects = [
{ text: 'My Jira Project (MJP)', value: 'MJP' },
{ text: 'My Second Jira Project (MSJP)', value: 'MSJP' },
diff --git a/spec/frontend/jobs/components/empty_state_spec.js b/spec/frontend/jobs/components/empty_state_spec.js
index c6eac4e27b3..29d0c4e07aa 100644
--- a/spec/frontend/jobs/components/empty_state_spec.js
+++ b/spec/frontend/jobs/components/empty_state_spec.js
@@ -1,12 +1,10 @@
-import Vue from 'vue';
-import component from '~/jobs/components/empty_state.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
+import EmptyState from '~/jobs/components/empty_state.vue';
describe('Empty State', () => {
- const Component = Vue.extend(component);
- let vm;
+ let wrapper;
- const props = {
+ const defaultProps = {
illustrationPath: 'illustrations/pending_job_empty.svg',
illustrationSizeClass: 'svg-430',
title: 'This job has not started yet',
@@ -14,100 +12,107 @@ describe('Empty State', () => {
variablesSettingsUrl: '',
};
+ const createWrapper = props => {
+ wrapper = mount(EmptyState, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
const content = 'This job is in pending state and is waiting to be picked by a runner';
+ const findEmptyStateImage = () => wrapper.find('img');
+ const findTitle = () => wrapper.find('[data-testid="job-empty-state-title"]');
+ const findContent = () => wrapper.find('[data-testid="job-empty-state-content"]');
+ const findAction = () => wrapper.find('[data-testid="job-empty-state-action"]');
+ const findManualVarsForm = () => wrapper.find('[data-testid="manual-vars-form"]');
+
afterEach(() => {
- vm.$destroy();
+ if (wrapper?.destroy) {
+ wrapper.destroy();
+ wrapper = null;
+ }
});
describe('renders image and title', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
- ...props,
- content,
- });
+ createWrapper();
});
- it('renders img with provided path and size', () => {
- expect(vm.$el.querySelector('img').getAttribute('src')).toEqual(props.illustrationPath);
- expect(vm.$el.querySelector('.svg-content').classList).toContain(props.illustrationSizeClass);
+ it('renders empty state image', () => {
+ expect(findEmptyStateImage().exists()).toBe(true);
});
it('renders provided title', () => {
- expect(vm.$el.querySelector('.js-job-empty-state-title').textContent.trim()).toEqual(
- props.title,
- );
+ expect(
+ findTitle()
+ .text()
+ .trim(),
+ ).toBe(defaultProps.title);
});
});
describe('with content', () => {
- it('renders content', () => {
- vm = mountComponent(Component, {
- ...props,
- content,
- });
+ beforeEach(() => {
+ createWrapper({ content });
+ });
- expect(vm.$el.querySelector('.js-job-empty-state-content').textContent.trim()).toEqual(
- content,
- );
+ it('renders content', () => {
+ expect(
+ findContent()
+ .text()
+ .trim(),
+ ).toBe(content);
});
});
describe('without content', () => {
- it('does not render content', () => {
- vm = mountComponent(Component, {
- ...props,
- });
+ beforeEach(() => {
+ createWrapper();
+ });
- expect(vm.$el.querySelector('.js-job-empty-state-content')).toBeNull();
+ it('does not render content', () => {
+ expect(findContent().exists()).toBe(false);
});
});
describe('with action', () => {
- it('renders action', () => {
- vm = mountComponent(Component, {
- ...props,
- content,
+ beforeEach(() => {
+ createWrapper({
action: {
path: 'runner',
button_title: 'Check runner',
method: 'post',
},
});
+ });
- expect(vm.$el.querySelector('.js-job-empty-state-action').getAttribute('href')).toEqual(
- 'runner',
- );
+ it('renders action', () => {
+ expect(findAction().attributes('href')).toBe('runner');
});
});
describe('without action', () => {
- it('does not render action', () => {
- vm = mountComponent(Component, {
- ...props,
- content,
+ beforeEach(() => {
+ createWrapper({
action: null,
});
+ });
- expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull();
+ it('does not render action', () => {
+ expect(findAction().exists()).toBe(false);
});
- });
- describe('without playbale action', () => {
it('does not render manual variables form', () => {
- vm = mountComponent(Component, {
- ...props,
- content,
- });
-
- expect(vm.$el.querySelector('.js-manual-vars-form')).toBeNull();
+ expect(findManualVarsForm().exists()).toBe(false);
});
});
- describe('with playbale action and not scheduled job', () => {
+ describe('with playable action and not scheduled job', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
- ...props,
+ createWrapper({
content,
playable: true,
scheduled: false,
@@ -120,22 +125,25 @@ describe('Empty State', () => {
});
it('renders manual variables form', () => {
- expect(vm.$el.querySelector('.js-manual-vars-form')).not.toBeNull();
+ expect(findManualVarsForm().exists()).toBe(true);
});
it('does not render the empty state action', () => {
- expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull();
+ expect(findAction().exists()).toBe(false);
});
});
- describe('with playbale action and scheduled job', () => {
- it('does not render manual variables form', () => {
- vm = mountComponent(Component, {
- ...props,
+ describe('with playable action and scheduled job', () => {
+ beforeEach(() => {
+ createWrapper({
+ playable: true,
+ scheduled: true,
content,
});
+ });
- expect(vm.$el.querySelector('.js-manual-vars-form')).toBeNull();
+ it('does not render manual variables form', () => {
+ expect(findManualVarsForm().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js
index d0b3d4f6247..e9ecafcd4c3 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job_app_spec.js
@@ -1,12 +1,19 @@
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { getJSONFixture } from 'helpers/fixtures';
+import { TEST_HOST } from 'jest/helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import JobApp from '~/jobs/components/job_app.vue';
+import Sidebar from '~/jobs/components/sidebar.vue';
+import StuckBlock from '~/jobs/components/stuck_block.vue';
+import UnmetPrerequisitesBlock from '~/jobs/components/unmet_prerequisites_block.vue';
+import EnvironmentsBlock from '~/jobs/components/environments_block.vue';
+import ErasedBlock from '~/jobs/components/erased_block.vue';
+import EmptyState from '~/jobs/components/empty_state.vue';
import createStore from '~/jobs/store';
import job from '../mock_data';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('Job App', () => {
const localVue = createLocalVue();
@@ -55,6 +62,26 @@ describe('Job App', () => {
.then(() => wrapper.vm.$nextTick());
};
+ const findLoadingComponent = () => wrapper.find(GlLoadingIcon);
+ const findSidebar = () => wrapper.find(Sidebar);
+ const findJobContent = () => wrapper.find('[data-testid="job-content"');
+ const findStuckBlockComponent = () => wrapper.find(StuckBlock);
+ const findStuckBlockWithTags = () => wrapper.find('[data-testid="job-stuck-with-tags"');
+ const findStuckBlockNoActiveRunners = () =>
+ wrapper.find('[data-testid="job-stuck-no-active-runners"');
+ const findFailedJobComponent = () => wrapper.find(UnmetPrerequisitesBlock);
+ const findEnvironmentsBlockComponent = () => wrapper.find(EnvironmentsBlock);
+ const findErasedBlock = () => wrapper.find(ErasedBlock);
+ const findArchivedJob = () => wrapper.find('[data-testid="archived-job"]');
+ const findEmptyState = () => wrapper.find(EmptyState);
+ const findJobNewIssueLink = () => wrapper.find('[data-testid="job-new-issue"]');
+ const findJobEmptyStateTitle = () => wrapper.find('[data-testid="job-empty-state-title"]');
+ const findJobTraceScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]');
+ const findJobTraceScrollBottom = () =>
+ wrapper.find('[data-testid="job-controller-scroll-bottom"]');
+ const findJobTraceController = () => wrapper.find('[data-testid="job-raw-link-controller"]');
+ const findJobTraceEraseLink = () => wrapper.find('[data-testid="job-log-erase-link"]');
+
beforeEach(() => {
mock = new MockAdapter(axios);
store = createStore();
@@ -72,9 +99,9 @@ describe('Job App', () => {
});
it('renders loading icon', () => {
- expect(wrapper.find('.js-job-loading').exists()).toBe(true);
- expect(wrapper.find('.js-job-sidebar').exists()).toBe(false);
- expect(wrapper.find('.js-job-content').exists()).toBe(false);
+ expect(findLoadingComponent().exists()).toBe(true);
+ expect(findSidebar().exists()).toBe(false);
+ expect(findJobContent().exists()).toBe(false);
});
});
@@ -115,7 +142,7 @@ describe('Job App', () => {
});
it('should render new issue link', () => {
- expect(wrapper.find('.js-new-issue').attributes('href')).toEqual(job.new_issue_path);
+ expect(findJobNewIssueLink().attributes('href')).toEqual(job.new_issue_path);
});
});
@@ -134,7 +161,7 @@ describe('Job App', () => {
});
describe('stuck block', () => {
- describe('without active runners availabl', () => {
+ describe('without active runners available', () => {
it('renders stuck block when there are no runners', () =>
setupAndMount({
jobData: {
@@ -153,8 +180,8 @@ describe('Job App', () => {
tags: [],
},
}).then(() => {
- expect(wrapper.find('.js-job-stuck').exists()).toBe(true);
- expect(wrapper.find('.js-job-stuck .js-stuck-no-active-runner').exists()).toBe(true);
+ expect(findStuckBlockComponent().exists()).toBe(true);
+ expect(findStuckBlockNoActiveRunners().exists()).toBe(true);
}));
});
@@ -176,8 +203,8 @@ describe('Job App', () => {
},
},
}).then(() => {
- expect(wrapper.find('.js-job-stuck').text()).toContain(job.tags[0]);
- expect(wrapper.find('.js-job-stuck .js-stuck-with-tags').exists()).toBe(true);
+ expect(findStuckBlockComponent().text()).toContain(job.tags[0]);
+ expect(findStuckBlockWithTags().exists()).toBe(true);
}));
});
@@ -199,8 +226,8 @@ describe('Job App', () => {
},
},
}).then(() => {
- expect(wrapper.find('.js-job-stuck').text()).toContain(job.tags[0]);
- expect(wrapper.find('.js-job-stuck .js-stuck-with-tags').exists()).toBe(true);
+ expect(findStuckBlockComponent().text()).toContain(job.tags[0]);
+ expect(findStuckBlockWithTags().exists()).toBe(true);
}));
});
@@ -210,7 +237,7 @@ describe('Job App', () => {
runners: { available: true },
},
}).then(() => {
- expect(wrapper.find('.js-job-stuck').exists()).toBe(false);
+ expect(findStuckBlockComponent().exists()).toBe(false);
}));
});
@@ -239,7 +266,7 @@ describe('Job App', () => {
tags: [],
},
}).then(() => {
- expect(wrapper.find('.js-job-failed').exists()).toBe(true);
+ expect(findFailedJobComponent().exists()).toBe(true);
}));
});
@@ -255,12 +282,12 @@ describe('Job App', () => {
},
},
}).then(() => {
- expect(wrapper.find('.js-job-environment').exists()).toBe(true);
+ expect(findEnvironmentsBlockComponent().exists()).toBe(true);
}));
it('does not render environment block when job has environment', () =>
setupAndMount().then(() => {
- expect(wrapper.find('.js-job-environment').exists()).toBe(false);
+ expect(findEnvironmentsBlockComponent().exists()).toBe(false);
}));
});
@@ -275,7 +302,7 @@ describe('Job App', () => {
erased_at: '2016-11-07T11:11:16.525Z',
},
}).then(() => {
- expect(wrapper.find('.js-job-erased-block').exists()).toBe(true);
+ expect(findErasedBlock().exists()).toBe(true);
}));
it('does not render erased block when `erased` is false', () =>
@@ -284,7 +311,7 @@ describe('Job App', () => {
erased_at: null,
},
}).then(() => {
- expect(wrapper.find('.js-job-erased-block').exists()).toBe(false);
+ expect(findErasedBlock().exists()).toBe(false);
}));
});
@@ -313,7 +340,7 @@ describe('Job App', () => {
},
},
}).then(() => {
- expect(wrapper.find('.js-job-empty-state').exists()).toBe(true);
+ expect(findEmptyState().exists()).toBe(true);
}));
it('does not render empty state when job does not have trace but it is running', () =>
@@ -329,12 +356,12 @@ describe('Job App', () => {
},
},
}).then(() => {
- expect(wrapper.find('.js-job-empty-state').exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
}));
it('does not render empty state when job has trace but it is not running', () =>
setupAndMount({ jobData: { has_trace: true } }).then(() => {
- expect(wrapper.find('.js-job-empty-state').exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(false);
}));
it('displays remaining time for a delayed job', () => {
@@ -345,9 +372,9 @@ describe('Job App', () => {
() => new Date(delayedJobFixture.scheduled_at).getTime() - oneHourInMilliseconds,
);
return setupAndMount({ jobData: delayedJobFixture }).then(() => {
- expect(wrapper.find('.js-job-empty-state').exists()).toBe(true);
+ expect(findEmptyState().exists()).toBe(true);
- const title = wrapper.find('.js-job-empty-state-title').text();
+ const title = findJobEmptyStateTitle().text();
expect(title).toEqual('This is a delayed job to run in 01:00:00');
});
@@ -386,7 +413,7 @@ describe('Job App', () => {
beforeEach(() => setupAndMount({ jobData: { archived: true } }));
it('renders warning about job being archived', () => {
- expect(wrapper.find('.js-archived-job ').exists()).toBe(true);
+ expect(findArchivedJob().exists()).toBe(true);
});
});
@@ -394,7 +421,7 @@ describe('Job App', () => {
beforeEach(() => setupAndMount());
it('does not warning about job being archived', () => {
- expect(wrapper.find('.js-archived-job ').exists()).toBe(false);
+ expect(findArchivedJob().exists()).toBe(false);
});
});
@@ -413,16 +440,16 @@ describe('Job App', () => {
);
it('should render scroll buttons', () => {
- expect(wrapper.find('.js-scroll-top').exists()).toBe(true);
- expect(wrapper.find('.js-scroll-bottom').exists()).toBe(true);
+ expect(findJobTraceScrollTop().exists()).toBe(true);
+ expect(findJobTraceScrollBottom().exists()).toBe(true);
});
it('should render link to raw ouput', () => {
- expect(wrapper.find('.js-raw-link-controller').exists()).toBe(true);
+ expect(findJobTraceController().exists()).toBe(true);
});
it('should render link to erase job', () => {
- expect(wrapper.find('.js-erase-link').exists()).toBe(true);
+ expect(findJobTraceEraseLink().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/jobs/components/job_log_controllers_spec.js b/spec/frontend/jobs/components/job_log_controllers_spec.js
index 04f20811601..233cef05622 100644
--- a/spec/frontend/jobs/components/job_log_controllers_spec.js
+++ b/spec/frontend/jobs/components/job_log_controllers_spec.js
@@ -1,16 +1,17 @@
-import Vue from 'vue';
-import component from '~/jobs/components/job_log_controllers.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import { mount } from '@vue/test-utils';
+import JobLogControllers from '~/jobs/components/job_log_controllers.vue';
describe('Job log controllers', () => {
- const Component = Vue.extend(component);
- let vm;
+ let wrapper;
afterEach(() => {
- vm.$destroy();
+ if (wrapper?.destroy) {
+ wrapper.destroy();
+ wrapper = null;
+ }
});
- const props = {
+ const defaultProps = {
rawPath: '/raw',
erasePath: '/erase',
size: 511952,
@@ -20,70 +21,80 @@ describe('Job log controllers', () => {
isTraceSizeVisible: true,
};
+ const createWrapper = props => {
+ wrapper = mount(JobLogControllers, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ const findTruncatedInfo = () => wrapper.find('[data-testid="log-truncated-info"]');
+ const findRawLink = () => wrapper.find('[data-testid="raw-link"]');
+ const findRawLinkController = () => wrapper.find('[data-testid="job-raw-link-controller"]');
+ const findEraseLink = () => wrapper.find('[data-testid="job-log-erase-link"]');
+ const findScrollTop = () => wrapper.find('[data-testid="job-controller-scroll-top"]');
+ const findScrollBottom = () => wrapper.find('[data-testid="job-controller-scroll-bottom"]');
+
describe('Truncate information', () => {
describe('with isTraceSizeVisible', () => {
beforeEach(() => {
- vm = mountComponent(Component, props);
+ createWrapper();
});
it('renders size information', () => {
- expect(vm.$el.querySelector('.js-truncated-info').textContent).toContain('499.95 KiB');
+ expect(findTruncatedInfo().text()).toMatch('499.95 KiB');
});
it('renders link to raw trace', () => {
- expect(vm.$el.querySelector('.js-raw-link').getAttribute('href')).toEqual('/raw');
+ expect(findRawLink().attributes('href')).toBe(defaultProps.rawPath);
});
});
});
describe('links section', () => {
describe('with raw trace path', () => {
- it('renders raw trace link', () => {
- vm = mountComponent(Component, props);
+ beforeEach(() => {
+ createWrapper();
+ });
- expect(vm.$el.querySelector('.js-raw-link-controller').getAttribute('href')).toEqual(
- '/raw',
- );
+ it('renders raw trace link', () => {
+ expect(findRawLinkController().attributes('href')).toBe(defaultProps.rawPath);
});
});
describe('without raw trace path', () => {
- it('does not render raw trace link', () => {
- vm = mountComponent(Component, {
- erasePath: '/erase',
- size: 511952,
- isScrollTopDisabled: true,
- isScrollBottomDisabled: true,
- isScrollingDown: false,
- isTraceSizeVisible: true,
+ beforeEach(() => {
+ createWrapper({
+ rawPath: null,
});
+ });
- expect(vm.$el.querySelector('.js-raw-link-controller')).toBeNull();
+ it('does not render raw trace link', () => {
+ expect(findRawLinkController().exists()).toBe(false);
});
});
describe('when is erasable', () => {
beforeEach(() => {
- vm = mountComponent(Component, props);
+ createWrapper();
});
it('renders erase job link', () => {
- expect(vm.$el.querySelector('.js-erase-link')).not.toBeNull();
+ expect(findEraseLink().exists()).toBe(true);
});
});
describe('when it is not erasable', () => {
- it('does not render erase button', () => {
- vm = mountComponent(Component, {
- rawPath: '/raw',
- size: 511952,
- isScrollTopDisabled: true,
- isScrollBottomDisabled: true,
- isScrollingDown: false,
- isTraceSizeVisible: true,
+ beforeEach(() => {
+ createWrapper({
+ erasePath: null,
});
+ });
- expect(vm.$el.querySelector('.js-erase-link')).toBeNull();
+ it('does not render erase button', () => {
+ expect(findEraseLink().exists()).toBe(false);
});
});
});
@@ -92,45 +103,39 @@ describe('Job log controllers', () => {
describe('scroll top button', () => {
describe('when user can scroll top', () => {
beforeEach(() => {
- vm = mountComponent(Component, props);
+ createWrapper({
+ isScrollTopDisabled: false,
+ });
});
- it('renders enabled scroll top button', () => {
- expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toBeNull();
- });
+ it('emits scrollJobLogTop event on click', async () => {
+ findScrollTop().trigger('click');
- it('emits scrollJobLogTop event on click', () => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- vm.$el.querySelector('.js-scroll-top').click();
+ await wrapper.vm.$nextTick();
- expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogTop');
+ expect(wrapper.emitted().scrollJobLogTop).toHaveLength(1);
});
});
describe('when user can not scroll top', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
- rawPath: '/raw',
- erasePath: '/erase',
- size: 511952,
+ createWrapper({
isScrollTopDisabled: true,
isScrollBottomDisabled: false,
isScrollingDown: false,
- isTraceSizeVisible: true,
});
});
it('renders disabled scroll top button', () => {
- expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toEqual(
- 'disabled',
- );
+ expect(findScrollTop().attributes('disabled')).toBe('disabled');
});
- it('does not emit scrollJobLogTop event on click', () => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- vm.$el.querySelector('.js-scroll-top').click();
+ it('does not emit scrollJobLogTop event on click', async () => {
+ findScrollTop().trigger('click');
- expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogTop');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.emitted().scrollJobLogTop).toBeUndefined();
});
});
});
@@ -138,69 +143,61 @@ describe('Job log controllers', () => {
describe('scroll bottom button', () => {
describe('when user can scroll bottom', () => {
beforeEach(() => {
- vm = mountComponent(Component, props);
+ createWrapper();
});
- it('renders enabled scroll bottom button', () => {
- expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toBeNull();
- });
+ it('emits scrollJobLogBottom event on click', async () => {
+ findScrollBottom().trigger('click');
- it('emits scrollJobLogBottom event on click', () => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- vm.$el.querySelector('.js-scroll-bottom').click();
+ await wrapper.vm.$nextTick();
- expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogBottom');
+ expect(wrapper.emitted().scrollJobLogBottom).toHaveLength(1);
});
});
describe('when user can not scroll bottom', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
- rawPath: '/raw',
- erasePath: '/erase',
- size: 511952,
+ createWrapper({
isScrollTopDisabled: false,
isScrollBottomDisabled: true,
isScrollingDown: false,
- isTraceSizeVisible: true,
});
});
it('renders disabled scroll bottom button', () => {
- expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toEqual(
- 'disabled',
- );
+ expect(findScrollBottom().attributes('disabled')).toEqual('disabled');
});
- it('does not emit scrollJobLogBottom event on click', () => {
- jest.spyOn(vm, '$emit').mockImplementation(() => {});
- vm.$el.querySelector('.js-scroll-bottom').click();
+ it('does not emit scrollJobLogBottom event on click', async () => {
+ findScrollBottom().trigger('click');
- expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogBottom');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.emitted().scrollJobLogBottom).toBeUndefined();
});
});
describe('while isScrollingDown is true', () => {
- it('renders animate class for the scroll down button', () => {
- vm = mountComponent(Component, props);
+ beforeEach(() => {
+ createWrapper();
+ });
- expect(vm.$el.querySelector('.js-scroll-bottom').className).toContain('animate');
+ it('renders animate class for the scroll down button', () => {
+ expect(findScrollBottom().classes()).toContain('animate');
});
});
describe('while isScrollingDown is false', () => {
- it('does not render animate class for the scroll down button', () => {
- vm = mountComponent(Component, {
- rawPath: '/raw',
- erasePath: '/erase',
- size: 511952,
+ beforeEach(() => {
+ createWrapper({
isScrollTopDisabled: true,
isScrollBottomDisabled: false,
isScrollingDown: false,
- isTraceSizeVisible: true,
});
+ });
- expect(vm.$el.querySelector('.js-scroll-bottom').className).not.toContain('animate');
+ it('does not render animate class for the scroll down button', () => {
+ expect(findScrollBottom().classes()).not.toContain('animate');
});
});
});
diff --git a/spec/frontend/jobs/components/log/mock_data.js b/spec/frontend/jobs/components/log/mock_data.js
index a6a767f7921..eb8c4fe8bc9 100644
--- a/spec/frontend/jobs/components/log/mock_data.js
+++ b/spec/frontend/jobs/components/log/mock_data.js
@@ -34,7 +34,7 @@ export const utilsMockData = [
content: [
{
text:
- 'Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.27-lfs-2.9-chrome-83-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34',
+ 'Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.28-lfs-2.9-chrome-84-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34',
},
],
section: 'prepare-executor',
diff --git a/spec/frontend/jobs/components/sidebar_spec.js b/spec/frontend/jobs/components/sidebar_spec.js
index 0c8e2dc3aef..48788df0c93 100644
--- a/spec/frontend/jobs/components/sidebar_spec.js
+++ b/spec/frontend/jobs/components/sidebar_spec.js
@@ -59,11 +59,13 @@ describe('Sidebar details block', () => {
describe('actions', () => {
it('should render link to new issue', () => {
- expect(vm.$el.querySelector('.js-new-issue').getAttribute('href')).toEqual(
+ expect(vm.$el.querySelector('[data-testid="job-new-issue"]').getAttribute('href')).toEqual(
job.new_issue_path,
);
- expect(vm.$el.querySelector('.js-new-issue').textContent.trim()).toEqual('New issue');
+ expect(vm.$el.querySelector('[data-testid="job-new-issue"]').textContent.trim()).toEqual(
+ 'New issue',
+ );
});
it('should render link to retry job', () => {
diff --git a/spec/frontend/jobs/components/stuck_block_spec.js b/spec/frontend/jobs/components/stuck_block_spec.js
index c320793b2be..926286bf75a 100644
--- a/spec/frontend/jobs/components/stuck_block_spec.js
+++ b/spec/frontend/jobs/components/stuck_block_spec.js
@@ -1,31 +1,50 @@
-import Vue from 'vue';
-import component from '~/jobs/components/stuck_block.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import { GlBadge, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import StuckBlock from '~/jobs/components/stuck_block.vue';
describe('Stuck Block Job component', () => {
- const Component = Vue.extend(component);
- let vm;
+ let wrapper;
afterEach(() => {
- vm.$destroy();
+ if (wrapper?.destroy) {
+ wrapper.destroy();
+ wrapper = null;
+ }
});
+ const createWrapper = props => {
+ wrapper = shallowMount(StuckBlock, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const tags = ['docker', 'gitlab-org'];
+
+ const findStuckNoActiveRunners = () =>
+ wrapper.find('[data-testid="job-stuck-no-active-runners"]');
+ const findStuckNoRunners = () => wrapper.find('[data-testid="job-stuck-no-runners"]');
+ const findStuckWithTags = () => wrapper.find('[data-testid="job-stuck-with-tags"]');
+ const findRunnerPathLink = () => wrapper.find(GlLink);
+ const findAllBadges = () => wrapper.findAll(GlBadge);
+
describe('with no runners for project', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
+ createWrapper({
hasNoRunnersForProject: true,
runnersPath: '/root/project/runners#js-runners-settings',
});
});
it('renders only information about project not having runners', () => {
- expect(vm.$el.querySelector('.js-stuck-no-runners')).not.toBeNull();
- expect(vm.$el.querySelector('.js-stuck-with-tags')).toBeNull();
- expect(vm.$el.querySelector('.js-stuck-no-active-runner')).toBeNull();
+ expect(findStuckNoRunners().exists()).toBe(true);
+ expect(findStuckWithTags().exists()).toBe(false);
+ expect(findStuckNoActiveRunners().exists()).toBe(false);
});
it('renders link to runners page', () => {
- expect(vm.$el.querySelector('.js-runners-path').getAttribute('href')).toEqual(
+ expect(findRunnerPathLink().attributes('href')).toBe(
'/root/project/runners#js-runners-settings',
);
});
@@ -33,26 +52,27 @@ describe('Stuck Block Job component', () => {
describe('with tags', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
+ createWrapper({
hasNoRunnersForProject: false,
- tags: ['docker', 'gitlab-org'],
+ tags,
runnersPath: '/root/project/runners#js-runners-settings',
});
});
it('renders information about the tags not being set', () => {
- expect(vm.$el.querySelector('.js-stuck-no-runners')).toBeNull();
- expect(vm.$el.querySelector('.js-stuck-with-tags')).not.toBeNull();
- expect(vm.$el.querySelector('.js-stuck-no-active-runner')).toBeNull();
+ expect(findStuckWithTags().exists()).toBe(true);
+ expect(findStuckNoActiveRunners().exists()).toBe(false);
+ expect(findStuckNoRunners().exists()).toBe(false);
});
it('renders tags', () => {
- expect(vm.$el.textContent).toContain('docker');
- expect(vm.$el.textContent).toContain('gitlab-org');
+ findAllBadges().wrappers.forEach((badgeElt, index) => {
+ return expect(badgeElt.text()).toBe(tags[index]);
+ });
});
it('renders link to runners page', () => {
- expect(vm.$el.querySelector('.js-runners-path').getAttribute('href')).toEqual(
+ expect(findRunnerPathLink().attributes('href')).toBe(
'/root/project/runners#js-runners-settings',
);
});
@@ -60,20 +80,20 @@ describe('Stuck Block Job component', () => {
describe('without active runners', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
+ createWrapper({
hasNoRunnersForProject: false,
runnersPath: '/root/project/runners#js-runners-settings',
});
});
it('renders information about project not having runners', () => {
- expect(vm.$el.querySelector('.js-stuck-no-runners')).toBeNull();
- expect(vm.$el.querySelector('.js-stuck-with-tags')).toBeNull();
- expect(vm.$el.querySelector('.js-stuck-no-active-runner')).not.toBeNull();
+ expect(findStuckNoActiveRunners().exists()).toBe(true);
+ expect(findStuckNoRunners().exists()).toBe(false);
+ expect(findStuckWithTags().exists()).toBe(false);
});
it('renders link to runners page', () => {
- expect(vm.$el.querySelector('.js-runners-path').getAttribute('href')).toEqual(
+ expect(findRunnerPathLink().attributes('href')).toBe(
'/root/project/runners#js-runners-settings',
);
});
diff --git a/spec/frontend/labels_select_spec.js b/spec/frontend/labels_select_spec.js
index 8b08eb9e124..cbc9a923f8b 100644
--- a/spec/frontend/labels_select_spec.js
+++ b/spec/frontend/labels_select_spec.js
@@ -29,7 +29,7 @@ const mockScopedLabels2 = [
title: 'Foo::Bar2',
description: 'Foobar2',
color: '#FFFFFF',
- text_color: '#000000',
+ text_color: '#333333',
},
];
@@ -61,10 +61,11 @@ describe('LabelsSelect', () => {
expect($labelEl.find('a').attr('title')).toBe(label.description);
});
- it('generated label item template has correct label styles', () => {
+ it('generated label item template has correct label styles and classes', () => {
expect($labelEl.find('span.gl-label-text').attr('style')).toBe(
- `background-color: ${label.color}; color: ${label.text_color};`,
+ `background-color: ${label.color};`,
);
+ expect($labelEl.find('span.gl-label-text')).toHaveClass('gl-label-text-light');
});
it('generated label item has a gl-label-text class', () => {
@@ -100,16 +101,12 @@ describe('LabelsSelect', () => {
expect($labelEl.find('a').attr('data-html')).toBe('true');
});
- it('generated label item template has correct label styles', () => {
+ it('generated label item template has correct label styles and classes', () => {
expect($labelEl.find('span.gl-label-text').attr('style')).toBe(
- `background-color: ${label.color}; color: ${label.text_color};`,
+ `background-color: ${label.color};`,
);
- expect(
- $labelEl
- .find('span.gl-label-text')
- .last()
- .attr('style'),
- ).toBe(`color: ${label.color};`);
+ expect($labelEl.find('span.gl-label-text')).toHaveClass('gl-label-text-light');
+ expect($labelEl.find('span.gl-label-text').last()).not.toHaveClass('gl-label-text-light');
});
it('generated label item has a badge class', () => {
@@ -131,16 +128,12 @@ describe('LabelsSelect', () => {
);
});
- it('generated label item template has correct label styles', () => {
+ it('generated label item template has correct label styles and classes', () => {
expect($labelEl.find('span.gl-label-text').attr('style')).toBe(
- `background-color: ${label.color}; color: ${label.text_color};`,
+ `background-color: ${label.color};`,
);
- expect(
- $labelEl
- .find('span.gl-label-text')
- .last()
- .attr('style'),
- ).toBe(`color: ${label.text_color};`);
+ expect($labelEl.find('span.gl-label-text')).toHaveClass('gl-label-text-dark');
+ expect($labelEl.find('span.gl-label-text').last()).toHaveClass('gl-label-text-dark');
});
});
});
diff --git a/spec/frontend/lazy_loader_spec.js b/spec/frontend/lazy_loader_spec.js
index 79a49aedf37..5eb09bc2359 100644
--- a/spec/frontend/lazy_loader_spec.js
+++ b/spec/frontend/lazy_loader_spec.js
@@ -1,8 +1,8 @@
import { noop } from 'lodash';
-import LazyLoader from '~/lazy_loader';
import { TEST_HOST } from 'helpers/test_constants';
-import waitForPromises from './helpers/wait_for_promises';
import { useMockMutationObserver, useMockIntersectionObserver } from 'helpers/mock_dom_observer';
+import LazyLoader from '~/lazy_loader';
+import waitForPromises from './helpers/wait_for_promises';
const execImmediately = callback => {
callback();
@@ -45,10 +45,24 @@ describe('LazyLoader', () => {
return newImg;
};
+ const mockLoadEvent = () => {
+ const addEventListener = window.addEventListener.bind(window);
+
+ jest.spyOn(window, 'addEventListener').mockImplementation((event, callback) => {
+ if (event === 'load') {
+ callback();
+ } else {
+ addEventListener(event, callback);
+ }
+ });
+ };
+
beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(execImmediately);
jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
jest.spyOn(LazyLoader, 'loadImage');
+
+ mockLoadEvent();
});
afterEach(() => {
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 585f0de9cc3..effc446d846 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1,5 +1,5 @@
-import * as commonUtils from '~/lib/utils/common_utils';
import $ from 'jquery';
+import * as commonUtils from '~/lib/utils/common_utils';
describe('common_utils', () => {
describe('parseUrl', () => {
diff --git a/spec/frontend/lib/utils/csrf_token_spec.js b/spec/frontend/lib/utils/csrf_token_spec.js
index 1b98ef126e9..55dd29571c0 100644
--- a/spec/frontend/lib/utils/csrf_token_spec.js
+++ b/spec/frontend/lib/utils/csrf_token_spec.js
@@ -1,5 +1,5 @@
-import csrf from '~/lib/utils/csrf';
import { setHTMLFixture } from 'helpers/fixtures';
+import csrf from '~/lib/utils/csrf';
describe('csrf', () => {
let testContext;
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index adf5c312149..9eb5587e83c 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -1,6 +1,6 @@
-import { __, s__ } from '~/locale';
import $ from 'jquery';
import timezoneMock from 'timezone-mock';
+import { __, s__ } from '~/locale';
import '~/commons/bootstrap';
import * as datetimeUtility from '~/lib/utils/datetime_utility';
@@ -628,3 +628,28 @@ describe('localTimeAgo', () => {
expect(element.getAttribute('title')).toBe(title);
});
});
+
+describe('dateFromParams', () => {
+ it('returns the expected date object', () => {
+ const expectedDate = new Date('2019-07-17T00:00:00.000Z');
+ const date = datetimeUtility.dateFromParams(2019, 6, 17);
+
+ expect(date.getYear()).toBe(expectedDate.getYear());
+ expect(date.getMonth()).toBe(expectedDate.getMonth());
+ expect(date.getDate()).toBe(expectedDate.getDate());
+ });
+});
+
+describe('differenceInSeconds', () => {
+ const startDateTime = new Date('2019-07-17T00:00:00.000Z');
+
+ it.each`
+ startDate | endDate | expected
+ ${startDateTime} | ${new Date('2019-07-17T00:00:00.000Z')} | ${0}
+ ${startDateTime} | ${new Date('2019-07-17T12:00:00.000Z')} | ${43200}
+ ${startDateTime} | ${new Date('2019-07-18T00:00:00.000Z')} | ${86400}
+ ${new Date('2019-07-18T00:00:00.000Z')} | ${startDateTime} | ${-86400}
+ `('returns $expected for $endDate - $startDate', ({ startDate, endDate, expected }) => {
+ expect(datetimeUtility.differenceInSeconds(startDate, endDate)).toBe(expected);
+ });
+});
diff --git a/spec/frontend/lib/utils/poll_spec.js b/spec/frontend/lib/utils/poll_spec.js
index 5ee9738ebf3..135c752b5cb 100644
--- a/spec/frontend/lib/utils/poll_spec.js
+++ b/spec/frontend/lib/utils/poll_spec.js
@@ -1,6 +1,6 @@
+import waitForPromises from 'helpers/wait_for_promises';
import Poll from '~/lib/utils/poll';
import { successCodes } from '~/lib/utils/http_status';
-import waitForPromises from 'helpers/wait_for_promises';
describe('Poll', () => {
let callbacks;
@@ -128,6 +128,35 @@ describe('Poll', () => {
});
});
+ describe('with delayed initial request', () => {
+ it('delays the first request', async done => {
+ mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
+
+ const Polling = new Poll({
+ resource: service,
+ method: 'fetch',
+ data: { page: 1 },
+ successCallback: callbacks.success,
+ errorCallback: callbacks.error,
+ });
+
+ Polling.makeDelayedRequest(1);
+
+ expect(Polling.timeoutID).toBeTruthy();
+
+ waitForAllCallsToFinish(2, () => {
+ Polling.stop();
+
+ expect(service.fetch.mock.calls).toHaveLength(2);
+ expect(service.fetch).toHaveBeenCalledWith({ page: 1 });
+ expect(callbacks.success).toHaveBeenCalled();
+ expect(callbacks.error).not.toHaveBeenCalled();
+
+ done();
+ });
+ });
+ });
+
describe('stop', () => {
it('stops polling when method is called', done => {
mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
diff --git a/spec/frontend/lib/utils/poll_until_complete_spec.js b/spec/frontend/lib/utils/poll_until_complete_spec.js
index 15602b87b9c..c1df30756fd 100644
--- a/spec/frontend/lib/utils/poll_until_complete_spec.js
+++ b/spec/frontend/lib/utils/poll_until_complete_spec.js
@@ -1,8 +1,8 @@
import AxiosMockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import pollUntilComplete from '~/lib/utils/poll_until_complete';
import httpStatusCodes from '~/lib/utils/http_status';
-import { TEST_HOST } from 'helpers/test_constants';
const endpoint = `${TEST_HOST}/foo`;
const mockData = 'mockData';
diff --git a/spec/frontend/lib/utils/sticky_spec.js b/spec/frontend/lib/utils/sticky_spec.js
index 4ad68cc9ff6..01e8fe777af 100644
--- a/spec/frontend/lib/utils/sticky_spec.js
+++ b/spec/frontend/lib/utils/sticky_spec.js
@@ -1,5 +1,5 @@
-import { isSticky } from '~/lib/utils/sticky';
import { setHTMLFixture } from 'helpers/fixtures';
+import { isSticky } from '~/lib/utils/sticky';
const TEST_OFFSET_TOP = 500;
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index e769580b587..a13ac3778cf 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -160,6 +160,118 @@ describe('URL utility', () => {
'https://host/path?op=%2B&foo=bar',
);
});
+
+ describe('with spread array option', () => {
+ const spreadArrayOptions = { spreadArrays: true };
+
+ it('maintains multiple values', () => {
+ expect(mergeUrlParams({}, '?array[]=foo&array[]=bar', spreadArrayOptions)).toBe(
+ '?array[]=foo&array[]=bar',
+ );
+ });
+
+ it('overrides multiple values with one', () => {
+ expect(
+ mergeUrlParams({ array: ['baz'] }, '?array[]=foo&array[]=bar', spreadArrayOptions),
+ ).toBe('?array[]=baz');
+ });
+ it('removes existing params', () => {
+ expect(
+ mergeUrlParams({ array: null }, '?array[]=foo&array[]=bar', spreadArrayOptions),
+ ).toBe('');
+ });
+ it('removes existing params and keeps others', () => {
+ expect(
+ mergeUrlParams(
+ { array: null },
+ '?array[]=foo&array[]=bar&other=quis',
+ spreadArrayOptions,
+ ),
+ ).toBe('?other=quis');
+ });
+ it('removes existing params along others', () => {
+ expect(
+ mergeUrlParams(
+ { array: null, other: 'quis' },
+ '?array[]=foo&array[]=bar',
+ spreadArrayOptions,
+ ),
+ ).toBe('?other=quis');
+ });
+ it('handles empty arrays along other parameters', () => {
+ expect(mergeUrlParams({ array: [], other: 'quis' }, '?array=baz', spreadArrayOptions)).toBe(
+ '?array[]=&other=quis',
+ );
+ });
+ it('handles multiple values along other parameters', () => {
+ expect(
+ mergeUrlParams(
+ { array: ['foo', 'bar'], other: 'quis' },
+ '?array=baz',
+ spreadArrayOptions,
+ ),
+ ).toBe('?array[]=foo&array[]=bar&other=quis');
+ });
+ it('handles array values with encoding', () => {
+ expect(
+ mergeUrlParams({ array: ['foo+', 'bar,baz'] }, '?array[]=%2Fbaz', spreadArrayOptions),
+ ).toBe('?array[]=foo%2B&array[]=bar%2Cbaz');
+ });
+ it('handles multiple arrays', () => {
+ expect(
+ mergeUrlParams(
+ { array1: ['foo+', 'bar,baz'], array2: ['quis', 'quux'] },
+ '?array1[]=%2Fbaz',
+ spreadArrayOptions,
+ ),
+ ).toBe('?array1[]=foo%2B&array1[]=bar%2Cbaz&array2[]=quis&array2[]=quux');
+ });
+ });
+
+ describe('without spread array option', () => {
+ it('maintains multiple values', () => {
+ expect(mergeUrlParams({}, '?array=foo%2Cbar')).toBe('?array=foo%2Cbar');
+ });
+ it('overrides multiple values with one', () => {
+ expect(mergeUrlParams({ array: ['baz'] }, '?array=foo%2Cbar')).toBe('?array=baz');
+ });
+ it('removes existing params', () => {
+ expect(mergeUrlParams({ array: null }, '?array=foo%2Cbar')).toBe('');
+ });
+ it('removes existing params and keeps others', () => {
+ expect(mergeUrlParams({ array: null }, '?array=foo&array=bar&other=quis')).toBe(
+ '?other=quis',
+ );
+ });
+ it('removes existing params along others', () => {
+ expect(mergeUrlParams({ array: null, other: 'quis' }, '?array=foo&array=bar')).toBe(
+ '?other=quis',
+ );
+ });
+ it('handles empty arrays along other parameters', () => {
+ expect(mergeUrlParams({ array: [], other: 'quis' }, '?array=baz')).toBe(
+ '?array=&other=quis',
+ );
+ });
+ it('handles multiple values along other parameters', () => {
+ expect(mergeUrlParams({ array: ['foo', 'bar'], other: 'quis' }, '?array=baz')).toBe(
+ '?array=foo%2Cbar&other=quis',
+ );
+ });
+ it('handles array values with encoding', () => {
+ expect(mergeUrlParams({ array: ['foo+', 'bar,baz'] }, '?array=%2Fbaz')).toBe(
+ '?array=foo%2B%2Cbar%2Cbaz',
+ );
+ });
+ it('handles multiple arrays', () => {
+ expect(
+ mergeUrlParams(
+ { array1: ['foo+', 'bar,baz'], array2: ['quis', 'quux'] },
+ '?array1=%2Fbaz',
+ ),
+ ).toBe('?array1=foo%2B%2Cbar%2Cbaz&array2=quis%2Cquux');
+ });
+ });
});
describe('removeParams', () => {
diff --git a/spec/frontend/locale/index_spec.js b/spec/frontend/locale/index_spec.js
index 346ed5182f4..d65d7c195b2 100644
--- a/spec/frontend/locale/index_spec.js
+++ b/spec/frontend/locale/index_spec.js
@@ -1,6 +1,5 @@
-import { createDateTimeFormat, languageCode } from '~/locale';
-
import { setLanguage } from 'helpers/locale_helper';
+import { createDateTimeFormat, languageCode } from '~/locale';
describe('locale', () => {
afterEach(() => setLanguage(null));
diff --git a/spec/frontend/logs/components/environment_logs_spec.js b/spec/frontend/logs/components/environment_logs_spec.js
index dee62709d81..6421aca684f 100644
--- a/spec/frontend/logs/components/environment_logs_spec.js
+++ b/spec/frontend/logs/components/environment_logs_spec.js
@@ -1,4 +1,4 @@
-import { GlSprintf, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlSprintf, GlIcon, GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import EnvironmentLogs from '~/logs/components/environment_logs.vue';
@@ -124,7 +124,7 @@ describe('EnvironmentLogs', () => {
expect(wrapper.isVueInstance()).toBe(true);
expect(wrapper.isEmpty()).toBe(false);
- expect(findEnvironmentsDropdown().is(GlDropdown)).toBe(true);
+ expect(findEnvironmentsDropdown().is(GlDeprecatedDropdown)).toBe(true);
expect(findSimpleFilters().exists()).toBe(true);
expect(findLogControlButtons().exists()).toBe(true);
@@ -167,7 +167,7 @@ describe('EnvironmentLogs', () => {
it('displays a disabled environments dropdown', () => {
expect(findEnvironmentsDropdown().attributes('disabled')).toBe('true');
- expect(findEnvironmentsDropdown().findAll(GlDropdownItem).length).toBe(0);
+ expect(findEnvironmentsDropdown().findAll(GlDeprecatedDropdownItem).length).toBe(0);
});
it('does not update buttons state', () => {
@@ -244,7 +244,7 @@ describe('EnvironmentLogs', () => {
});
it('populates environments dropdown', () => {
- const items = findEnvironmentsDropdown().findAll(GlDropdownItem);
+ const items = findEnvironmentsDropdown().findAll(GlDeprecatedDropdownItem);
expect(findEnvironmentsDropdown().props('text')).toBe(mockEnvName);
expect(items.length).toBe(mockEnvironments.length);
mockEnvironments.forEach((env, i) => {
@@ -254,7 +254,7 @@ describe('EnvironmentLogs', () => {
});
it('dropdown has one environment selected', () => {
- const items = findEnvironmentsDropdown().findAll(GlDropdownItem);
+ const items = findEnvironmentsDropdown().findAll(GlDeprecatedDropdownItem);
mockEnvironments.forEach((env, i) => {
const item = items.at(i);
@@ -289,7 +289,7 @@ describe('EnvironmentLogs', () => {
describe('when user clicks', () => {
it('environment name, trace is refreshed', () => {
- const items = findEnvironmentsDropdown().findAll(GlDropdownItem);
+ const items = findEnvironmentsDropdown().findAll(GlDeprecatedDropdownItem);
const index = 1; // any env
expect(dispatch).not.toHaveBeenCalledWith(`${module}/showEnvironment`, expect.anything());
diff --git a/spec/frontend/logs/components/log_advanced_filters_spec.js b/spec/frontend/logs/components/log_advanced_filters_spec.js
index adcd6b4fb07..007c5000e16 100644
--- a/spec/frontend/logs/components/log_advanced_filters_spec.js
+++ b/spec/frontend/logs/components/log_advanced_filters_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import { defaultTimeRange } from '~/vue_shared/constants';
import { GlFilteredSearch } from '@gitlab/ui';
+import { defaultTimeRange } from '~/vue_shared/constants';
import { convertToFixedRange } from '~/lib/utils/datetime_range';
import { createStore } from '~/logs/stores';
import { TOKEN_TYPE_POD_NAME } from '~/logs/constants';
diff --git a/spec/frontend/logs/components/log_simple_filters_spec.js b/spec/frontend/logs/components/log_simple_filters_spec.js
index 13504a2b1fc..e739621431e 100644
--- a/spec/frontend/logs/components/log_simple_filters_spec.js
+++ b/spec/frontend/logs/components/log_simple_filters_spec.js
@@ -1,4 +1,4 @@
-import { GlIcon, GlDropdownItem } from '@gitlab/ui';
+import { GlIcon, GlDeprecatedDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createStore } from '~/logs/stores';
import { mockPods, mockPodName } from '../mock_data';
@@ -17,7 +17,7 @@ describe('LogSimpleFilters', () => {
const findPodsNoPodsText = () => wrapper.find({ ref: 'noPodsMsg' });
const findPodsDropdownItems = () =>
findPodsDropdown()
- .findAll(GlDropdownItem)
+ .findAll(GlDeprecatedDropdownItem)
.filter(item => !item.is('[disabled]'));
const mockPodsLoading = () => {
diff --git a/spec/frontend/logs/mock_data.js b/spec/frontend/logs/mock_data.js
index f9b3508e01c..f4c567a2ea3 100644
--- a/spec/frontend/logs/mock_data.js
+++ b/spec/frontend/logs/mock_data.js
@@ -36,6 +36,16 @@ export const mockManagedApps = [
path: '/root/autodevops-deploy/-/clusters/15',
gitlab_managed_apps_logs_path: '/root/autodevops-deploy/-/logs?cluster_id=15',
},
+ {
+ cluster_type: 'project_type',
+ enabled: true,
+ environment_scope: '*',
+ name: 'kubernetes-cluster-2',
+ provider_type: 'user',
+ status: 'connected',
+ path: '/root/autodevops-deploy/-/clusters/16',
+ gitlab_managed_apps_logs_path: null,
+ },
];
export const mockPodName = 'production-764c58d697-aaaaa';
diff --git a/spec/frontend/logs/stores/actions_spec.js b/spec/frontend/logs/stores/actions_spec.js
index acd9536a682..e4501abdc76 100644
--- a/spec/frontend/logs/stores/actions_spec.js
+++ b/spec/frontend/logs/stores/actions_spec.js
@@ -17,7 +17,7 @@ import {
import { defaultTimeRange } from '~/vue_shared/constants';
import axios from '~/lib/utils/axios_utils';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
import {
mockPodName,
diff --git a/spec/frontend/logs/stores/mutations_spec.js b/spec/frontend/logs/stores/mutations_spec.js
index 137533f02d7..4a095e0f26e 100644
--- a/spec/frontend/logs/stores/mutations_spec.js
+++ b/spec/frontend/logs/stores/mutations_spec.js
@@ -272,7 +272,8 @@ describe('Logs Store Mutations', () => {
mutations[types.RECEIVE_MANAGED_APPS_DATA_SUCCESS](state, mockManagedApps);
- expect(state.managedApps.options).toEqual(mockManagedApps);
+ expect(state.managedApps.options.length).toEqual(1);
+ expect(state.managedApps.options).toEqual([mockManagedApps[0]]);
expect(state.managedApps.isLoading).toBe(false);
});
});
diff --git a/spec/frontend/maintenance_mode_settings/components/app_spec.js b/spec/frontend/maintenance_mode_settings/components/app_spec.js
index 0453354b008..ad753642e85 100644
--- a/spec/frontend/maintenance_mode_settings/components/app_spec.js
+++ b/spec/frontend/maintenance_mode_settings/components/app_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
+import { GlToggle, GlFormTextarea, GlButton } from '@gitlab/ui';
import MaintenanceModeSettingsApp from '~/maintenance_mode_settings/components/app.vue';
-import { GlToggle, GlFormTextarea, GlDeprecatedButton } from '@gitlab/ui';
describe('MaintenanceModeSettingsApp', () => {
let wrapper;
@@ -16,7 +16,7 @@ describe('MaintenanceModeSettingsApp', () => {
const findMaintenanceModeSettingsContainer = () => wrapper.find('article');
const findGlToggle = () => wrapper.find(GlToggle);
const findGlFormTextarea = () => wrapper.find(GlFormTextarea);
- const findGlButton = () => wrapper.find(GlDeprecatedButton);
+ const findGlButton = () => wrapper.find(GlButton);
describe('template', () => {
beforeEach(() => {
@@ -35,7 +35,7 @@ describe('MaintenanceModeSettingsApp', () => {
expect(findGlFormTextarea().exists()).toBe(true);
});
- it('renders the GlDeprecatedButton', () => {
+ it('renders the GlButton', () => {
expect(findGlButton().exists()).toBe(true);
});
});
diff --git a/spec/frontend/merge_request_spec.js b/spec/frontend/merge_request_spec.js
index f4f2a78f5f7..16f04d032fd 100644
--- a/spec/frontend/merge_request_spec.js
+++ b/spec/frontend/merge_request_spec.js
@@ -1,10 +1,10 @@
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
import MergeRequest from '~/merge_request';
import CloseReopenReportToggle from '~/close_reopen_report_toggle';
import IssuablesHelper from '~/helpers/issuables_helper';
-import { TEST_HOST } from 'spec/test_constants';
describe('MergeRequest', () => {
const test = {};
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index ad373d04ec0..85a4ee8974e 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -1,11 +1,11 @@
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
+import initMrPage from 'helpers/init_vue_mr_page_helper';
import axios from '~/lib/utils/axios_utils';
import MergeRequestTabs from '~/merge_request_tabs';
import '~/commit/pipelines/pipelines_bundle';
import '~/lib/utils/common_utils';
import 'vendor/jquery.scrollTo';
-import initMrPage from 'helpers/init_vue_mr_page_helper';
jest.mock('~/lib/utils/webpack', () => ({
resetServiceWorkersPublicPath: jest.fn(),
diff --git a/spec/frontend/milestones/project_milestone_combobox_spec.js b/spec/frontend/milestones/project_milestone_combobox_spec.js
index a7321d21559..2265c9bdc2e 100644
--- a/spec/frontend/milestones/project_milestone_combobox_spec.js
+++ b/spec/frontend/milestones/project_milestone_combobox_spec.js
@@ -1,9 +1,9 @@
-import { milestones as projectMilestones } from './mock_data';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
-import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue';
import { GlNewDropdown, GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
+import MilestoneCombobox from '~/milestones/project_milestone_combobox.vue';
+import { milestones as projectMilestones } from './mock_data';
const TEST_SEARCH_ENDPOINT = '/api/v4/projects/8/search';
diff --git a/spec/frontend/monitoring/alert_widget_spec.js b/spec/frontend/monitoring/alert_widget_spec.js
index f0355dfa01b..193dbb3e63f 100644
--- a/spec/frontend/monitoring/alert_widget_spec.js
+++ b/spec/frontend/monitoring/alert_widget_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlTooltip, GlSprintf, GlBadge } from '@gitlab/ui';
-import AlertWidget from '~/monitoring/components/alert_widget.vue';
import waitForPromises from 'helpers/wait_for_promises';
-import createFlash from '~/flash';
+import AlertWidget from '~/monitoring/components/alert_widget.vue';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
const mockReadAlert = jest.fn();
const mockCreateAlert = jest.fn();
diff --git a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
index e7c51d82cd2..7ef956f8e05 100644
--- a/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/dashboard_template_spec.js.snap
@@ -20,7 +20,6 @@ exports[`Dashboard template matches the default snapshot 1`] = `
data-qa-selector="dashboards_filter_dropdown"
defaultbranch="master"
id="monitor-dashboards-dropdown"
- modalid="duplicateDashboard"
toggle-class="dropdown-menu-toggle"
/>
</div>
@@ -33,26 +32,24 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="mb-2 pr-2 d-flex d-sm-block"
>
- <gl-dropdown-stub
+ <gl-new-dropdown-stub
+ category="tertiary"
class="flex-grow-1"
data-qa-selector="environments_dropdown"
+ headertext=""
id="monitor-environments-dropdown"
menu-class="monitor-environment-dropdown-menu"
+ size="medium"
text="production"
- toggle-class="dropdown-menu-toggle"
+ toggleclass="dropdown-menu-toggle"
+ variant="default"
>
<div
class="d-flex flex-column overflow-hidden"
>
- <gl-dropdown-header-stub
- class="monitor-environment-dropdown-header text-center"
- >
-
- Environment
-
- </gl-dropdown-header-stub>
-
- <gl-dropdown-divider-stub />
+ <gl-new-dropdown-header-stub>
+ Environment
+ </gl-new-dropdown-header-stub>
<gl-search-box-by-type-stub
class="m-2"
@@ -72,7 +69,7 @@ exports[`Dashboard template matches the default snapshot 1`] = `
</div>
</div>
- </gl-dropdown-stub>
+ </gl-new-dropdown-stub>
</div>
<div
@@ -100,45 +97,23 @@ exports[`Dashboard template matches the default snapshot 1`] = `
<div
class="d-sm-flex"
>
- <div
- class="mb-2 mr-2 d-flex"
- >
- <div
- class="flex-grow-1"
- title="Star dashboard"
- >
- <gl-deprecated-button-stub
- class="w-100"
- size="md"
- variant="default"
- >
- <gl-icon-stub
- name="star-o"
- size="16"
- />
- </gl-deprecated-button-stub>
- </div>
- </div>
-
<!---->
<!---->
- <!---->
-
- <!---->
-
- <!---->
-
- <!---->
+ <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>
-
- <duplicate-dashboard-modal-stub
- defaultbranch="master"
- modalid="duplicateDashboard"
- />
</div>
<empty-state-stub
diff --git a/spec/frontend/monitoring/components/alert_widget_form_spec.js b/spec/frontend/monitoring/components/alert_widget_form_spec.js
index a8416216a94..6d71a9b09e5 100644
--- a/spec/frontend/monitoring/components/alert_widget_form_spec.js
+++ b/spec/frontend/monitoring/components/alert_widget_form_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
+import INVALID_URL from '~/lib/utils/invalid_url';
import AlertWidgetForm from '~/monitoring/components/alert_widget_form.vue';
import ModalStub from '../stubs/modal_stub';
@@ -24,7 +25,13 @@ describe('AlertWidgetForm', () => {
const propsWithAlertData = {
...defaultProps,
alertsToManage: {
- alert: { alert_path: alertPath, operator: '<', threshold: 5, metricId },
+ alert: {
+ alert_path: alertPath,
+ operator: '<',
+ threshold: 5,
+ metricId,
+ runbookUrl: INVALID_URL,
+ },
},
configuredAlert: metricId,
};
@@ -46,15 +53,11 @@ describe('AlertWidgetForm', () => {
const modal = () => wrapper.find(ModalStub);
const modalTitle = () => modal().attributes('title');
const submitButton = () => modal().find(GlLink);
+ const findRunbookField = () => modal().find('[data-testid="alertRunbookField"]');
+ const findThresholdField = () => modal().find('[data-qa-selector="alert_threshold_field"]');
const submitButtonTrackingOpts = () =>
JSON.parse(submitButton().attributes('data-tracking-options'));
- const e = {
- preventDefault: jest.fn(),
- };
-
- beforeEach(() => {
- e.preventDefault.mockReset();
- });
+ const stubEvent = { preventDefault: jest.fn() };
afterEach(() => {
if (wrapper) wrapper.destroy();
@@ -81,35 +84,34 @@ describe('AlertWidgetForm', () => {
expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.create);
});
- it('emits a "create" event when form submitted without existing alert', () => {
- createComponent();
+ it('emits a "create" event when form submitted without existing alert', async () => {
+ createComponent(defaultProps);
- wrapper.vm.selectQuery('9');
- wrapper.setData({
- threshold: 900,
- });
+ modal().vm.$emit('shown');
+
+ findThresholdField().vm.$emit('input', 900);
+ findRunbookField().vm.$emit('input', INVALID_URL);
- wrapper.vm.handleSubmit(e);
+ modal().vm.$emit('ok', stubEvent);
expect(wrapper.emitted().create[0]).toEqual([
{
alert: undefined,
operator: '>',
threshold: 900,
- prometheus_metric_id: '9',
+ prometheus_metric_id: '8',
+ runbookUrl: INVALID_URL,
},
]);
- expect(e.preventDefault).toHaveBeenCalledTimes(1);
});
it('resets form when modal is dismissed (hidden)', () => {
- createComponent();
+ createComponent(defaultProps);
- wrapper.vm.selectQuery('9');
- wrapper.vm.selectQuery('>');
- wrapper.setData({
- threshold: 800,
- });
+ modal().vm.$emit('shown');
+
+ findThresholdField().vm.$emit('input', 800);
+ findRunbookField().vm.$emit('input', INVALID_URL);
modal().vm.$emit('hidden');
@@ -117,6 +119,7 @@ describe('AlertWidgetForm', () => {
expect(wrapper.vm.operator).toBe(null);
expect(wrapper.vm.threshold).toBe(null);
expect(wrapper.vm.prometheusMetricId).toBe(null);
+ expect(wrapper.vm.runbookUrl).toBe(null);
});
it('sets selectedAlert to the provided configuredAlert on modal show', () => {
@@ -163,7 +166,7 @@ describe('AlertWidgetForm', () => {
beforeEach(() => {
createComponent(propsWithAlertData);
- wrapper.vm.selectQuery(metricId);
+ modal().vm.$emit('shown');
});
it('sets tracking options for delete alert', () => {
@@ -176,7 +179,7 @@ describe('AlertWidgetForm', () => {
});
it('emits "delete" event when form values unchanged', () => {
- wrapper.vm.handleSubmit(e);
+ modal().vm.$emit('ok', stubEvent);
expect(wrapper.emitted().delete[0]).toEqual([
{
@@ -184,37 +187,52 @@ describe('AlertWidgetForm', () => {
operator: '<',
threshold: 5,
prometheus_metric_id: '8',
+ runbookUrl: INVALID_URL,
},
]);
- expect(e.preventDefault).toHaveBeenCalledTimes(1);
});
+ });
- it('emits "update" event when form changed', () => {
- wrapper.setData({
- threshold: 11,
- });
+ it('emits "update" event when form changed', () => {
+ const updatedRunbookUrl = `${INVALID_URL}/test`;
- wrapper.vm.handleSubmit(e);
+ createComponent(propsWithAlertData);
- expect(wrapper.emitted().update[0]).toEqual([
- {
- alert: 'alert',
- operator: '<',
- threshold: 11,
- prometheus_metric_id: '8',
- },
- ]);
- expect(e.preventDefault).toHaveBeenCalledTimes(1);
- });
+ modal().vm.$emit('shown');
+
+ findRunbookField().vm.$emit('input', updatedRunbookUrl);
+ findThresholdField().vm.$emit('input', 11);
- it('sets tracking options for update alert', () => {
- wrapper.setData({
+ modal().vm.$emit('ok', stubEvent);
+
+ expect(wrapper.emitted().update[0]).toEqual([
+ {
+ alert: 'alert',
+ operator: '<',
threshold: 11,
- });
+ prometheus_metric_id: '8',
+ runbookUrl: updatedRunbookUrl,
+ },
+ ]);
+ });
+
+ it('sets tracking options for update alert', async () => {
+ createComponent(propsWithAlertData);
+
+ modal().vm.$emit('shown');
+
+ findThresholdField().vm.$emit('input', 11);
+
+ await wrapper.vm.$nextTick();
+
+ expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.update);
+ });
+
+ describe('alert runbooks', () => {
+ it('shows the runbook field', () => {
+ createComponent();
- return wrapper.vm.$nextTick(() => {
- expect(submitButtonTrackingOpts()).toEqual(dataTrackingOptions.update);
- });
+ expect(findRunbookField().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/monitoring/components/charts/gauge_spec.js b/spec/frontend/monitoring/components/charts/gauge_spec.js
new file mode 100644
index 00000000000..850e2ca87db
--- /dev/null
+++ b/spec/frontend/monitoring/components/charts/gauge_spec.js
@@ -0,0 +1,215 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlGaugeChart } from '@gitlab/ui/dist/charts';
+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.find(GlGaugeChart);
+
+ const createWrapper = ({ ...graphProps } = {}) => {
+ wrapper = shallowMount(GaugeChart, {
+ propsData: {
+ graphData: {
+ ...defaultGraphData,
+ ...graphProps,
+ },
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ 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
index 2a1c78025ae..27a2021e9be 100644
--- a/spec/frontend/monitoring/components/charts/heatmap_spec.js
+++ b/spec/frontend/monitoring/components/charts/heatmap_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { GlHeatmap } from '@gitlab/ui/dist/charts';
import timezoneMock from 'timezone-mock';
import Heatmap from '~/monitoring/components/charts/heatmap.vue';
-import { graphDataPrometheusQueryRangeMultiTrack } from '../../mock_data';
+import { heatmapGraphData } from '../../graph_data';
describe('Heatmap component', () => {
let wrapper;
@@ -10,10 +10,12 @@ describe('Heatmap component', () => {
const findChart = () => wrapper.find(GlHeatmap);
+ const graphData = heatmapGraphData();
+
const createWrapper = (props = {}) => {
wrapper = shallowMount(Heatmap, {
propsData: {
- graphData: graphDataPrometheusQueryRangeMultiTrack,
+ graphData: heatmapGraphData(),
containerWidth: 100,
...props,
},
@@ -38,11 +40,11 @@ describe('Heatmap component', () => {
});
it('should display a label on the x axis', () => {
- expect(wrapper.vm.xAxisName).toBe(graphDataPrometheusQueryRangeMultiTrack.x_label);
+ expect(wrapper.vm.xAxisName).toBe(graphData.xLabel);
});
it('should display a label on the y axis', () => {
- expect(wrapper.vm.yAxisName).toBe(graphDataPrometheusQueryRangeMultiTrack.y_label);
+ expect(wrapper.vm.yAxisName).toBe(graphData.y_label);
});
// According to the echarts docs https://echarts.apache.org/en/option.html#series-heatmap.data
@@ -54,24 +56,24 @@ describe('Heatmap component', () => {
const row = wrapper.vm.chartData[0];
expect(row.length).toBe(3);
- expect(wrapper.vm.chartData.length).toBe(30);
+ 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(5);
+ expect(xAxisLabels.length).toBe(2);
});
describe('y axis labels', () => {
- const gmtLabels = ['3:00 PM', '4:00 PM', '5:00 PM', '6:00 PM', '7:00 PM', '8:00 PM'];
+ 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 = ['8:00 AM', '9:00 AM', '10:00 AM', '11:00 AM', '12:00 PM', '1:00 PM'];
+ const ptLabels = ['1:10 PM', '1:12 PM', '1:14 PM'];
const utcLabels = gmtLabels; // Identical in this case
beforeAll(() => {
diff --git a/spec/frontend/monitoring/components/charts/options_spec.js b/spec/frontend/monitoring/components/charts/options_spec.js
index 1c8fdc01e3e..3372d27e4f9 100644
--- a/spec/frontend/monitoring/components/charts/options_spec.js
+++ b/spec/frontend/monitoring/components/charts/options_spec.js
@@ -1,5 +1,9 @@
import { SUPPORTED_FORMATS } from '~/lib/utils/unit_format';
-import { getYAxisOptions, getTooltipFormatter } from '~/monitoring/components/charts/options';
+import {
+ getYAxisOptions,
+ getTooltipFormatter,
+ getValidThresholds,
+} from '~/monitoring/components/charts/options';
describe('options spec', () => {
describe('getYAxisOptions', () => {
@@ -82,4 +86,242 @@ describe('options spec', () => {
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
index 3783b1eebd2..37712eb3012 100644
--- a/spec/frontend/monitoring/components/charts/single_stat_spec.js
+++ b/spec/frontend/monitoring/components/charts/single_stat_spec.js
@@ -1,71 +1,91 @@
import { shallowMount } from '@vue/test-utils';
+import { GlSingleStat } from '@gitlab/ui/dist/charts';
import SingleStatChart from '~/monitoring/components/charts/single_stat.vue';
import { singleStatGraphData } from '../../graph_data';
describe('Single Stat Chart component', () => {
- let singleStatChart;
+ let wrapper;
- beforeEach(() => {
- singleStatChart = shallowMount(SingleStatChart, {
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(SingleStatChart, {
propsData: {
graphData: singleStatGraphData({}, { unit: 'MB' }),
+ ...props,
},
});
+ };
+
+ const findChart = () => wrapper.find(GlSingleStat);
+
+ beforeEach(() => {
+ createComponent();
});
afterEach(() => {
- singleStatChart.destroy();
+ wrapper.destroy();
});
describe('computed', () => {
describe('statValue', () => {
it('should interpolate the value and unit props', () => {
- expect(singleStatChart.vm.statValue).toBe('1.00MB');
+ expect(findChart().props('value')).toBe('1.00MB');
});
it('should change the value representation to a percentile one', () => {
- singleStatChart.setProps({
+ createComponent({
graphData: singleStatGraphData({ max_value: 120 }, { value: 91 }),
});
- expect(singleStatChart.vm.statValue).toContain('75.83%');
+ expect(findChart().props('value')).toContain('75.83%');
});
it('should display NaN for non numeric maxValue values', () => {
- singleStatChart.setProps({
+ createComponent({
graphData: singleStatGraphData({ max_value: 'not a number' }),
});
- expect(singleStatChart.vm.statValue).toContain('NaN');
+ expect(findChart().props('value')).toContain('NaN');
});
it('should display NaN for missing query values', () => {
- singleStatChart.setProps({
+ createComponent({
graphData: singleStatGraphData({ max_value: 120 }, { value: 'NaN' }),
});
- expect(singleStatChart.vm.statValue).toContain('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');
});
- describe('field attribute', () => {
+ 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', () => {
- singleStatChart.setProps({
+ createComponent({
graphData: singleStatGraphData({ field: 'job' }, { isVector: true }),
});
- return singleStatChart.vm.$nextTick(() => {
- expect(singleStatChart.vm.statValue).toContain('prometheus');
- });
+ expect(findChart().props('value')).toContain('prometheus');
});
it('displays No data to display if field attribute is not present', () => {
- singleStatChart.setProps({
+ createComponent({
graphData: singleStatGraphData({ field: 'this-does-not-exist' }),
});
- return singleStatChart.vm.$nextTick(() => {
- expect(singleStatChart.vm.statValue).toContain('No data to display');
- });
+ expect(findChart().props('value')).toContain('No data to display');
});
});
});
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index 97386be9e32..6f9a89feb3e 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -12,7 +12,12 @@ import {
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
import { panelTypes, chartHeight } from '~/monitoring/constants';
import TimeSeries from '~/monitoring/components/charts/time_series.vue';
-import { deploymentData, mockProjectDir, annotationsData } from '../../mock_data';
+import {
+ deploymentData,
+ mockProjectDir,
+ annotationsData,
+ mockFixedTimeRange,
+} from '../../mock_data';
import { timeSeriesGraphData } from '../../graph_data';
@@ -42,6 +47,7 @@ describe('Time series component', () => {
deploymentData,
annotations: annotationsData,
projectPath: `${TEST_HOST}${mockProjectDir}`,
+ timeRange: mockFixedTimeRange,
...props,
},
stubs: {
@@ -382,6 +388,25 @@ describe('Time series component', () => {
});
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', () => {
+ wrapper.setProps({
+ timeRange: {},
+ });
+ return wrapper.vm.$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);
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
new file mode 100644
index 00000000000..024b2cbd7f1
--- /dev/null
+++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
@@ -0,0 +1,440 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlNewDropdownItem } from '@gitlab/ui';
+import { createStore } from '~/monitoring/stores';
+import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
+import { setupAllDashboards, setupStoreWithData } from '../store_utils';
+import { redirectTo } from '~/lib/utils/url_utility';
+import Tracking from '~/tracking';
+import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
+import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
+import { dashboardActionsMenuProps, dashboardGitResponse } from '../mock_data';
+import * as types from '~/monitoring/stores/mutation_types';
+
+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,
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ store = createStore();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('add metric item', () => {
+ it('is rendered when custom metrics are available', () => {
+ createShallowWrapper();
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findAddMetricItem().exists()).toBe(true);
+ });
+ });
+
+ it('is not rendered when custom metrics are not available', () => {
+ createShallowWrapper({
+ addingMetricsAvailable: false,
+ });
+
+ return wrapper.vm.$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().attributes().modalid).toBe('addMetric');
+ });
+
+ it('add metric modal submit button exists', () => {
+ expect(findAddMetricModalSubmitButton().exists()).toBe(true);
+ });
+
+ it('renders custom metrics form fields', () => {
+ expect(wrapper.find(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(done => {
+ jest.spyOn(Tracking, 'event').mockReturnValue();
+ createShallowWrapper();
+
+ setupStoreWithData(store);
+
+ origPage = document.body.dataset.page;
+ document.body.dataset.page = 'projects:environments:metrics';
+
+ wrapper.vm.$nextTick(done);
+ });
+
+ afterEach(() => {
+ document.body.dataset.page = origPage;
+ });
+
+ it('is tracked', done => {
+ const submitButton = findAddMetricModalSubmitButton().vm;
+
+ wrapper.vm.$nextTick(() => {
+ submitButton.$el.click();
+ wrapper.vm.$nextTick(() => {
+ expect(Tracking.event).toHaveBeenCalledWith(
+ document.body.dataset.page,
+ 'click_button',
+ {
+ label: 'add_new_metric',
+ property: 'modal',
+ value: undefined,
+ },
+ );
+ done();
+ });
+ });
+ });
+ });
+ });
+
+ describe('add panel item', () => {
+ const GlNewDropdownItemStub = {
+ extends: GlNewDropdownItem,
+ props: {
+ to: [String, Object],
+ },
+ };
+
+ let $route;
+
+ beforeEach(() => {
+ $route = { name: DASHBOARD_PAGE, params: { dashboard: 'my_dashboard.yml' } };
+
+ createShallowWrapper(
+ {
+ isOotbDashboard: false,
+ },
+ {
+ mocks: { $route },
+ stubs: { GlNewDropdownItem: GlNewDropdownItemStub },
+ },
+ );
+ });
+
+ it('is disabled for ootb dashboards', () => {
+ createShallowWrapper({
+ isOotbDashboard: true,
+ });
+
+ return wrapper.vm.$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', () => {
+ const modalId = 'duplicateDashboard';
+ const modalTrigger = findDuplicateDashboardItem();
+ const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
+
+ modalTrigger.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ 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', () => {
+ delete window.location;
+ window.location = new URL('https://localhost');
+
+ const newDashboard = dashboardGitResponse[1];
+
+ const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml';
+ findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(redirectTo).toHaveBeenCalled();
+ expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl);
+ });
+ });
+ });
+ });
+
+ 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')).toBeFalsy();
+ });
+
+ it('is disabled when starring is taking place', () => {
+ store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findStarDashboardItem().exists()).toBe(true);
+ expect(findStarDashboardItem().attributes('disabled')).toBe('true');
+ });
+ });
+
+ it('on click it dispatches a toggle star action', () => {
+ findStarDashboardItem().vm.$emit('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/toggleStarredValue',
+ undefined,
+ );
+ });
+ });
+
+ describe('when dashboard is not starred', () => {
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard: dashboardGitResponse[0].path,
+ });
+ return wrapper.vm.$nextTick();
+ });
+
+ it('item text shows "Star dashboard"', () => {
+ expect(findStarDashboardItem().html()).toMatch(/Star dashboard/);
+ });
+ });
+
+ describe('when dashboard is starred', () => {
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard: dashboardGitResponse[1].path,
+ });
+ return wrapper.vm.$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')).toBe('true');
+ });
+
+ 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', () => {
+ const modalId = 'createDashboard';
+ const modalTrigger = findCreateDashboardItem();
+ const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
+
+ modalTrigger.trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ 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')).toBe('true');
+ });
+
+ 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
index 5a1a615c703..5cf24706ebd 100644
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -1,16 +1,23 @@
import { shallowMount } from '@vue/test-utils';
+import { GlNewDropdownItem, GlSearchBoxByType, GlLoadingIcon, GlButton } from '@gitlab/ui';
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 RefreshButton from '~/monitoring/components/refresh_button.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import DuplicateDashboardModal from '~/monitoring/components/duplicate_dashboard_modal.vue';
-import CreateDashboardModal from '~/monitoring/components/create_dashboard_modal.vue';
-import { setupAllDashboards } from '../store_utils';
+import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
+import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
+import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils';
import {
+ environmentData,
dashboardGitResponse,
selfMonitoringDashboardGitResponse,
dashboardHeaderProps,
} from '../mock_data';
import { redirectTo } from '~/lib/utils/url_utility';
+const mockProjectPath = 'https://path/to/project';
+
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
queryToObject: jest.fn(),
@@ -21,13 +28,22 @@ describe('Dashboard header', () => {
let store;
let wrapper;
- const findActionsMenu = () => wrapper.find('[data-testid="actions-menu"]');
- const findCreateDashboardMenuItem = () =>
- findActionsMenu().find('[data-testid="action-create-dashboard"]');
- const findCreateDashboardDuplicateItem = () =>
- findActionsMenu().find('[data-testid="action-duplicate-dashboard"]');
- const findDuplicateDashboardModal = () => wrapper.find(DuplicateDashboardModal);
- const findCreateDashboardModal = () => wrapper.find('[data-testid="create-dashboard-modal"]');
+ const findDashboardDropdown = () => wrapper.find(DashboardsDropdown);
+
+ const findEnvsDropdown = () => wrapper.find({ ref: 'monitorEnvironmentsDropdown' });
+ const findEnvsDropdownItems = () => findEnvsDropdown().findAll(GlNewDropdownItem);
+ const findEnvsDropdownSearch = () => findEnvsDropdown().find(GlSearchBoxByType);
+ const findEnvsDropdownSearchMsg = () => wrapper.find({ ref: 'monitorEnvironmentsDropdownMsg' });
+ const findEnvsDropdownLoadingIcon = () => findEnvsDropdown().find(GlLoadingIcon);
+
+ const findDateTimePicker = () => wrapper.find(DateTimePicker);
+ const findRefreshButton = () => wrapper.find(RefreshButton);
+
+ const findActionsMenu = () => wrapper.find(ActionsMenu);
+
+ const setSearchTerm = searchTerm => {
+ store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
+ };
const createShallowWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(DashboardHeader, {
@@ -45,139 +61,315 @@ describe('Dashboard header', () => {
wrapper.destroy();
});
- describe('when a dashboard has been duplicated in the duplicate dashboard modal', () => {
+ describe('dashboards dropdown', () => {
beforeEach(() => {
- store.state.monitoringDashboard.projectPath = 'root/sandbox';
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ projectPath: mockProjectPath,
+ });
+
+ createShallowWrapper();
});
- /**
- * The duplicate dashboard modal gets called both by a menu item from the
- * dashboards dropdown and by an item from the actions menu.
- *
- * This spec is context agnostic, so it addresses all cases where the
- * duplicate dashboard modal gets called.
- */
- it('redirects to the newly created dashboard', () => {
- delete window.location;
- window.location = new URL('https://localhost');
- const newDashboard = dashboardGitResponse[1];
+ it('shows the dashboard dropdown', () => {
+ expect(findDashboardDropdown().exists()).toBe(true);
+ });
- createShallowWrapper();
+ 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',
+ });
- const newDashboardUrl = 'root/sandbox/-/metrics/dashboard.yml';
- findDuplicateDashboardModal().vm.$emit('dashboardDuplicated', newDashboard);
+ expect(redirectTo).toHaveBeenCalledWith(
+ `${mockProjectPath}/-/metrics/.gitlab%2Fdashboards%2Fdashboard%26copy.yml`,
+ );
+ });
- return wrapper.vm.$nextTick().then(() => {
- expect(redirectTo).toHaveBeenCalled();
- expect(redirectTo).toHaveBeenCalledWith(newDashboardUrl);
+ 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',
});
+
+ expect(redirectTo).toHaveBeenCalledWith(`${mockProjectPath}/-/metrics/dashboard%26copy.yml`);
});
});
- describe('actions menu', () => {
+ describe('environments dropdown', () => {
beforeEach(() => {
- store.state.monitoringDashboard.projectPath = '';
createShallowWrapper();
});
- it('is rendered if projectPath is set in store', () => {
- store.state.monitoringDashboard.projectPath = 'https://path/to/project';
+ it('shows the environments dropdown', () => {
+ expect(findEnvsDropdown().exists()).toBe(true);
+ });
- return wrapper.vm.$nextTick().then(() => {
- expect(findActionsMenu().exists()).toBe(true);
+ it('renders a search input', () => {
+ expect(findEnvsDropdownSearch().exists()).toBe(true);
+ });
+
+ describe('when environments data is not loaded', () => {
+ beforeEach(() => {
+ setupStoreWithDashboard(store);
+ return wrapper.vm.$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(() => {
+ setupStoreWithData(store);
+ store.state.monitoringDashboard.projectPath = mockProjectPath;
+ store.state.monitoringDashboard.currentDashboard = currentDashboard;
+ store.state.monitoringDashboard.currentEnvironmentName = currentEnvironmentName;
+
+ return wrapper.vm.$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', () => {
+ const searchTerm = 'production';
+ const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1);
+ setSearchTerm(searchTerm);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findEnvsDropdownItems()).toHaveLength(resultEnvs.length);
+ });
+ });
+
+ it('does not filter dropdown items if search term is empty string', () => {
+ const searchTerm = '';
+ setSearchTerm(searchTerm);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findEnvsDropdownItems()).toHaveLength(environmentData.length);
+ });
+ });
+
+ it("shows error message if search term doesn't match", () => {
+ const searchTerm = 'does-not-exist';
+ setSearchTerm(searchTerm);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(findEnvsDropdownSearchMsg().isVisible()).toBe(true);
+ });
+ });
+
+ it('shows loading element when environments fetch is still loading', () => {
+ store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
+
+ return wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(findEnvsDropdownLoadingIcon().exists()).toBe(true);
+ })
+ .then(() => {
+ store.commit(
+ `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
+ environmentData,
+ );
+ })
+ .then(() => {
+ expect(findEnvsDropdownLoadingIcon().exists()).toBe(false);
+ });
});
});
+ });
- it('is not rendered if projectPath is not set in store', () => {
- expect(findActionsMenu().exists()).toBe(false);
+ describe('date time picker', () => {
+ beforeEach(() => {
+ createShallowWrapper();
});
- it('contains a modal', () => {
- store.state.monitoringDashboard.projectPath = 'https://path/to/project';
+ it('is rendered', () => {
+ expect(findDateTimePicker().exists()).toBe(true);
+ });
- return wrapper.vm.$nextTick().then(() => {
- expect(findActionsMenu().contains(CreateDashboardModal)).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(() => {
+ store.state.monitoringDashboard.externalDashboardUrl = '/mockUrl';
+ createShallowWrapper();
+
+ return wrapper.vm.$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');
+ });
+ });
- const duplicableCases = [
- null, // When no path is specified, it uses the default dashboard path.
+ describe('actions menu', () => {
+ const ootbDashboards = [
dashboardGitResponse[0].path,
- dashboardGitResponse[2].path,
selfMonitoringDashboardGitResponse[0].path,
];
+ const customDashboards = [
+ dashboardGitResponse[1].path,
+ selfMonitoringDashboardGitResponse[1].path,
+ ];
- describe.each(duplicableCases)(
- 'when the selected dashboard can be duplicated',
- dashboardPath => {
- it('contains a "Create New" menu item and a "Duplicate Dashboard" menu item', () => {
- store.state.monitoringDashboard.projectPath = 'https://path/to/project';
- setupAllDashboards(store, dashboardPath);
+ it('is rendered', () => {
+ createShallowWrapper();
- return wrapper.vm.$nextTick().then(() => {
- expect(findCreateDashboardMenuItem().exists()).toBe(true);
- expect(findCreateDashboardDuplicateItem().exists()).toBe(true);
- });
+ expect(findActionsMenu().exists()).toBe(true);
+ });
+
+ describe('adding metrics prop', () => {
+ it.each(ootbDashboards)('gets passed true if current dashboard is OOTB', dashboardPath => {
+ createShallowWrapper({ customMetricsAvailable: true });
+
+ store.state.monitoringDashboard.emptyState = false;
+ setupAllDashboards(store, dashboardPath);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findActionsMenu().props('addingMetricsAvailable')).toBe(true);
});
- },
- );
+ });
- const nonDuplicableCases = [
- dashboardGitResponse[1].path,
- selfMonitoringDashboardGitResponse[1].path,
- ];
+ it.each(customDashboards)(
+ 'gets passed false if current dashboard is custom',
+ dashboardPath => {
+ createShallowWrapper({ customMetricsAvailable: true });
- describe.each(nonDuplicableCases)(
- 'when the selected dashboard cannot be duplicated',
- dashboardPath => {
- it('contains a "Create New" menu item and no "Duplicate Dashboard" menu item', () => {
- store.state.monitoringDashboard.projectPath = 'https://path/to/project';
+ store.state.monitoringDashboard.emptyState = false;
setupAllDashboards(store, dashboardPath);
return wrapper.vm.$nextTick().then(() => {
- expect(findCreateDashboardMenuItem().exists()).toBe(true);
- expect(findCreateDashboardDuplicateItem().exists()).toBe(false);
+ expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
});
+ },
+ );
+
+ it('gets passed false if empty state is shown', () => {
+ createShallowWrapper({ customMetricsAvailable: true });
+
+ store.state.monitoringDashboard.emptyState = true;
+ setupAllDashboards(store, ootbDashboards[0]);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
});
- },
- );
- });
+ });
- describe('actions menu modals', () => {
- const url = 'https://path/to/project';
+ it('gets passed false if custom metrics are not available', () => {
+ createShallowWrapper({ customMetricsAvailable: false });
- beforeEach(() => {
- store.state.monitoringDashboard.projectPath = url;
- setupAllDashboards(store);
+ store.state.monitoringDashboard.emptyState = false;
+ setupAllDashboards(store, ootbDashboards[0]);
- createShallowWrapper();
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findActionsMenu().props('addingMetricsAvailable')).toBe(false);
+ });
+ });
});
- it('Clicking on "Create New" opens up a modal', () => {
- const modalId = 'createDashboard';
- const modalTrigger = findCreateDashboardMenuItem();
- const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
+ it('custom metrics path gets passed', () => {
+ const path = 'https://path/to/customMetrics';
- modalTrigger.trigger('click');
+ createShallowWrapper({ customMetricsPath: path });
return wrapper.vm.$nextTick().then(() => {
- expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
+ expect(findActionsMenu().props('customMetricsPath')).toBe(path);
});
});
- it('"Create new dashboard" modal contains correct buttons', () => {
- expect(findCreateDashboardModal().props('projectPath')).toBe(url);
+ it('validate query path gets passed', () => {
+ const path = 'https://path/to/validateQuery';
+
+ createShallowWrapper({ validateQueryPath: path });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findActionsMenu().props('validateQueryPath')).toBe(path);
+ });
});
- it('"Duplicate Dashboard" opens up a modal', () => {
- const modalId = 'duplicateDashboard';
- const modalTrigger = findCreateDashboardDuplicateItem();
- const rootEmit = jest.spyOn(wrapper.vm.$root, '$emit');
+ it('default branch gets passed', () => {
+ const branch = 'branchName';
- modalTrigger.trigger('click');
+ createShallowWrapper({ defaultBranch: branch });
return wrapper.vm.$nextTick().then(() => {
- expect(rootEmit.mock.calls[0]).toContainEqual(modalId);
+ expect(findActionsMenu().props('defaultBranch')).toBe(branch);
});
});
});
diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
new file mode 100644
index 00000000000..587ddd23d3f
--- /dev/null
+++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
@@ -0,0 +1,234 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlCard, GlForm, GlFormTextarea, GlAlert } from '@gitlab/ui';
+import { createStore } from '~/monitoring/stores';
+import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
+import * as types from '~/monitoring/stores/mutation_types';
+import { metricsDashboardResponse } from '../fixture_data';
+import { mockTimeRange } from '../mock_data';
+
+import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue';
+import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
+
+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.find(GlForm);
+ const findTxtArea = () => findForm().find(GlFormTextarea);
+ const findSubmitBtn = () => findForm().find('[type="submit"]');
+ const findClipboardCopyBtn = () => wrapper.find({ ref: 'clipboardCopyBtn' });
+ const findViewDocumentationBtn = () => wrapper.find({ ref: 'viewDocumentationBtn' });
+ const findOpenRepositoryBtn = () => wrapper.find({ ref: 'openRepositoryBtn' });
+ const findPanel = () => wrapper.find(DashboardPanel);
+ const findTimeRangePicker = () => wrapper.find(DateTimePicker);
+ const findRefreshButton = () => wrapper.find('[data-testid="previewRefreshButton"]');
+
+ beforeEach(() => {
+ mockShowToast = jest.fn();
+ store = createStore();
+ createComponent();
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+ });
+
+ afterEach(() => {});
+
+ 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().is('[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', () => {
+ findForm().vm.$emit('submit', new Event('submit'));
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/fetchPanelPreview',
+ expect.stringContaining('title:'),
+ );
+ });
+ });
+
+ describe('when form is submitted', () => {
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.REQUEST_PANEL_PREVIEW}`, 'mock yml content');
+ return wrapper.vm.$nextTick();
+ });
+
+ it('submit button is disabled', () => {
+ expect(findSubmitBtn().is('[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', () => {
+ // mimic initial state where SET_PANEL_PREVIEW_IS_SHOWN is set to false
+ store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, false);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+ });
+
+ it('when changed triggers data fetch if preview panel button is clicked', () => {
+ findForm().vm.$emit('submit', new Event('submit'));
+
+ store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange);
+
+ return wrapper.vm.$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', () => {
+ // mimic initial state where SET_PANEL_PREVIEW_IS_SHOWN is set to false
+ store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_IS_SHOWN}`, false);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+ });
+
+ it('when clicked triggers data fetch if preview panel button is clicked', () => {
+ // 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');
+
+ return wrapper.vm.$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 ocurred!';
+
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_FAILURE}`, mockError);
+ return wrapper.vm.$nextTick();
+ });
+
+ it('displays an alert', () => {
+ expect(wrapper.find(GlAlert).exists()).toBe(true);
+ expect(wrapper.find(GlAlert).text()).toBe(mockError);
+ });
+
+ it('displays an empty dashboard panel', () => {
+ expect(findPanel().props('graphData')).toBe(null);
+ });
+
+ it('changing time range should not refetch data', () => {
+ store.commit(`monitoringDashboard/${types.SET_PANEL_PREVIEW_TIME_RANGE}`, mockTimeRange);
+
+ return wrapper.vm.$nextTick(() => {
+ expect(store.dispatch).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when panel data is available', () => {
+ beforeEach(() => {
+ store.commit(`monitoringDashboard/${types.RECEIVE_PANEL_PREVIEW_SUCCESS}`, mockPanel);
+ return wrapper.vm.$nextTick();
+ });
+
+ it('displays no alert', () => {
+ expect(wrapper.find(GlAlert).exists()).toBe(false);
+ });
+
+ it('displays panel with data', () => {
+ const { title, type } = wrapper.find(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
index 693818aa55a..fb96bcc042f 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -2,23 +2,23 @@ import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import { setTestTimeout } from 'helpers/timeout';
+import { GlNewDropdownItem as GlDropdownItem } from '@gitlab/ui';
import invalidUrl from '~/lib/utils/invalid_url';
import axios from '~/lib/utils/axios_utils';
-import { GlNewDropdownItem as GlDropdownItem } from '@gitlab/ui';
import AlertWidget from '~/monitoring/components/alert_widget.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import {
+ mockAlert,
mockLogsHref,
mockLogsPath,
mockNamespace,
mockNamespacedData,
mockTimeRange,
- graphDataPrometheusQueryRangeMultiTrack,
barMockData,
} from '../mock_data';
import { dashboardProps, graphData, graphDataEmpty } from '../fixture_data';
-import { anomalyGraphData, singleStatGraphData } from '../graph_data';
+import { anomalyGraphData, singleStatGraphData, heatmapGraphData } from '../graph_data';
import { panelTypes } from '~/monitoring/constants';
@@ -56,9 +56,10 @@ describe('Dashboard Panel', () => {
const findCtxMenu = () => wrapper.find({ ref: 'contextualMenu' });
const findMenuItems = () => wrapper.findAll(GlDropdownItem);
const findMenuItemByText = text => findMenuItems().filter(i => i.text() === text);
+ const findAlertsWidget = () => wrapper.find(AlertWidget);
- const createWrapper = (props, options) => {
- wrapper = shallowMount(DashboardPanel, {
+ const createWrapper = (props, { mountFn = shallowMount, ...options } = {}) => {
+ wrapper = mountFn(DashboardPanel, {
propsData: {
graphData,
settingsPath: dashboardProps.settingsPath,
@@ -79,6 +80,9 @@ describe('Dashboard Panel', () => {
});
};
+ const setMetricsSavedToDb = val =>
+ monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
+
beforeEach(() => {
setTestTimeout(1000);
@@ -235,7 +239,7 @@ describe('Dashboard Panel', () => {
${anomalyGraphData()} | ${MonitorAnomalyChart} | ${false}
${dataWithType(panelTypes.COLUMN)} | ${MonitorColumnChart} | ${false}
${dataWithType(panelTypes.STACKED_COLUMN)} | ${MonitorStackedColumnChart} | ${false}
- ${graphDataPrometheusQueryRangeMultiTrack} | ${MonitorHeatmapChart} | ${false}
+ ${heatmapGraphData()} | ${MonitorHeatmapChart} | ${false}
${barMockData} | ${MonitorBarChart} | ${false}
`('when $data.type data is provided', ({ data, component, hasCtxMenu }) => {
const attrs = { attr1: 'attr1Value', attr2: 'attr2Value' };
@@ -255,6 +259,35 @@ describe('Dashboard Panel', () => {
});
});
});
+
+ describe('computed', () => {
+ describe('fixedCurrentTimeRange', () => {
+ it('returns fixed time for valid time range', () => {
+ state.timeRange = mockTimeRange;
+ return wrapper.vm.$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', ({ input, output }) => {
+ state.timeRange = input;
+ return wrapper.vm.$nextTick(() => {
+ expect(findTimeChart().props('timeRange')).toEqual(output);
+ });
+ });
+ });
+ });
});
describe('Edit custom metric dropdown item', () => {
@@ -444,7 +477,7 @@ describe('Dashboard Panel', () => {
describe('csvText', () => {
it('converts metrics data from json to csv', () => {
- const header = `timestamp,${graphData.y_label}`;
+ 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]}`;
@@ -523,7 +556,7 @@ describe('Dashboard Panel', () => {
});
it('displays a heatmap in local timezone', () => {
- createWrapper({ graphData: graphDataPrometheusQueryRangeMultiTrack });
+ createWrapper({ graphData: heatmapGraphData() });
expect(wrapper.find(MonitorHeatmapChart).props('timezone')).toBe('LOCAL');
});
@@ -538,7 +571,7 @@ describe('Dashboard Panel', () => {
});
it('displays a heatmap with UTC', () => {
- createWrapper({ graphData: graphDataPrometheusQueryRangeMultiTrack });
+ createWrapper({ graphData: heatmapGraphData() });
expect(wrapper.find(MonitorHeatmapChart).props('timezone')).toBe('UTC');
});
});
@@ -573,10 +606,6 @@ describe('Dashboard Panel', () => {
});
describe('panel alerts', () => {
- const setMetricsSavedToDb = val =>
- monitoringDashboard.getters.metricsSavedToDb.mockReturnValue(val);
- const findAlertsWidget = () => wrapper.find(AlertWidget);
-
beforeEach(() => {
mockGetterReturnValue('metricsSavedToDb', []);
@@ -702,4 +731,60 @@ describe('Dashboard Panel', () => {
expect(findManageLinksItem().exists()).toBe(false);
});
});
+
+ describe('Runbook url', () => {
+ const findRunbookLinks = () => wrapper.findAll('[data-testid="runbookLink"]');
+ const { metricId } = graphData.metrics[0];
+ const { alert_path: alertPath } = mockAlert;
+
+ const mockRunbookAlert = {
+ ...mockAlert,
+ metricId,
+ };
+
+ beforeEach(() => {
+ mockGetterReturnValue('metricsSavedToDb', []);
+ });
+
+ it('does not show a runbook link when alerts are not present', () => {
+ createWrapper();
+
+ expect(findRunbookLinks().length).toBe(0);
+ });
+
+ describe('when alerts are present', () => {
+ beforeEach(() => {
+ setMetricsSavedToDb([metricId]);
+
+ createWrapper({
+ alertsEndpoint: '/endpoint',
+ prometheusAlertsAvailable: true,
+ });
+ });
+
+ it('does not show a runbook link when a runbook is not set', async () => {
+ findAlertsWidget().vm.$emit('setAlerts', alertPath, {
+ ...mockRunbookAlert,
+ runbookUrl: '',
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(findRunbookLinks().length).toBe(0);
+ });
+
+ it('shows a runbook link when a runbook is set', async () => {
+ findAlertsWidget().vm.$emit('setAlerts', alertPath, mockRunbookAlert);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findRunbookLinks().length).toBe(1);
+ expect(
+ findRunbookLinks()
+ .at(0)
+ .attributes('href'),
+ ).toBe(invalidUrl);
+ });
+ });
+ });
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 4b7f7a9ddb3..f37d95317ab 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -1,19 +1,14 @@
import { shallowMount, mount } from '@vue/test-utils';
-import Tracking from '~/tracking';
-import { ESC_KEY, ESC_KEY_IE11 } from '~/lib/utils/keys';
-import { GlModal, GlDropdownItem, GlDeprecatedButton, GlIcon } from '@gitlab/ui';
-import { objectToQuery } from '~/lib/utils/url_utility';
import VueDraggable from 'vuedraggable';
import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
+import { ESC_KEY } from '~/lib/utils/keys';
+import { objectToQuery } from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
-import RefreshButton from '~/monitoring/components/refresh_button.vue';
-import DateTimePicker from '~/vue_shared/components/date_time_picker/date_time_picker.vue';
-import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
-import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
import EmptyState from '~/monitoring/components/empty_state.vue';
import GroupEmptyState from '~/monitoring/components/group_empty_state.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
@@ -29,14 +24,13 @@ import {
setupStoreWithDataForPanelCount,
setupStoreWithLinks,
} from '../store_utils';
-import { environmentData, dashboardGitResponse, storeVariables } from '../mock_data';
+import { dashboardGitResponse, storeVariables } from '../mock_data';
import {
metricsDashboardViewModel,
metricsDashboardPanelCount,
dashboardProps,
} from '../fixture_data';
-import createFlash from '~/flash';
-import { TEST_HOST } from 'helpers/test_constants';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
@@ -45,14 +39,6 @@ describe('Dashboard', () => {
let wrapper;
let mock;
- const findDashboardHeader = () => wrapper.find(DashboardHeader);
- const findEnvironmentsDropdown = () =>
- findDashboardHeader().find({ ref: 'monitorEnvironmentsDropdown' });
- const findAllEnvironmentsDropdownItems = () => findEnvironmentsDropdown().findAll(GlDropdownItem);
- const setSearchTerm = searchTerm => {
- store.commit(`monitoringDashboard/${types.SET_ENVIRONMENTS_FILTER}`, searchTerm);
- };
-
const createShallowWrapper = (props = {}, options = {}) => {
wrapper = shallowMount(Dashboard, {
propsData: { ...dashboardProps, ...props },
@@ -90,28 +76,6 @@ describe('Dashboard', () => {
}
});
- describe('no metrics are available yet', () => {
- beforeEach(() => {
- createShallowWrapper();
- });
-
- it('shows the environment selector', () => {
- expect(findEnvironmentsDropdown().exists()).toBe(true);
- });
- });
-
- describe('no data found', () => {
- beforeEach(() => {
- createShallowWrapper();
-
- return wrapper.vm.$nextTick();
- });
-
- it('shows the environment selector dropdown', () => {
- expect(findEnvironmentsDropdown().exists()).toBe(true);
- });
- });
-
describe('request information to the server', () => {
it('calls to set time range and fetch data', () => {
createShallowWrapper({ hasMetrics: true });
@@ -149,17 +113,14 @@ describe('Dashboard', () => {
});
it('fetches the metrics data with proper time window', () => {
- jest.spyOn(store, 'dispatch');
-
createMountedWrapper({ hasMetrics: true });
- store.commit(
- `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
- environmentData,
- );
-
return wrapper.vm.$nextTick().then(() => {
- expect(store.dispatch).toHaveBeenCalled();
+ expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/fetchData', undefined);
+ expect(store.dispatch).toHaveBeenCalledWith(
+ 'monitoringDashboard/setTimeRange',
+ expect.objectContaining({ duration: { seconds: 28800 } }),
+ );
});
});
});
@@ -427,37 +388,6 @@ describe('Dashboard', () => {
);
});
});
-
- describe('when custom dashboard is selected', () => {
- const windowLocation = window.location;
- const findDashboardDropdown = () => wrapper.find(DashboardHeader).find(DashboardsDropdown);
-
- beforeEach(() => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- projectPath: TEST_HOST,
- });
-
- delete window.location;
- window.location = { ...windowLocation, assign: jest.fn() };
- createMountedWrapper();
-
- return wrapper.vm.$nextTick();
- });
-
- afterEach(() => {
- window.location = windowLocation;
- });
-
- it('encodes dashboard param', () => {
- findDashboardDropdown().vm.$emit('selectDashboard', {
- path: '.gitlab/dashboards/dashboard&copy.yml',
- display_name: 'dashboard&copy.yml',
- });
- expect(window.location.assign).toHaveBeenCalledWith(
- `${TEST_HOST}/-/metrics/dashboard%26copy.yml`,
- );
- });
- });
});
describe('when all panels in the first group are loading', () => {
@@ -500,21 +430,6 @@ describe('Dashboard', () => {
return wrapper.vm.$nextTick();
});
- it('renders the environments dropdown with a number of environments', () => {
- expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length);
-
- findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => {
- const anchorEl = itemWrapper.find('a');
- if (anchorEl.exists()) {
- const href = anchorEl.attributes('href');
- const currentDashboard = encodeURIComponent(dashboardGitResponse[0].path);
- const environmentId = encodeURIComponent(environmentData[index].id);
- const url = `${TEST_HOST}/-/metrics/${currentDashboard}?environment=${environmentId}`;
- expect(href).toBe(url);
- }
- });
- });
-
it('it does not show loading icons in any group', () => {
setupStoreWithData(store);
@@ -524,127 +439,6 @@ describe('Dashboard', () => {
});
});
});
-
- // Note: This test is not working, .active does not show the active environment
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip('renders the environments dropdown with a single active element', () => {
- const activeItem = findAllEnvironmentsDropdownItems().wrappers.filter(itemWrapper =>
- itemWrapper.find('.active').exists(),
- );
-
- expect(activeItem.length).toBe(1);
- });
- });
-
- describe('star dashboards', () => {
- const findToggleStar = () => wrapper.find(DashboardHeader).find({ ref: 'toggleStarBtn' });
- const findToggleStarIcon = () => findToggleStar().find(GlIcon);
-
- beforeEach(() => {
- createShallowWrapper();
- setupAllDashboards(store);
- });
-
- it('toggle star button is shown', () => {
- expect(findToggleStar().exists()).toBe(true);
- expect(findToggleStar().props('disabled')).toBe(false);
- });
-
- it('toggle star button is disabled when starring is taking place', () => {
- store.commit(`monitoringDashboard/${types.REQUEST_DASHBOARD_STARRING}`);
-
- return wrapper.vm.$nextTick(() => {
- expect(findToggleStar().exists()).toBe(true);
- expect(findToggleStar().props('disabled')).toBe(true);
- });
- });
-
- describe('when the dashboard list is loaded', () => {
- // Tooltip element should wrap directly
- const getToggleTooltip = () => findToggleStar().element.parentElement.getAttribute('title');
-
- beforeEach(() => {
- setupAllDashboards(store);
- jest.spyOn(store, 'dispatch');
- });
-
- it('dispatches a toggle star action', () => {
- findToggleStar().vm.$emit('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(store.dispatch).toHaveBeenCalledWith(
- 'monitoringDashboard/toggleStarredValue',
- undefined,
- );
- });
- });
-
- describe('when dashboard is not starred', () => {
- beforeEach(() => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: dashboardGitResponse[0].path,
- });
- return wrapper.vm.$nextTick();
- });
-
- it('toggle star button shows "Star dashboard"', () => {
- expect(getToggleTooltip()).toBe('Star dashboard');
- });
-
- it('toggle star button shows an unstarred state', () => {
- expect(findToggleStarIcon().attributes('name')).toBe('star-o');
- });
- });
-
- describe('when dashboard is starred', () => {
- beforeEach(() => {
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: dashboardGitResponse[1].path,
- });
- return wrapper.vm.$nextTick();
- });
-
- it('toggle star button shows "Star dashboard"', () => {
- expect(getToggleTooltip()).toBe('Unstar dashboard');
- });
-
- it('toggle star button shows a starred state', () => {
- expect(findToggleStarIcon().attributes('name')).toBe('star');
- });
- });
- });
- });
-
- it('hides the environments dropdown list when there is no environments', () => {
- createMountedWrapper({ hasMetrics: true });
-
- setupStoreWithDashboard(store);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findAllEnvironmentsDropdownItems()).toHaveLength(0);
- });
- });
-
- it('renders the datetimepicker dropdown', () => {
- createMountedWrapper({ hasMetrics: true });
-
- setupStoreWithData(store);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.find(DateTimePicker).exists()).toBe(true);
- });
- });
-
- it('renders the refresh dashboard button', () => {
- createMountedWrapper({ hasMetrics: true });
-
- setupStoreWithData(store);
-
- return wrapper.vm.$nextTick().then(() => {
- const refreshBtn = wrapper.find(DashboardHeader).find(RefreshButton);
-
- expect(refreshBtn.exists()).toBe(true);
- });
});
describe('variables section', () => {
@@ -772,15 +566,6 @@ describe('Dashboard', () => {
undefined,
);
});
-
- it('restores dashboard from full screen by typing the Escape key on IE11', () => {
- mockKeyup(ESC_KEY_IE11);
-
- expect(store.dispatch).toHaveBeenCalledWith(
- `monitoringDashboard/clearExpandedPanel`,
- undefined,
- );
- });
});
});
@@ -811,100 +596,6 @@ describe('Dashboard', () => {
});
});
- describe('searchable environments dropdown', () => {
- beforeEach(() => {
- createMountedWrapper({ hasMetrics: true }, { attachToDocument: true });
-
- setupStoreWithData(store);
-
- return wrapper.vm.$nextTick();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders a search input', () => {
- expect(
- wrapper
- .find(DashboardHeader)
- .find({ ref: 'monitorEnvironmentsDropdownSearch' })
- .exists(),
- ).toBe(true);
- });
-
- it('renders dropdown items', () => {
- findAllEnvironmentsDropdownItems().wrappers.forEach((itemWrapper, index) => {
- const anchorEl = itemWrapper.find('a');
- if (anchorEl.exists()) {
- expect(anchorEl.text()).toBe(environmentData[index].name);
- }
- });
- });
-
- it('filters rendered dropdown items', () => {
- const searchTerm = 'production';
- const resultEnvs = environmentData.filter(({ name }) => name.indexOf(searchTerm) !== -1);
- setSearchTerm(searchTerm);
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findAllEnvironmentsDropdownItems().length).toEqual(resultEnvs.length);
- });
- });
-
- it('does not filter dropdown items if search term is empty string', () => {
- const searchTerm = '';
- setSearchTerm(searchTerm);
-
- return wrapper.vm.$nextTick(() => {
- expect(findAllEnvironmentsDropdownItems().length).toEqual(environmentData.length);
- });
- });
-
- it("shows error message if search term doesn't match", () => {
- const searchTerm = 'does-not-exist';
- setSearchTerm(searchTerm);
-
- return wrapper.vm.$nextTick(() => {
- expect(
- wrapper
- .find(DashboardHeader)
- .find({ ref: 'monitorEnvironmentsDropdownMsg' })
- .isVisible(),
- ).toBe(true);
- });
- });
-
- it('shows loading element when environments fetch is still loading', () => {
- store.commit(`monitoringDashboard/${types.REQUEST_ENVIRONMENTS_DATA}`);
-
- return wrapper.vm
- .$nextTick()
- .then(() => {
- expect(
- wrapper
- .find(DashboardHeader)
- .find({ ref: 'monitorEnvironmentsDropdownLoading' })
- .exists(),
- ).toBe(true);
- })
- .then(() => {
- store.commit(
- `monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`,
- environmentData,
- );
- })
- .then(() => {
- expect(
- wrapper
- .find(DashboardHeader)
- .find({ ref: 'monitorEnvironmentsDropdownLoading' })
- .exists(),
- ).toBe(false);
- });
- });
- });
-
describe('drag and drop function', () => {
const findDraggables = () => wrapper.findAll(VueDraggable);
const findEnabledDraggables = () => findDraggables().filter(f => !f.attributes('disabled'));
@@ -998,57 +689,6 @@ describe('Dashboard', () => {
});
});
- describe('dashboard timezone', () => {
- const setupWithTimezone = value => {
- store = createStore({ dashboardTimezone: value });
- setupStoreWithData(store);
- createShallowWrapper({ hasMetrics: true });
- return wrapper.vm.$nextTick;
- };
-
- describe('local timezone is enabled by default', () => {
- beforeEach(() => {
- return setupWithTimezone();
- });
-
- it('shows the data time picker in local timezone', () => {
- expect(
- findDashboardHeader()
- .find(DateTimePicker)
- .props('utc'),
- ).toBe(false);
- });
- });
-
- describe('when LOCAL timezone is enabled', () => {
- beforeEach(() => {
- return setupWithTimezone('LOCAL');
- });
-
- it('shows the data time picker in local timezone', () => {
- expect(
- findDashboardHeader()
- .find(DateTimePicker)
- .props('utc'),
- ).toBe(false);
- });
- });
-
- describe('when UTC timezone is enabled', () => {
- beforeEach(() => {
- return setupWithTimezone('UTC');
- });
-
- it('shows the data time picker in UTC format', () => {
- expect(
- findDashboardHeader()
- .find(DateTimePicker)
- .props('utc'),
- ).toBe(true);
- });
- });
- });
-
describe('cluster health', () => {
beforeEach(() => {
createShallowWrapper({ hasMetrics: true, showHeader: false });
@@ -1068,36 +708,9 @@ describe('Dashboard', () => {
});
});
- describe('dashboard edit link', () => {
- const findEditLink = () => wrapper.find('.js-edit-link');
-
- beforeEach(() => {
- createShallowWrapper({ hasMetrics: true });
-
- setupAllDashboards(store);
- return wrapper.vm.$nextTick();
- });
-
- it('is not present for the default dashboard', () => {
- expect(findEditLink().exists()).toBe(false);
- });
-
- it('is present for a custom dashboard, and links to its edit_path', () => {
- const dashboard = dashboardGitResponse[1];
- store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
- currentDashboard: dashboard.path,
- });
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findEditLink().exists()).toBe(true);
- expect(findEditLink().attributes('href')).toBe(dashboard.project_blob_path);
- });
- });
- });
-
describe('document title', () => {
const originalTitle = 'Original Title';
- const defaultDashboardName = dashboardGitResponse[0].display_name;
+ const overviewDashboardName = dashboardGitResponse[0].display_name;
beforeEach(() => {
document.title = originalTitle;
@@ -1108,11 +721,11 @@ describe('Dashboard', () => {
document.title = '';
});
- it('is prepended with default dashboard name by default', () => {
+ it('is prepended with the overview dashboard name by default', () => {
setupAllDashboards(store);
return wrapper.vm.$nextTick().then(() => {
- expect(document.title.startsWith(`${defaultDashboardName} · `)).toBe(true);
+ expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
});
});
@@ -1127,11 +740,11 @@ describe('Dashboard', () => {
});
});
- it('is prepended with default dashboard name is path is not known', () => {
+ it('is prepended with the overview dashboard name if path is not known', () => {
setupAllDashboards(store, 'unknown/path');
return wrapper.vm.$nextTick().then(() => {
- expect(document.title.startsWith(`${defaultDashboardName} · `)).toBe(true);
+ expect(document.title.startsWith(`${overviewDashboardName} · `)).toBe(true);
});
});
@@ -1151,41 +764,6 @@ describe('Dashboard', () => {
});
});
- describe('Dashboard dropdown', () => {
- beforeEach(() => {
- createMountedWrapper({ hasMetrics: true });
- setupAllDashboards(store);
- return wrapper.vm.$nextTick();
- });
-
- it('shows the dashboard dropdown', () => {
- const dashboardDropdown = wrapper.find(DashboardsDropdown);
-
- expect(dashboardDropdown.exists()).toBe(true);
- });
- });
-
- describe('external dashboard link', () => {
- beforeEach(() => {
- createMountedWrapper({
- hasMetrics: true,
- showPanels: false,
- showTimeWindowDropdown: false,
- externalDashboardUrl: '/mockUrl',
- });
-
- return wrapper.vm.$nextTick();
- });
-
- it('shows the link', () => {
- const externalDashboardButton = wrapper.find('.js-external-dashboard-link');
-
- expect(externalDashboardButton.exists()).toBe(true);
- expect(externalDashboardButton.is(GlDeprecatedButton)).toBe(true);
- expect(externalDashboardButton.text()).toContain('View full dashboard');
- });
- });
-
describe('Clipboard text in panels', () => {
const currentDashboard = dashboardGitResponse[1].path;
const panelIndex = 1; // skip expanded panel
@@ -1243,74 +821,4 @@ describe('Dashboard', () => {
expect(dashboardPanel.exists()).toBe(true);
});
});
-
- describe('add custom metrics', () => {
- const findAddMetricButton = () => wrapper.find(DashboardHeader).find({ ref: 'addMetricBtn' });
-
- describe('when not available', () => {
- beforeEach(() => {
- createShallowWrapper({
- hasMetrics: true,
- customMetricsPath: '/endpoint',
- });
- });
- it('does not render add button on the dashboard', () => {
- expect(findAddMetricButton().exists()).toBe(false);
- });
- });
-
- describe('when available', () => {
- let origPage;
- beforeEach(done => {
- jest.spyOn(Tracking, 'event').mockReturnValue();
- createShallowWrapper({
- hasMetrics: true,
- customMetricsPath: '/endpoint',
- customMetricsAvailable: true,
- });
- setupStoreWithData(store);
-
- origPage = document.body.dataset.page;
- document.body.dataset.page = 'projects:environments:metrics';
-
- wrapper.vm.$nextTick(done);
- });
- afterEach(() => {
- document.body.dataset.page = origPage;
- });
-
- it('renders add button on the dashboard', () => {
- expect(findAddMetricButton()).toBeDefined();
- });
-
- it('uses modal for custom metrics form', () => {
- expect(wrapper.find(GlModal).exists()).toBe(true);
- expect(wrapper.find(GlModal).attributes().modalid).toBe('addMetric');
- });
- it('adding new metric is tracked', done => {
- const submitButton = wrapper
- .find(DashboardHeader)
- .find({ ref: 'submitCustomMetricsFormBtn' }).vm;
- wrapper.vm.$nextTick(() => {
- submitButton.$el.click();
- wrapper.vm.$nextTick(() => {
- expect(Tracking.event).toHaveBeenCalledWith(
- document.body.dataset.page,
- 'click_button',
- {
- label: 'add_new_metric',
- property: 'modal',
- value: undefined,
- },
- );
- done();
- });
- });
- });
-
- it('renders custom metrics form fields', () => {
- expect(wrapper.find(CustomMetricsFormFields).exists()).toBe(true);
- });
- });
- });
});
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index 276e20bae6a..c4630bde32f 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
queryToObject,
redirectTo,
diff --git a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
index d09fcc92ee7..89adbad386f 100644
--- a/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
+++ b/spec/frontend/monitoring/components/dashboards_dropdown_spec.js
@@ -1,12 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDropdownItem, GlIcon } from '@gitlab/ui';
+import { GlNewDropdownItem, GlIcon } from '@gitlab/ui';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
-import { dashboardGitResponse, selfMonitoringDashboardGitResponse } from '../mock_data';
+import { dashboardGitResponse } from '../mock_data';
const defaultBranch = 'master';
-const modalId = 'duplicateDashboardModalId';
const starredDashboards = dashboardGitResponse.filter(({ starred }) => starred);
const notStarredDashboards = dashboardGitResponse.filter(({ starred }) => !starred);
@@ -17,20 +16,16 @@ describe('DashboardsDropdown', () => {
function createComponent(props, opts = {}) {
const storeOpts = {
- methods: {
- duplicateSystemDashboard: jest.fn(),
- },
computed: {
allDashboards: () => mockDashboards,
selectedDashboard: () => mockSelectedDashboard,
},
};
- return shallowMount(DashboardsDropdown, {
+ wrapper = shallowMount(DashboardsDropdown, {
propsData: {
...props,
defaultBranch,
- modalId,
},
sync: false,
...storeOpts,
@@ -38,8 +33,8 @@ describe('DashboardsDropdown', () => {
});
}
- const findItems = () => wrapper.findAll(GlDropdownItem);
- const findItemAt = i => wrapper.findAll(GlDropdownItem).at(i);
+ const findItems = () => wrapper.findAll(GlNewDropdownItem);
+ const findItemAt = i => wrapper.findAll(GlNewDropdownItem).at(i);
const findSearchInput = () => wrapper.find({ ref: 'monitorDashboardsDropdownSearch' });
const findNoItemsMsg = () => wrapper.find({ ref: 'monitorDashboardsDropdownMsg' });
const findStarredListDivider = () => wrapper.find({ ref: 'starredListDivider' });
@@ -52,7 +47,7 @@ describe('DashboardsDropdown', () => {
describe('when it receives dashboards data', () => {
beforeEach(() => {
- wrapper = createComponent();
+ createComponent();
});
it('displays an item for each dashboard', () => {
@@ -78,7 +73,7 @@ describe('DashboardsDropdown', () => {
});
it('filters dropdown items when searched for item exists in the list', () => {
- const searchTerm = 'Default';
+ const searchTerm = 'Overview';
setSearchTerm(searchTerm);
return wrapper.vm.$nextTick().then(() => {
@@ -96,10 +91,22 @@ describe('DashboardsDropdown', () => {
});
});
+ 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 }));
- wrapper = createComponent();
+ createComponent();
});
it('displays items with the dashboard path, with starred dashboards first', () => {
@@ -112,7 +119,7 @@ describe('DashboardsDropdown', () => {
describe('when it receives starred dashboards', () => {
beforeEach(() => {
mockDashboards = starredDashboards;
- wrapper = createComponent();
+ createComponent();
});
it('displays an item for each dashboard', () => {
@@ -133,7 +140,7 @@ describe('DashboardsDropdown', () => {
describe('when it receives only not-starred dashboards', () => {
beforeEach(() => {
mockDashboards = notStarredDashboards;
- wrapper = createComponent();
+ createComponent();
});
it('displays an item for each dashboard', () => {
@@ -150,90 +157,9 @@ describe('DashboardsDropdown', () => {
});
});
- const duplicableCases = [
- dashboardGitResponse[0],
- dashboardGitResponse[2],
- selfMonitoringDashboardGitResponse[0],
- ];
-
- describe.each(duplicableCases)('when the selected dashboard can be duplicated', dashboard => {
- let duplicateDashboardAction;
- let modalDirective;
-
- beforeEach(() => {
- mockSelectedDashboard = dashboard;
- modalDirective = jest.fn();
- duplicateDashboardAction = jest.fn().mockResolvedValue();
-
- wrapper = createComponent(
- {},
- {
- directives: {
- GlModal: modalDirective,
- },
- methods: {
- // Mock vuex actions
- duplicateSystemDashboard: duplicateDashboardAction,
- },
- },
- );
- });
-
- it('displays a dropdown item for each dashboard', () => {
- expect(findItems().length).toEqual(dashboardGitResponse.length + 1);
- });
-
- it('displays one "duplicate dashboard" dropdown item with a directive attached', () => {
- const item = wrapper.findAll('[data-testid="duplicateDashboardItem"]');
-
- expect(item.length).toBe(1);
- });
-
- it('"duplicate dashboard" dropdown item directive works', () => {
- const item = wrapper.find('[data-testid="duplicateDashboardItem"]');
-
- item.trigger('click');
-
- return wrapper.vm.$nextTick().then(() => {
- expect(modalDirective).toHaveBeenCalled();
- });
- });
-
- it('id is correct, as the value of modal directive binding matches modal id', () => {
- expect(modalDirective).toHaveBeenCalledTimes(1);
-
- // Binding's second argument contains the modal id
- expect(modalDirective.mock.calls[0][1]).toEqual(
- expect.objectContaining({
- value: modalId,
- }),
- );
- });
- });
-
- const nonDuplicableCases = [dashboardGitResponse[1], selfMonitoringDashboardGitResponse[1]];
-
- describe.each(nonDuplicableCases)(
- 'when the selected dashboard can not be duplicated',
- dashboard => {
- beforeEach(() => {
- mockSelectedDashboard = dashboard;
-
- wrapper = createComponent();
- });
-
- it('displays a dropdown list item for each dashboard, but no list item for "duplicate dashboard"', () => {
- const item = wrapper.findAll('[data-testid="duplicateDashboardItem"]');
-
- expect(findItems()).toHaveLength(dashboardGitResponse.length);
- expect(item.length).toBe(0);
- });
- },
- );
-
describe('when a dashboard gets selected by the user', () => {
beforeEach(() => {
- wrapper = createComponent();
+ createComponent();
findItemAt(1).vm.$emit('click');
});
diff --git a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
index 4e7fee81d66..74f265930b1 100644
--- a/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
+++ b/spec/frontend/monitoring/components/embeds/metric_embed_spec.js
@@ -1,10 +1,10 @@
import { createLocalVue, shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
-import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { TEST_HOST } from 'helpers/test_constants';
+import { setHTMLFixture } from 'helpers/fixtures';
+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';
-import { setHTMLFixture } from 'helpers/fixtures';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/monitoring/components/graph_group_spec.js b/spec/frontend/monitoring/components/graph_group_spec.js
index 81f5d90c310..86e2523f708 100644
--- a/spec/frontend/monitoring/components/graph_group_spec.js
+++ b/spec/frontend/monitoring/components/graph_group_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import GraphGroup from '~/monitoring/components/graph_group.vue';
import { GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import GraphGroup from '~/monitoring/components/graph_group.vue';
describe('Graph group component', () => {
let wrapper;
diff --git a/spec/frontend/monitoring/components/group_empty_state_spec.js b/spec/frontend/monitoring/components/group_empty_state_spec.js
index e8ef8192067..90bd6f67196 100644
--- a/spec/frontend/monitoring/components/group_empty_state_spec.js
+++ b/spec/frontend/monitoring/components/group_empty_state_spec.js
@@ -24,7 +24,7 @@ describe('GroupEmptyState', () => {
'FOO STATE', // does not fail with unknown states
];
- test.each(supportedStates)('Renders an empty state for %s', selectedState => {
+ it.each(supportedStates)('Renders an empty state for %s', selectedState => {
const wrapper = createComponent({ selectedState });
expect(wrapper.element).toMatchSnapshot();
diff --git a/spec/frontend/monitoring/components/refresh_button_spec.js b/spec/frontend/monitoring/components/refresh_button_spec.js
index 29615638453..a9b8295f38e 100644
--- a/spec/frontend/monitoring/components/refresh_button_spec.js
+++ b/spec/frontend/monitoring/components/refresh_button_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import { createStore } from '~/monitoring/stores';
+import Visibility from 'visibilityjs';
import { GlNewDropdown, GlNewDropdownItem, GlButton } from '@gitlab/ui';
-
+import { createStore } from '~/monitoring/stores';
import RefreshButton from '~/monitoring/components/refresh_button.vue';
describe('RefreshButton', () => {
@@ -10,8 +10,8 @@ describe('RefreshButton', () => {
let dispatch;
let documentHidden;
- const createWrapper = () => {
- wrapper = shallowMount(RefreshButton, { store });
+ const createWrapper = (options = {}) => {
+ wrapper = shallowMount(RefreshButton, { store, ...options });
};
const findRefreshBtn = () => wrapper.find(GlButton);
@@ -31,14 +31,8 @@ describe('RefreshButton', () => {
jest.spyOn(store, 'dispatch').mockResolvedValue();
dispatch = store.dispatch;
- // Document can be mock hidden by overriding the `hidden` property
documentHidden = false;
- Object.defineProperty(document, 'hidden', {
- configurable: true,
- get() {
- return documentHidden;
- },
- });
+ jest.spyOn(Visibility, 'hidden').mockImplementation(() => documentHidden);
createWrapper();
});
@@ -57,6 +51,20 @@ describe('RefreshButton', () => {
expect(findDropdown().props('text')).toBe('Off');
});
+ describe('when feature flag disable_metric_dashboard_refresh_rate is on', () => {
+ beforeEach(() => {
+ createWrapper({
+ provide: {
+ glFeatures: { disableMetricDashboardRefreshRate: true },
+ },
+ });
+ });
+
+ it('refresh rate is not available', () => {
+ expect(findDropdown().exists()).toBe(false);
+ });
+ });
+
describe('refresh rate options', () => {
it('presents multiple options', () => {
expect(findOptions().length).toBeGreaterThan(1);
diff --git a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
index cc384aef231..788f3abf617 100644
--- a/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
+++ b/spec/frontend/monitoring/components/variables/dropdown_field_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
import DropdownField from '~/monitoring/components/variables/dropdown_field.vue';
describe('Custom variable component', () => {
@@ -23,8 +23,8 @@ describe('Custom variable component', () => {
});
};
- const findDropdown = () => wrapper.find(GlDropdown);
- const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findDropdown = () => wrapper.find(GlDeprecatedDropdown);
+ const findDropdownItems = () => wrapper.findAll(GlDeprecatedDropdownItem);
it('renders dropdown element when all necessary props are passed', () => {
createShallowWrapper();
diff --git a/spec/frontend/monitoring/csv_export_spec.js b/spec/frontend/monitoring/csv_export_spec.js
new file mode 100644
index 00000000000..eb2a6e40243
--- /dev/null
+++ b/spec/frontend/monitoring/csv_export_spec.js
@@ -0,0 +1,126 @@
+import { timeSeriesGraphData } from './graph_data';
+import { graphDataToCsv } from '~/monitoring/csv_export';
+
+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
index 97edf7bda74..30040d3f89f 100644
--- a/spec/frontend/monitoring/fixture_data.js
+++ b/spec/frontend/monitoring/fixture_data.js
@@ -29,36 +29,12 @@ const datasetState = stateAndPropsFromDataset(
// https://gitlab.com/gitlab-org/gitlab/-/issues/229256
export const dashboardProps = {
...datasetState.dataProps,
- addDashboardDocumentationPath: 'https://path/to/docs',
alertsEndpoint: null,
};
export const metricsDashboardViewModel = mapToDashboardViewModel(metricsDashboardPayload);
export const metricsDashboardPanelCount = 22;
-export const metricResultStatus = {
- // First metric in fixture `metrics_dashboard/environment_metrics_dashboard.json`
- metricId: 'NO_DB_response_metrics_nginx_ingress_throughput_status_code',
- data: {
- resultType: 'matrix',
- result: metricsResult,
- },
-};
-export const metricResultPods = {
- // Second metric in fixture `metrics_dashboard/environment_metrics_dashboard.json`
- metricId: 'NO_DB_response_metrics_nginx_ingress_latency_pod_average',
- data: {
- resultType: 'matrix',
- result: metricsResult,
- },
-};
-export const metricResultEmpty = {
- metricId: 'NO_DB_response_metrics_nginx_ingress_16_throughput_status_code',
- data: {
- resultType: 'matrix',
- result: [],
- },
-};
// Graph data
diff --git a/spec/frontend/monitoring/graph_data.js b/spec/frontend/monitoring/graph_data.js
index e1b95723f3d..f85351e55d7 100644
--- a/spec/frontend/monitoring/graph_data.js
+++ b/spec/frontend/monitoring/graph_data.js
@@ -1,10 +1,38 @@
import { mapPanelToViewModel, normalizeQueryResponseData } from '~/monitoring/stores/utils';
import { panelTypes, metricStates } from '~/monitoring/constants';
-const initTime = 1435781451.781;
+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 + 15 * i, 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
@@ -82,7 +110,7 @@ const matrixMultiResult = ({ values1 = ['1', '2', '3'], values2 = ['4', '5', '6'
* @param {Object} dataOptions.isMultiSeries
*/
export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => {
- const { metricCount = 1, isMultiSeries = false } = dataOptions;
+ const { metricCount = 1, isMultiSeries = false, withLabels = true } = dataOptions;
return mapPanelToViewModel({
title: 'Time Series Panel',
@@ -90,7 +118,7 @@ export const timeSeriesGraphData = (panelOptions = {}, dataOptions = {}) => {
x_label: 'X Axis',
y_label: 'Y Axis',
metrics: Array.from(Array(metricCount), (_, i) => ({
- label: `Metric ${i + 1}`,
+ label: withLabels ? `Metric ${i + 1}` : undefined,
state: metricStates.OK,
result: isMultiSeries ? matrixMultiResult() : matrixSingleResult(),
})),
@@ -162,3 +190,59 @@ export const anomalyGraphData = (panelOptions = {}, dataOptions = {}) => {
...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(),
+ },
+ ],
+ });
+};
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index 49ad33402c6..28a7dd1af4f 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -1,3 +1,4 @@
+import invalidUrl from '~/lib/utils/invalid_url';
// 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
import { TEST_HOST } from '../helpers/test_constants';
@@ -170,7 +171,7 @@ export const environmentData = [
export const dashboardGitResponse = [
{
default: true,
- display_name: 'Default',
+ display_name: 'Overview',
can_edit: false,
system_dashboard: true,
out_of_the_box_dashboard: true,
@@ -209,7 +210,7 @@ export const selfMonitoringDashboardGitResponse = [
default: true,
display_name: 'Default',
can_edit: false,
- system_dashboard: false,
+ system_dashboard: true,
out_of_the_box_dashboard: true,
project_blob_path: null,
path: 'config/prometheus/self_monitoring_default.yml',
@@ -244,83 +245,6 @@ export const metricsResult = [
},
];
-export const graphDataPrometheusQueryRangeMultiTrack = {
- title: 'Super Chart A3',
- type: 'heatmap',
- weight: 3,
- x_label: 'Status Code',
- y_label: 'Time',
- metrics: [
- {
- metricId: '1_metric_b',
- id: 'response_metrics_nginx_ingress_throughput_status_code',
- query_range:
- 'sum(rate(nginx_upstream_responses_total{upstream=~"%{kube_namespace}-%{ci_environment_slug}-.*"}[60m])) by (status_code)',
- unit: 'req / sec',
- label: 'Status Code',
- prometheus_endpoint_path:
- '/root/rails_nodb/environments/3/prometheus/api/v1/query_range?query=sum%28rate%28nginx_upstream_responses_total%7Bupstream%3D~%22%25%7Bkube_namespace%7D-%25%7Bci_environment_slug%7D-.%2A%22%7D%5B2m%5D%29%29+by+%28status_code%29',
- result: [
- {
- metric: { status_code: '1xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 0],
- ['2019-08-30T16:00:00.000Z', 2],
- ['2019-08-30T17:00:00.000Z', 0],
- ['2019-08-30T18:00:00.000Z', 0],
- ['2019-08-30T19:00:00.000Z', 0],
- ['2019-08-30T20:00:00.000Z', 3],
- ],
- },
- {
- metric: { status_code: '2xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 1],
- ['2019-08-30T16:00:00.000Z', 3],
- ['2019-08-30T17:00:00.000Z', 6],
- ['2019-08-30T18:00:00.000Z', 10],
- ['2019-08-30T19:00:00.000Z', 8],
- ['2019-08-30T20:00:00.000Z', 6],
- ],
- },
- {
- metric: { status_code: '3xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 1],
- ['2019-08-30T16:00:00.000Z', 2],
- ['2019-08-30T17:00:00.000Z', 3],
- ['2019-08-30T18:00:00.000Z', 3],
- ['2019-08-30T19:00:00.000Z', 2],
- ['2019-08-30T20:00:00.000Z', 1],
- ],
- },
- {
- metric: { status_code: '4xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 2],
- ['2019-08-30T16:00:00.000Z', 0],
- ['2019-08-30T17:00:00.000Z', 0],
- ['2019-08-30T18:00:00.000Z', 2],
- ['2019-08-30T19:00:00.000Z', 0],
- ['2019-08-30T20:00:00.000Z', 2],
- ],
- },
- {
- metric: { status_code: '5xx' },
- values: [
- ['2019-08-30T15:00:00.000Z', 0],
- ['2019-08-30T16:00:00.000Z', 1],
- ['2019-08-30T17:00:00.000Z', 0],
- ['2019-08-30T18:00:00.000Z', 0],
- ['2019-08-30T19:00:00.000Z', 0],
- ['2019-08-30T20:00:00.000Z', 2],
- ],
- },
- ],
- },
- ],
-};
-
export const stackedColumnMockedData = {
title: 'memories',
type: 'stacked-column',
@@ -420,6 +344,11 @@ 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',
@@ -688,10 +617,28 @@ export const storeVariables = [
export const dashboardHeaderProps = {
defaultBranch: 'master',
- addDashboardDocumentationPath: 'https://path/to/docs',
isRearrangingPanels: false,
selectedTimeRange: {
start: '2020-01-01T00:00:00.000Z',
end: '2020-01-01T01:00:00.000Z',
},
};
+
+export const dashboardActionsMenuProps = {
+ defaultBranch: 'master',
+ 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/panel_new_page_spec.js b/spec/frontend/monitoring/pages/panel_new_page_spec.js
new file mode 100644
index 00000000000..83365b754d9
--- /dev/null
+++ b/spec/frontend/monitoring/pages/panel_new_page_spec.js
@@ -0,0 +1,98 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import { DASHBOARD_PAGE, PANEL_NEW_PAGE } from '~/monitoring/router/constants';
+import { createStore } from '~/monitoring/stores';
+import DashboardPanelBuilder from '~/monitoring/components/dashboard_panel_builder.vue';
+
+import PanelNewPage from '~/monitoring/pages/panel_new_page.vue';
+
+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.find(GlButtonStub);
+ const findPanelBuilder = () => wrapper.find(DashboardPanelBuilder);
+
+ beforeEach(() => {
+ store = createStore();
+ mountComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ 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
new file mode 100644
index 00000000000..a91c209875a
--- /dev/null
+++ b/spec/frontend/monitoring/requests/index_spec.js
@@ -0,0 +1,149 @@
+import MockAdapter from 'axios-mock-adapter';
+import { backoffMockImplementation } from 'jest/helpers/backoff_helper';
+import axios from '~/lib/utils/axios_utils';
+import statusCodes from '~/lib/utils/http_status';
+import * as commonUtils from '~/lib/utils/common_utils';
+import { metricsDashboardResponse } from '../fixture_data';
+import { getDashboard, getPrometheusQueryData } from '~/monitoring/requests';
+
+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(statusCodes.OK, response);
+
+ return getDashboard(dashboardEndpoint, params).then(data => {
+ expect(data).toEqual(metricsDashboardResponse);
+ });
+ });
+
+ it('returns a dashboard response after retrying twice', () => {
+ mock.onGet(dashboardEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(dashboardEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(dashboardEndpoint).reply(statusCodes.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(500);
+
+ 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(statusCodes.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(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).reply(statusCodes.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(500, {
+ status: 'error',
+ error: 'An error ocurred',
+ });
+
+ 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(statusCodes.UNAUTHORIZED, {
+ status: 'error',
+ error: 'An error ocurred',
+ });
+
+ 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(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).replyOnce(statusCodes.NO_CONTENT);
+ mock.onGet(prometheusEndpoint).reply(500, {
+ status: 'error',
+ error: 'An error ocurred',
+ }); // 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);
+ });
+ });
+
+ test.each`
+ code | reason
+ ${statusCodes.BAD_REQUEST} | ${'Parameters are missing or incorrect'}
+ ${statusCodes.UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
+ ${statusCodes.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
index 5b8f4b3c83e..8b97c8ed125 100644
--- a/spec/frontend/monitoring/router_spec.js
+++ b/spec/frontend/monitoring/router_spec.js
@@ -1,18 +1,28 @@
import { mount, createLocalVue } from '@vue/test-utils';
import VueRouter from 'vue-router';
import DashboardPage from '~/monitoring/pages/dashboard_page.vue';
+import PanelNewPage from '~/monitoring/pages/panel_new_page.vue';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
import createRouter from '~/monitoring/router';
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 propsData = { dashboardProps: { ...dashboardProps, ...dashboardHeaderProps } };
- const NEW_BASE_PATH = '/project/my-group/test-project/-/metrics';
- const OLD_BASE_PATH = '/project/my-group/test-project/-/environments/71146/metrics';
const createWrapper = (basePath, routeArg) => {
const localVue = createLocalVue();
@@ -23,11 +33,10 @@ describe('Monitoring router', () => {
router.push(routeArg);
}
- return mount(DashboardPage, {
+ return mount(MockApp, {
localVue,
store,
router,
- propsData,
});
};
@@ -40,26 +49,32 @@ describe('Monitoring router', () => {
window.location.hash = '';
});
- describe('support old URL with full dashboard path', () => {
+ describe('support legacy URLs with full dashboard path to visit dashboard page', () => {
it.each`
- route | currentDashboard
+ path | currentDashboard
${'/dashboard.yml'} | ${'dashboard.yml'}
${'/folder1/dashboard.yml'} | ${'folder1/dashboard.yml'}
${'/?dashboard=dashboard.yml'} | ${'dashboard.yml'}
- `('sets component as $componentName for path "$route"', ({ route, currentDashboard }) => {
- const wrapper = createWrapper(OLD_BASE_PATH, route);
+ `('"$path" renders page with dashboard "$currentDashboard"', ({ path, currentDashboard }) => {
+ const wrapper = createWrapper(LEGACY_BASE_PATH, path);
expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setCurrentDashboard', {
currentDashboard,
});
- expect(wrapper.find(Dashboard)).toExist();
+ expect(wrapper.find(DashboardPage).exists()).toBe(true);
+ expect(
+ wrapper
+ .find(DashboardPage)
+ .find(Dashboard)
+ .exists(),
+ ).toBe(true);
});
});
- describe('supports new URL with short dashboard path', () => {
+ describe('supports URLs to visit dashboard page', () => {
it.each`
- route | currentDashboard
+ path | currentDashboard
${'/'} | ${null}
${'/dashboard.yml'} | ${'dashboard.yml'}
${'/folder1/dashboard.yml'} | ${'folder1/dashboard.yml'}
@@ -68,14 +83,35 @@ describe('Monitoring router', () => {
${'/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'}
- `('sets component as $componentName for path "$route"', ({ route, currentDashboard }) => {
- const wrapper = createWrapper(NEW_BASE_PATH, route);
+ `('"$path" renders page with dashboard "$currentDashboard"', ({ path, currentDashboard }) => {
+ const wrapper = createWrapper(BASE_PATH, path);
expect(store.dispatch).toHaveBeenCalledWith('monitoringDashboard/setCurrentDashboard', {
currentDashboard,
});
- expect(wrapper.find(Dashboard)).toExist();
+ expect(wrapper.find(DashboardPage).exists()).toBe(true);
+ expect(
+ wrapper
+ .find(DashboardPage)
+ .find(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.find(PanelNewPage).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index 22f2b2e3c77..5c7ab4e6a1f 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -1,10 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
+import { backoffMockImplementation } from 'jest/helpers/backoff_helper';
import Tracking from '~/tracking';
import axios from '~/lib/utils/axios_utils';
import statusCodes from '~/lib/utils/http_status';
import * as commonUtils from '~/lib/utils/common_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { defaultTimeRange } from '~/vue_shared/constants';
import * as getters from '~/monitoring/stores/getters';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
@@ -30,6 +31,7 @@ import {
duplicateSystemDashboard,
updateVariablesAndFetchData,
fetchVariableMetricLabelValues,
+ fetchPanelPreview,
} from '~/monitoring/stores/actions';
import {
gqClient,
@@ -73,19 +75,7 @@ describe('Monitoring store actions', () => {
commit = jest.fn();
dispatch = jest.fn();
- jest.spyOn(commonUtils, 'backOff').mockImplementation(callback => {
- const q = new Promise((resolve, reject) => {
- const stop = arg => (arg instanceof Error ? reject(arg) : resolve(arg));
- const next = () => callback(next, stop);
- // Define a timeout based on a mock timer
- setTimeout(() => {
- callback(next, stop);
- });
- });
- // Run all resolved promises in chain
- jest.runOnlyPendingTimers();
- return q;
- });
+ jest.spyOn(commonUtils, 'backOff').mockImplementation(backoffMockImplementation);
});
afterEach(() => {
@@ -483,7 +473,6 @@ describe('Monitoring store actions', () => {
],
[],
() => {
- expect(mock.history.get).toHaveLength(1);
done();
},
).catch(done.fail);
@@ -569,46 +558,8 @@ describe('Monitoring store actions', () => {
});
});
- it('commits result, when waiting for results', done => {
- // Mock multiple attempts while the cache is filling up
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).reply(200, { data }); // 4th attempt
-
- 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).toHaveLength(4);
- done();
- },
- ).catch(done.fail);
- });
-
it('commits failure, when waiting for results and getting a server error', done => {
- // Mock multiple attempts while the cache is filling up and fails
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).replyOnce(statusCodes.NO_CONTENT);
- mock.onGet(prometheusEndpointPath).reply(500); // 4th attempt
+ mock.onGet(prometheusEndpointPath).reply(500);
const error = new Error('Request failed with status code 500');
@@ -633,7 +584,6 @@ describe('Monitoring store actions', () => {
],
[],
).catch(e => {
- expect(mock.history.get).toHaveLength(4);
expect(e).toEqual(error);
done();
});
@@ -1205,4 +1155,69 @@ describe('Monitoring store actions', () => {
);
});
});
+
+ 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(statusCodes.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(statusCodes.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(500);
+
+ 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/getters_spec.js b/spec/frontend/monitoring/store/getters_spec.js
index a69f5265ea7..509de8a4596 100644
--- a/spec/frontend/monitoring/store/getters_spec.js
+++ b/spec/frontend/monitoring/store/getters_spec.js
@@ -11,37 +11,36 @@ import {
storeVariables,
mockLinks,
} from '../mock_data';
-import {
- metricsDashboardPayload,
- metricResultStatus,
- metricResultPods,
- metricResultEmpty,
-} from '../fixture_data';
+import { metricsDashboardPayload } from '../fixture_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 state;
let getMetricStates;
- const setMetricSuccess = ({ result = metricsResult, group = 0, panel = 0, metric = 0 }) => {
- const { metricId } = state.dashboard.panelGroups[group].panels[panel].metrics[metric];
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, {
- metricId,
- data: {
- resultType: 'matrix',
- result,
- },
- });
- };
-
- const setMetricFailure = ({ group = 0, panel = 0, metric = 0 }) => {
- const { metricId } = state.dashboard.panelGroups[group].panels[panel].metrics[metric];
- mutations[types.RECEIVE_METRIC_RESULT_FAILURE](state, {
- metricId,
- });
- };
-
beforeEach(() => {
setupState = (initState = {}) => {
state = initState;
@@ -81,7 +80,7 @@ describe('Monitoring store Getters', () => {
it('on an empty metric with no result, returns NO_DATA', () => {
mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- setMetricSuccess({ result: [], group: 2 });
+ setMetricSuccess({ group: 2, result: [] });
expect(getMetricStates()).toEqual([metricStates.NO_DATA]);
});
@@ -147,7 +146,6 @@ describe('Monitoring store Getters', () => {
describe('metricsWithData', () => {
let metricsWithData;
let setupState;
- let state;
beforeEach(() => {
setupState = (initState = {}) => {
@@ -191,35 +189,39 @@ describe('Monitoring store Getters', () => {
it('an empty metric, returns empty', () => {
mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultEmpty);
+ setMetricSuccess({ result: [] });
expect(metricsWithData()).toEqual([]);
});
it('a metric with results, it returns a metric', () => {
mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultStatus);
+ setMetricSuccess();
- expect(metricsWithData()).toEqual([metricResultStatus.metricId]);
+ expect(metricsWithData()).toEqual([getMetric().metricId]);
});
it('multiple metrics with results, it return multiple metrics', () => {
mutations[types.RECEIVE_METRICS_DASHBOARD_SUCCESS](state, metricsDashboardPayload);
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultStatus);
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultPods);
+ setMetricSuccess({ panel: 0 });
+ setMetricSuccess({ panel: 1 });
- expect(metricsWithData()).toEqual([metricResultStatus.metricId, metricResultPods.metricId]);
+ 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);
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultStatus);
- mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](state, metricResultPods);
+
+ setMetricSuccess({ group: 1 });
+ setMetricSuccess({ group: 1, panel: 1 });
// First group has metrics
expect(metricsWithData(state.dashboard.panelGroups[1].key)).toEqual([
- metricResultStatus.metricId,
- metricResultPods.metricId,
+ getMetric({ group: 1 }).metricId,
+ getMetric({ group: 1, panel: 1 }).metricId,
]);
// Second group has no metrics
@@ -229,7 +231,6 @@ describe('Monitoring store Getters', () => {
});
describe('filteredEnvironments', () => {
- let state;
const setupState = (initState = {}) => {
state = {
...state,
@@ -284,7 +285,6 @@ describe('Monitoring store Getters', () => {
describe('metricsSavedToDb', () => {
let metricsSavedToDb;
- let state;
let mockData;
beforeEach(() => {
@@ -335,8 +335,6 @@ describe('Monitoring store Getters', () => {
});
describe('getCustomVariablesParams', () => {
- let state;
-
beforeEach(() => {
state = {
variables: {},
@@ -367,58 +365,65 @@ describe('Monitoring store Getters', () => {
describe('selectedDashboard', () => {
const { selectedDashboard } = getters;
- const localGetters = state => ({
- fullDashboardPath: getters.fullDashboardPath(state),
+ const localGetters = localState => ({
+ fullDashboardPath: getters.fullDashboardPath(localState),
});
it('returns a dashboard', () => {
- const state = {
+ const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: dashboardGitResponse[0].path,
customDashboardBasePath,
};
- expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[0]);
+ expect(selectedDashboard(localState, localGetters(localState))).toEqual(
+ dashboardGitResponse[0],
+ );
});
- it('returns a non-default dashboard', () => {
- const state = {
+ it('returns a dashboard different from the overview dashboard', () => {
+ const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: dashboardGitResponse[1].path,
customDashboardBasePath,
};
- expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[1]);
+ expect(selectedDashboard(localState, localGetters(localState))).toEqual(
+ dashboardGitResponse[1],
+ );
});
- it('returns a default dashboard when no dashboard is selected', () => {
- const state = {
+ it('returns the overview dashboard when no dashboard is selected', () => {
+ const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: null,
customDashboardBasePath,
};
- expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[0]);
+ expect(selectedDashboard(localState, localGetters(localState))).toEqual(
+ dashboardGitResponse[0],
+ );
});
- it('returns a default dashboard when dashboard cannot be found', () => {
- const state = {
+ it('returns the overview dashboard when dashboard cannot be found', () => {
+ const localState = {
allDashboards: dashboardGitResponse,
currentDashboard: 'wrong_path',
customDashboardBasePath,
};
- expect(selectedDashboard(state, localGetters(state))).toEqual(dashboardGitResponse[0]);
+ expect(selectedDashboard(localState, localGetters(localState))).toEqual(
+ dashboardGitResponse[0],
+ );
});
it('returns null when no dashboards are present', () => {
- const state = {
+ const localState = {
allDashboards: [],
currentDashboard: dashboardGitResponse[0].path,
customDashboardBasePath,
};
- expect(selectedDashboard(state, localGetters(state))).toEqual(null);
+ expect(selectedDashboard(localState, localGetters(localState))).toEqual(null);
});
});
describe('linksWithMetadata', () => {
- let state;
const setupState = (initState = {}) => {
state = {
...state,
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index 14b38d79aa2..8d1351fc909 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -4,8 +4,8 @@ import mutations from '~/monitoring/stores/mutations';
import * as types from '~/monitoring/stores/mutation_types';
import state from '~/monitoring/stores/state';
import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
-
import { deploymentData, dashboardGitResponse, storeTextVariables } from '../mock_data';
+import { prometheusMatrixMultiResult } from '../graph_data';
import { metricsDashboardPayload } from '../fixture_data';
describe('Monitoring mutations', () => {
@@ -259,27 +259,6 @@ describe('Monitoring mutations', () => {
describe('Individual panel/metric results', () => {
const metricId = 'NO_DB_response_metrics_nginx_ingress_throughput_status_code';
- const data = {
- resultType: 'matrix',
- result: [
- {
- metric: {
- __name__: 'up',
- job: 'prometheus',
- instance: 'localhost:9090',
- },
- values: [[1435781430.781, '1'], [1435781445.781, '1'], [1435781460.781, '1']],
- },
- {
- metric: {
- __name__: 'up',
- job: 'node',
- instance: 'localhost:9091',
- },
- values: [[1435781430.781, '0'], [1435781445.781, '0'], [1435781460.781, '1']],
- },
- ],
- };
const dashboard = metricsDashboardPayload;
const getMetric = () => stateCopy.dashboard.panelGroups[1].panels[0].metrics[0];
@@ -307,6 +286,8 @@ describe('Monitoring mutations', () => {
});
it('adds results to the store', () => {
+ const data = prometheusMatrixMultiResult();
+
expect(getMetric().result).toBe(null);
mutations[types.RECEIVE_METRIC_RESULT_SUCCESS](stateCopy, {
@@ -488,4 +469,128 @@ describe('Monitoring mutations', () => {
});
});
});
+
+ 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/utils_spec.js b/spec/frontend/monitoring/utils_spec.js
index 35ca6ba9b52..fd7d09f7f72 100644
--- a/spec/frontend/monitoring/utils_spec.js
+++ b/spec/frontend/monitoring/utils_spec.js
@@ -1,6 +1,6 @@
+import { TEST_HOST } from 'jest/helpers/test_constants';
import * as monitoringUtils from '~/monitoring/utils';
import * as urlUtils from '~/lib/utils/url_utility';
-import { TEST_HOST } from 'jest/helpers/test_constants';
import { mockProjectDir, barMockData } from './mock_data';
import { singleStatGraphData, anomalyGraphData } from './graph_data';
import { metricsDashboardViewModel, graphData } from './fixture_data';
diff --git a/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js b/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js
new file mode 100644
index 00000000000..a886715ce4b
--- /dev/null
+++ b/spec/frontend/notebook/cells/output/html_sanitize_fixtures.js
@@ -0,0 +1,114 @@
+export default [
+ [
+ 'protocol-based JS injection: simple, no spaces',
+ {
+ input: `<a href="javascript:alert('XSS');">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: simple, spaces before',
+ {
+ input: `<a href="javascript :alert('XSS');">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: simple, spaces after',
+ {
+ input: `<a href="javascript: alert('XSS');">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: simple, spaces before and after',
+ {
+ input: `<a href="javascript : alert('XSS');">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: preceding colon',
+ {
+ input: `<a href=":javascript:alert('XSS');">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: UTF-8 encoding',
+ {
+ input: '<a href="javascript&#58;">foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: long UTF-8 encoding',
+ {
+ input: '<a href="javascript&#0058;">foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: long UTF-8 encoding without semicolons',
+ {
+ input:
+ '<a href=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: hex encoding',
+ {
+ input: '<a href="javascript&#x3A;">foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: long hex encoding',
+ {
+ input: '<a href="javascript&#x003A;">foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: hex encoding without semicolons',
+ {
+ input:
+ '<a href=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: null char',
+ {
+ input: '<a href=java\u0000script:alert("XSS")>foo</a>',
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: invalid URL char',
+ { input: '<img src=javascript:alert("XSS")>', output: '<img>' },
+ ],
+ [
+ 'protocol-based JS injection: Unicode',
+ {
+ input: `<a href="\u0001java\u0003script:alert('XSS')">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'protocol-based JS injection: spaces and entities',
+ {
+ input: `<a href=" &#14; javascript:alert('XSS');">foo</a>`,
+ output: '<a>foo</a>',
+ },
+ ],
+ [
+ 'img on error',
+ {
+ input: '<img src="x" onerror="alert(document.domain)" />',
+ output: '<img src="x">',
+ },
+ ],
+ ['style tags are removed', { input: '<style>.foo {}</style> Foo', output: 'Foo' }],
+];
diff --git a/spec/frontend/notebook/cells/output/html_sanitize_tests.js b/spec/frontend/notebook/cells/output/html_sanitize_tests.js
deleted file mode 100644
index 74c48f04367..00000000000
--- a/spec/frontend/notebook/cells/output/html_sanitize_tests.js
+++ /dev/null
@@ -1,68 +0,0 @@
-export default {
- 'protocol-based JS injection: simple, no spaces': {
- input: '<a href="javascript:alert(\'XSS\');">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: simple, spaces before': {
- input: '<a href="javascript :alert(\'XSS\');">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: simple, spaces after': {
- input: '<a href="javascript: alert(\'XSS\');">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: simple, spaces before and after': {
- input: '<a href="javascript : alert(\'XSS\');">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: preceding colon': {
- input: '<a href=":javascript:alert(\'XSS\');">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: UTF-8 encoding': {
- input: '<a href="javascript&#58;">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: long UTF-8 encoding': {
- input: '<a href="javascript&#0058;">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: long UTF-8 encoding without semicolons': {
- input:
- '<a href=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: hex encoding': {
- input: '<a href="javascript&#x3A;">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: long hex encoding': {
- input: '<a href="javascript&#x003A;">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: hex encoding without semicolons': {
- input:
- '<a href=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: null char': {
- input: '<a href=java\0script:alert("XSS")>foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: invalid URL char': {
- input: '<img src=javascript:alert("XSS")>',
- output: '<img>',
- },
- 'protocol-based JS injection: Unicode': {
- input: '<a href="\u0001java\u0003script:alert(\'XSS\')">foo</a>',
- output: '<a>foo</a>',
- },
- 'protocol-based JS injection: spaces and entities': {
- input: '<a href=" &#14; javascript:alert(\'XSS\');">foo</a>',
- output: '<a>foo</a>',
- },
- 'img on error': {
- input: '<img src="x" onerror="alert(document.domain)" />',
- output: '<img src="x">',
- },
-};
diff --git a/spec/frontend/notebook/cells/output/html_spec.js b/spec/frontend/notebook/cells/output/html_spec.js
index 3ee404fb187..48d62d74a50 100644
--- a/spec/frontend/notebook/cells/output/html_spec.js
+++ b/spec/frontend/notebook/cells/output/html_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import htmlOutput from '~/notebook/cells/output/html.vue';
-import sanitizeTests from './html_sanitize_tests';
+import sanitizeTests from './html_sanitize_fixtures';
describe('html output cell', () => {
function createComponent(rawCode) {
@@ -15,17 +15,12 @@ describe('html output cell', () => {
}).$mount();
}
- describe('sanitizes output', () => {
- Object.keys(sanitizeTests).forEach(key => {
- it(key, () => {
- const test = sanitizeTests[key];
- const vm = createComponent(test.input);
- const outputEl = [...vm.$el.querySelectorAll('div')].pop();
+ it.each(sanitizeTests)('sanitizes output for: %p', (name, { input, output }) => {
+ const vm = createComponent(input);
+ const outputEl = [...vm.$el.querySelectorAll('div')].pop();
- expect(outputEl.innerHTML).toEqual(test.output);
+ expect(outputEl.innerHTML).toEqual(output);
- vm.$destroy();
- });
- });
+ vm.$destroy();
});
});
diff --git a/spec/frontend/notebook/cells/output/index_spec.js b/spec/frontend/notebook/cells/output/index_spec.js
index 2b1aa5317c5..b9a2dfb8f34 100644
--- a/spec/frontend/notebook/cells/output/index_spec.js
+++ b/spec/frontend/notebook/cells/output/index_spec.js
@@ -34,7 +34,7 @@ describe('Output component', () => {
expect(vm.$el.querySelector('pre')).not.toBeNull();
});
- it('renders promot', () => {
+ it('renders prompt', () => {
expect(vm.$el.querySelector('.prompt span')).not.toBeNull();
});
});
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
index 44dc148933c..3e1e43d0c6a 100644
--- a/spec/frontend/notes/components/discussion_actions_spec.js
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -21,7 +21,7 @@ const createUnallowedNote = () =>
describe('DiscussionActions', () => {
let wrapper;
- const createComponentFactory = (shallow = true) => props => {
+ const createComponentFactory = (shallow = true) => (props, options) => {
const store = createStore();
const mountFn = shallow ? shallowMount : mount;
@@ -35,6 +35,11 @@ describe('DiscussionActions', () => {
shouldShowJumpToNextDiscussion: true,
...props,
},
+ provide: {
+ glFeatures: {
+ hideJumpToNextUnresolvedInThreads: options?.hideJumpToNextUnresolvedInThreads,
+ },
+ },
});
};
@@ -96,6 +101,13 @@ describe('DiscussionActions', () => {
});
});
+ it('does not render jump to next discussion button if feature flag is enabled', () => {
+ const createComponent = createComponentFactory();
+ createComponent({}, { hideJumpToNextUnresolvedInThreads: true });
+
+ expect(wrapper.find(JumpToNextDiscussionButton).exists()).toBe(false);
+ });
+
describe('events handling', () => {
const createComponent = createComponentFactory(false);
diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js
index 7f042c0e9de..9a7896475e6 100644
--- a/spec/frontend/notes/components/discussion_filter_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_spec.js
@@ -1,8 +1,8 @@
-import createEventHub from '~/helpers/event_hub_factory';
import Vuex from 'vuex';
-
import { createLocalVue, mount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'jest/helpers/test_constants';
+import createEventHub from '~/helpers/event_hub_factory';
import axios from '~/lib/utils/axios_utils';
import notesModule from '~/notes/stores/modules';
@@ -10,7 +10,6 @@ import DiscussionFilter from '~/notes/components/discussion_filter.vue';
import { DISCUSSION_FILTERS_DEFAULT_VALUE, DISCUSSION_FILTER_TYPES } from '~/notes/constants';
import { discussionFiltersMock, discussionMock } from '../mock_data';
-import { TEST_HOST } from 'jest/helpers/test_constants';
const localVue = createLocalVue();
diff --git a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js b/spec/frontend/notes/components/discussion_navigator_spec.js
index e932133b869..122814b8e3f 100644
--- a/spec/frontend/notes/components/discussion_keyboard_navigator_spec.js
+++ b/spec/frontend/notes/components/discussion_navigator_spec.js
@@ -1,9 +1,11 @@
/* global Mousetrap */
import 'mousetrap';
+import Vue from 'vue';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import DiscussionKeyboardNavigator from '~/notes/components/discussion_keyboard_navigator.vue';
+import DiscussionNavigator from '~/notes/components/discussion_navigator.vue';
+import eventHub from '~/notes/event_hub';
-describe('notes/components/discussion_keyboard_navigator', () => {
+describe('notes/components/discussion_navigator', () => {
const localVue = createLocalVue();
let wrapper;
@@ -11,7 +13,7 @@ describe('notes/components/discussion_keyboard_navigator', () => {
let jumpToPreviousDiscussion;
const createComponent = () => {
- wrapper = shallowMount(DiscussionKeyboardNavigator, {
+ wrapper = shallowMount(DiscussionNavigator, {
mixins: [
localVue.extend({
methods: {
@@ -29,10 +31,29 @@ describe('notes/components/discussion_keyboard_navigator', () => {
});
afterEach(() => {
- wrapper.destroy();
+ if (wrapper) {
+ wrapper.destroy();
+ }
wrapper = null;
});
+ describe('on create', () => {
+ let onSpy;
+ let vm;
+
+ beforeEach(() => {
+ onSpy = jest.spyOn(eventHub, '$on');
+ vm = new (Vue.extend(DiscussionNavigator))();
+ });
+
+ it('listens for jumpToFirstUnresolvedDiscussion events', () => {
+ expect(onSpy).toHaveBeenCalledWith(
+ 'jumpToFirstUnresolvedDiscussion',
+ vm.jumpToFirstUnresolvedDiscussion,
+ );
+ });
+ });
+
describe('on mount', () => {
beforeEach(() => {
createComponent();
@@ -52,11 +73,16 @@ describe('notes/components/discussion_keyboard_navigator', () => {
});
describe('on destroy', () => {
+ let jumpFn;
+
beforeEach(() => {
jest.spyOn(Mousetrap, 'unbind');
+ jest.spyOn(eventHub, '$off');
createComponent();
+ jumpFn = wrapper.vm.jumpToFirstUnresolvedDiscussion;
+
wrapper.destroy();
});
@@ -65,6 +91,10 @@ describe('notes/components/discussion_keyboard_navigator', () => {
expect(Mousetrap.unbind).toHaveBeenCalledWith('p');
});
+ it('unbinds event hub listeners', () => {
+ expect(eventHub.$off).toHaveBeenCalledWith('jumpToFirstUnresolvedDiscussion', jumpFn);
+ });
+
it('does not call jumpToNextDiscussion when pressing `n`', () => {
Mousetrap.trigger('n');
diff --git a/spec/frontend/notes/components/discussion_notes_spec.js b/spec/frontend/notes/components/discussion_notes_spec.js
index 5a10deefd09..8cc98f978c2 100644
--- a/spec/frontend/notes/components/discussion_notes_spec.js
+++ b/spec/frontend/notes/components/discussion_notes_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { getByRole } from '@testing-library/dom';
import '~/behaviors/markdown/render_gfm';
import { SYSTEM_NOTE } from '~/notes/constants';
import DiscussionNotes from '~/notes/components/discussion_notes.vue';
@@ -9,14 +10,20 @@ import SystemNote from '~/vue_shared/components/notes/system_note.vue';
import createStore from '~/notes/stores';
import { noteableDataMock, discussionMock, notesDataMock } from '../mock_data';
+const LINE_RANGE = {};
+const DISCUSSION_WITH_LINE_RANGE = {
+ ...discussionMock,
+ position: {
+ line_range: LINE_RANGE,
+ },
+};
+
describe('DiscussionNotes', () => {
+ let store;
let wrapper;
- const createComponent = props => {
- const store = createStore();
- store.dispatch('setNoteableData', noteableDataMock);
- store.dispatch('setNotesData', notesDataMock);
-
+ const getList = () => getByRole(wrapper.element, 'list');
+ const createComponent = (props, features = {}) => {
wrapper = shallowMount(DiscussionNotes, {
store,
propsData: {
@@ -31,11 +38,21 @@ describe('DiscussionNotes', () => {
slots: {
'avatar-badge': '<span class="avatar-badge-slot-content" />',
},
+ provide: {
+ glFeatures: { multilineComments: true, ...features },
+ },
});
};
+ beforeEach(() => {
+ store = createStore();
+ store.dispatch('setNoteableData', noteableDataMock);
+ store.dispatch('setNotesData', notesDataMock);
+ });
+
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
describe('rendering', () => {
@@ -160,6 +177,26 @@ describe('DiscussionNotes', () => {
});
});
+ describe.each`
+ desc | props | features | event | expectedCalls
+ ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{}} | ${'mouseenter'} | ${[['setSelectedCommentPositionHover', LINE_RANGE]]}
+ ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{}} | ${'mouseleave'} | ${[['setSelectedCommentPositionHover']]}
+ ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{ multilineComments: false }} | ${'mouseenter'} | ${[]}
+ ${'with `discussion.position`'} | ${{ discussion: DISCUSSION_WITH_LINE_RANGE }} | ${{ multilineComments: false }} | ${'mouseleave'} | ${[]}
+ ${'without `discussion.position`'} | ${{}} | ${{}} | ${'mouseenter'} | ${[]}
+ ${'without `discussion.position`'} | ${{}} | ${{}} | ${'mouseleave'} | ${[]}
+ `('$desc and features $features', ({ props, event, features, expectedCalls }) => {
+ beforeEach(() => {
+ createComponent(props, features);
+ jest.spyOn(store, 'dispatch');
+ });
+
+ it(`calls store ${expectedCalls.length} times on ${event}`, () => {
+ getList().dispatchEvent(new MouseEvent(event));
+ expect(store.dispatch.mock.calls).toEqual(expectedCalls);
+ });
+ });
+
describe('componentData', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
index e62fb5db2c0..4348445f7ca 100644
--- a/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
+++ b/spec/frontend/notes/components/discussion_resolve_with_issue_button_spec.js
@@ -1,4 +1,4 @@
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { TEST_HOST } from 'spec/test_constants';
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
@@ -23,7 +23,7 @@ describe('ResolveWithIssueButton', () => {
});
it('it should have a link with the provided link property as href', () => {
- const button = wrapper.find(GlDeprecatedButton);
+ const button = wrapper.find(GlButton);
expect(button.attributes().href).toBe(url);
});
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index 5cc56cdefae..97d1752726b 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
import { shallowMount, createLocalVue, createWrapper } from '@vue/test-utils';
import { TEST_HOST } from 'spec/test_constants';
+import AxiosMockAdapter from 'axios-mock-adapter';
import createStore from '~/notes/stores';
import noteActions from '~/notes/components/note_actions.vue';
import { userDataMock } from '../mock_data';
-import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
describe('noteActions', () => {
diff --git a/spec/frontend/notes/components/note_awards_list_spec.js b/spec/frontend/notes/components/note_awards_list_spec.js
index 822b1f9efce..dce5424f154 100644
--- a/spec/frontend/notes/components/note_awards_list_spec.js
+++ b/spec/frontend/notes/components/note_awards_list_spec.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'jest/helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import createStore from '~/notes/stores';
import awardsNote from '~/notes/components/note_awards_list.vue';
import { noteableDataMock, notesDataMock } from '../mock_data';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('note_awards_list component', () => {
let store;
diff --git a/spec/frontend/notes/components/noteable_discussion_spec.js b/spec/frontend/notes/components/noteable_discussion_spec.js
index b14ec2a65be..1c6603899d3 100644
--- a/spec/frontend/notes/components/noteable_discussion_spec.js
+++ b/spec/frontend/notes/components/noteable_discussion_spec.js
@@ -1,4 +1,6 @@
import { mount, createLocalVue } from '@vue/test-utils';
+import mockDiffFile from 'jest/diffs/mock_data/diff_file';
+import { trimText } from 'helpers/text_helper';
import createStore from '~/notes/stores';
import noteableDiscussion from '~/notes/components/noteable_discussion.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
@@ -12,8 +14,6 @@ import {
loggedOutnoteableData,
userDataMock,
} from '../mock_data';
-import mockDiffFile from 'jest/diffs/mock_data/diff_file';
-import { trimText } from 'helpers/text_helper';
const discussionWithTwoUnresolvedNotes = 'merge_requests/resolved_diff_discussion.json';
@@ -89,6 +89,23 @@ describe('noteable_discussion component', () => {
});
});
+ it('should expand discussion', async () => {
+ const expandDiscussion = jest.fn();
+ const discussion = { ...discussionMock };
+ discussion.expanded = false;
+
+ wrapper.setProps({ discussion });
+ wrapper.setMethods({ expandDiscussion });
+
+ await wrapper.vm.$nextTick();
+
+ wrapper.vm.showReplyForm();
+
+ await wrapper.vm.$nextTick();
+
+ expect(expandDiscussion).toHaveBeenCalledWith({ discussionId: discussion.id });
+ });
+
it('does not render jump to thread button', () => {
expect(wrapper.find('*[data-original-title="Jump to next unresolved thread"]').exists()).toBe(
false,
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index fc238feb974..a08e86d92d3 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -83,18 +83,34 @@ describe('issue_note', () => {
});
});
- it('should render multiline comment if editing discussion root', () => {
- wrapper.setProps({ discussionRoot: true });
- wrapper.vm.isEditing = true;
-
- return wrapper.vm.$nextTick().then(() => {
- expect(findMultilineComment().exists()).toBe(true);
+ it('should only render if it has everything it needs', () => {
+ const position = {
+ line_range: {
+ start: {
+ line_code: 'abc_1_1',
+ type: null,
+ old_line: '',
+ new_line: '',
+ },
+ end: {
+ line_code: 'abc_2_2',
+ type: null,
+ old_line: '2',
+ new_line: '2',
+ },
+ },
+ };
+ const line = {
+ line_code: 'abc_1_1',
+ type: null,
+ old_line: '1',
+ new_line: '1',
+ };
+ wrapper.setProps({
+ note: { ...note, position },
+ discussionRoot: true,
+ line,
});
- });
-
- it('should not render multiline comment form unless it is the discussion root', () => {
- wrapper.setProps({ discussionRoot: false });
- wrapper.vm.isEditing = true;
return wrapper.vm.$nextTick().then(() => {
expect(findMultilineComment().exists()).toBe(false);
diff --git a/spec/frontend/notes/mixins/discussion_navigation_spec.js b/spec/frontend/notes/mixins/discussion_navigation_spec.js
index ecff95b6fe0..11c0bbfefc9 100644
--- a/spec/frontend/notes/mixins/discussion_navigation_spec.js
+++ b/spec/frontend/notes/mixins/discussion_navigation_spec.js
@@ -1,11 +1,11 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { setHTMLFixture } from 'helpers/fixtures';
import * as utils from '~/lib/utils/common_utils';
import discussionNavigation from '~/notes/mixins/discussion_navigation';
import eventHub from '~/notes/event_hub';
import createEventHub from '~/helpers/event_hub_factory';
import notesModule from '~/notes/stores/modules';
-import { setHTMLFixture } from 'helpers/fixtures';
const discussion = (id, index) => ({
id,
@@ -66,6 +66,35 @@ describe('Discussion navigation mixin', () => {
const findDiscussion = (selector, id) =>
document.querySelector(`${selector}[data-discussion-id="${id}"]`);
+ describe('jumpToFirstUnresolvedDiscussion method', () => {
+ let vm;
+
+ beforeEach(() => {
+ createComponent();
+
+ ({ vm } = wrapper);
+
+ jest.spyOn(store, 'dispatch');
+ jest.spyOn(vm, 'jumpToNextDiscussion');
+ });
+
+ it('triggers the setCurrentDiscussionId action with null as the value', () => {
+ vm.jumpToFirstUnresolvedDiscussion();
+
+ expect(store.dispatch).toHaveBeenCalledWith('setCurrentDiscussionId', null);
+ });
+
+ it('triggers the jumpToNextDiscussion action when the previous store action succeeds', () => {
+ store.dispatch.mockResolvedValue();
+
+ vm.jumpToFirstUnresolvedDiscussion();
+
+ return vm.$nextTick().then(() => {
+ expect(vm.jumpToNextDiscussion).toHaveBeenCalled();
+ });
+ });
+ });
+
describe('cycle through discussions', () => {
beforeEach(() => {
window.mrTabs = { eventHub: createEventHub(), tabShown: jest.fn() };
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 909a4a797ae..6b8d0790669 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -1,7 +1,7 @@
import { TEST_HOST } from 'spec/test_constants';
import AxiosMockAdapter from 'axios-mock-adapter';
import Api from '~/api';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
import * as actions from '~/notes/stores/actions';
import * as mutationTypes from '~/notes/stores/mutation_types';
import * as notesConstants from '~/notes/constants';
@@ -19,7 +19,9 @@ import {
} from '../mock_data';
import axios from '~/lib/utils/axios_utils';
import * as utils from '~/notes/stores/utils';
-import updateIssueConfidentialMutation from '~/sidebar/components/confidential/queries/update_issue_confidential.mutation.graphql';
+import updateIssueConfidentialMutation from '~/sidebar/components/confidential/mutations/update_issue_confidential.mutation.graphql';
+import updateMergeRequestLockMutation from '~/sidebar/components/lock/mutations/update_merge_request_lock.mutation.graphql';
+import updateIssueLockMutation from '~/sidebar/components/lock/mutations/update_issue_lock.mutation.graphql';
const TEST_ERROR_MESSAGE = 'Test error message';
jest.mock('~/flash');
@@ -1219,7 +1221,7 @@ describe('Actions Notes Store', () => {
});
});
- describe('updateConfidentialityOnIssue', () => {
+ describe('updateConfidentialityOnIssuable', () => {
state = { noteableData: { confidential: false } };
const iid = '1';
const projectPath = 'full/path';
@@ -1234,13 +1236,13 @@ describe('Actions Notes Store', () => {
});
it('calls gqClient mutation one time', () => {
- actions.updateConfidentialityOnIssue({ commit: () => {}, state, getters }, actionArgs);
+ actions.updateConfidentialityOnIssuable({ commit: () => {}, state, getters }, actionArgs);
expect(utils.gqClient.mutate).toHaveBeenCalledTimes(1);
});
it('calls gqClient mutation with the correct values', () => {
- actions.updateConfidentialityOnIssue({ commit: () => {}, state, getters }, actionArgs);
+ actions.updateConfidentialityOnIssuable({ commit: () => {}, state, getters }, actionArgs);
expect(utils.gqClient.mutate).toHaveBeenCalledWith({
mutation: updateIssueConfidentialMutation,
@@ -1253,7 +1255,7 @@ describe('Actions Notes Store', () => {
const commitSpy = jest.fn();
return actions
- .updateConfidentialityOnIssue({ commit: commitSpy, state, getters }, actionArgs)
+ .updateConfidentialityOnIssuable({ commit: commitSpy, state, getters }, actionArgs)
.then(() => {
expect(commitSpy).toHaveBeenCalledWith(
mutationTypes.SET_ISSUE_CONFIDENTIAL,
@@ -1263,4 +1265,75 @@ describe('Actions Notes Store', () => {
});
});
});
+
+ describe.each`
+ issuableType
+ ${'issue'} | ${'merge_request'}
+ `('updateLockedAttribute for issuableType=$issuableType', ({ issuableType }) => {
+ // Payload for mutation query
+ state = { noteableData: { discussion_locked: false } };
+ const targetType = issuableType;
+ const getters = { getNoteableData: { iid: '1', targetType } };
+
+ // Target state after mutation
+ const locked = true;
+ const actionArgs = { fullPath: 'full/path', locked };
+ const input = { iid: '1', projectPath: 'full/path', locked: true };
+
+ // Helper functions
+ const targetMutation = () => {
+ return targetType === 'issue' ? updateIssueLockMutation : updateMergeRequestLockMutation;
+ };
+
+ const mockResolvedValue = () => {
+ return targetType === 'issue'
+ ? { data: { issueSetLocked: { issue: { discussionLocked: locked } } } }
+ : { data: { mergeRequestSetLocked: { mergeRequest: { discussionLocked: locked } } } };
+ };
+
+ beforeEach(() => {
+ jest.spyOn(utils.gqClient, 'mutate').mockResolvedValue(mockResolvedValue());
+ });
+
+ it('calls gqClient mutation one time', () => {
+ actions.updateLockedAttribute({ commit: () => {}, state, getters }, actionArgs);
+
+ expect(utils.gqClient.mutate).toHaveBeenCalledTimes(1);
+ });
+
+ it('calls gqClient mutation with the correct values', () => {
+ actions.updateLockedAttribute({ commit: () => {}, state, getters }, actionArgs);
+
+ expect(utils.gqClient.mutate).toHaveBeenCalledWith({
+ mutation: targetMutation(),
+ variables: { input },
+ });
+ });
+
+ describe('on success of mutation', () => {
+ it('calls commit with the correct values', () => {
+ const commitSpy = jest.fn();
+
+ return actions
+ .updateLockedAttribute({ commit: commitSpy, state, getters }, actionArgs)
+ .then(() => {
+ expect(commitSpy).toHaveBeenCalledWith(mutationTypes.SET_ISSUABLE_LOCK, locked);
+ });
+ });
+ });
+ });
+
+ describe('updateDiscussionPosition', () => {
+ it('update the assignees state', done => {
+ const updatedPosition = { discussionId: 1, position: { test: true } };
+ testAction(
+ actions.updateDiscussionPosition,
+ updatedPosition,
+ { state: { discussions: [] } },
+ [{ type: mutationTypes.UPDATE_DISCUSSION_POSITION, payload: updatedPosition }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js
index 0ad18ba9b6a..b953bffc4fe 100644
--- a/spec/frontend/notes/stores/mutation_spec.js
+++ b/spec/frontend/notes/stores/mutation_spec.js
@@ -833,13 +833,27 @@ describe('Notes Store mutations', () => {
state = { noteableData: { confidential: false } };
});
- it('sets sort order', () => {
+ it('should set issuable as confidential', () => {
mutations.SET_ISSUE_CONFIDENTIAL(state, true);
expect(state.noteableData.confidential).toBe(true);
});
});
+ describe('SET_ISSUABLE_LOCK', () => {
+ let state;
+
+ beforeEach(() => {
+ state = { noteableData: { discussion_locked: false } };
+ });
+
+ it('should set issuable as locked', () => {
+ mutations.SET_ISSUABLE_LOCK(state, true);
+
+ expect(state.noteableData.discussion_locked).toBe(true);
+ });
+ });
+
describe('UPDATE_ASSIGNEES', () => {
it('should update assignees', () => {
const state = {
@@ -851,4 +865,20 @@ describe('Notes Store mutations', () => {
expect(state.noteableData.assignees).toEqual([userDataMock.id]);
});
});
+
+ describe('UPDATE_DISCUSSION_POSITION', () => {
+ it('should upate the discusion position', () => {
+ const discussion1 = { id: 1, position: { line_code: 'abc_1_1' } };
+ const discussion2 = { id: 2, position: { line_code: 'abc_2_2' } };
+ const discussion3 = { id: 3, position: { line_code: 'abc_3_3' } };
+ const state = {
+ discussions: [discussion1, discussion2, discussion3],
+ };
+ const discussion1Position = { ...discussion1.position };
+ const position = { ...discussion1Position, test: true };
+
+ mutations.UPDATE_DISCUSSION_POSITION(state, { discussionId: discussion1.id, position });
+ expect(state.discussions[0].position).toEqual(position);
+ });
+ });
});
diff --git a/spec/frontend/onboarding_issues/index_spec.js b/spec/frontend/onboarding_issues/index_spec.js
index b844caa07aa..d476ba1cf5a 100644
--- a/spec/frontend/onboarding_issues/index_spec.js
+++ b/spec/frontend/onboarding_issues/index_spec.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
+import setWindowLocation from 'helpers/set_window_location_helper';
import { showLearnGitLabIssuesPopover } from '~/onboarding_issues';
import { getCookie, setCookie, removeCookie } from '~/lib/utils/common_utils';
-import setWindowLocation from 'helpers/set_window_location_helper';
import Tracking from '~/tracking';
describe('Onboarding Issues Popovers', () => {
diff --git a/spec/frontend/operation_settings/components/metrics_settings_spec.js b/spec/frontend/operation_settings/components/metrics_settings_spec.js
index 398b61ec693..c7ea23f9913 100644
--- a/spec/frontend/operation_settings/components/metrics_settings_spec.js
+++ b/spec/frontend/operation_settings/components/metrics_settings_spec.js
@@ -1,5 +1,5 @@
import { mount, shallowMount } from '@vue/test-utils';
-import { GlDeprecatedButton, GlLink, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
+import { GlButton, GlLink, GlFormGroup, GlFormInput, GlFormSelect } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import MetricsSettings from '~/operation_settings/components/metrics_settings.vue';
@@ -9,7 +9,7 @@ import { timezones } from '~/monitoring/format_date';
import store from '~/operation_settings/store';
import axios from '~/lib/utils/axios_utils';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/flash');
@@ -56,12 +56,12 @@ describe('operation settings external dashboard component', () => {
it('renders header text', () => {
mountComponent();
- expect(wrapper.find('.js-section-header').text()).toBe('Metrics Dashboard');
+ expect(wrapper.find('.js-section-header').text()).toBe('Metrics dashboard');
});
describe('expand/collapse button', () => {
it('renders as an expand button by default', () => {
- const button = wrapper.find(GlDeprecatedButton);
+ const button = wrapper.find(GlButton);
expect(button.text()).toBe('Expand');
});
@@ -160,8 +160,7 @@ describe('operation settings external dashboard component', () => {
});
describe('submit button', () => {
- const findSubmitButton = () =>
- wrapper.find('.settings-content form').find(GlDeprecatedButton);
+ const findSubmitButton = () => wrapper.find('.settings-content form').find(GlButton);
const endpointRequest = [
operationsSettingsEndpoint,
diff --git a/spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap
new file mode 100644
index 00000000000..172b8919673
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/code_instruction_spec.js.snap
@@ -0,0 +1,46 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Package code instruction multiline to match the snapshot 1`] = `
+<div>
+ <pre
+ class="js-instruction-pre"
+ >
+ this is some
+multiline text
+ </pre>
+</div>
+`;
+
+exports[`Package code instruction single line to match the default snapshot 1`] = `
+<div
+ class="input-group gl-mb-3"
+>
+ <input
+ class="form-control monospace js-instruction-input"
+ readonly="readonly"
+ type="text"
+ />
+
+ <span
+ class="input-group-append js-instruction-button"
+ >
+ <button
+ class="btn input-group-text btn-secondary btn-md btn-default"
+ data-clipboard-text="npm i @my-package"
+ title="Copy npm install command"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ class="gl-icon s16"
+ data-testid="copy-to-clipboard-icon"
+ >
+ <use
+ href="#copy-to-clipboard"
+ />
+ </svg>
+ </button>
+ </span>
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap
new file mode 100644
index 00000000000..852292e084b
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/conan_installation_spec.js.snap
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ConanInstallation renders all the messages 1`] = `
+<div>
+ <h3
+ class="gl-font-lg"
+ >
+ Installation
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+
+ Conan Command
+
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy Conan Command"
+ instruction="foo/command"
+ trackingaction="copy_conan_command"
+ />
+
+ <h3
+ class="gl-font-lg"
+ >
+ Registry setup
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+
+ Add Conan Remote
+
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy Conan Setup Command"
+ instruction="foo/setup"
+ trackingaction="copy_conan_setup_command"
+ />
+
+ <gl-sprintf-stub
+ message="For more information on the Conan registry, %{linkStart}see the documentation%{linkEnd}."
+ />
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap
new file mode 100644
index 00000000000..28b7ca442eb
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/dependency_row_spec.js.snap
@@ -0,0 +1,34 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`DependencyRow renders full dependency 1`] = `
+<div
+ class="gl-responsive-table-row"
+>
+ <div
+ class="table-section section-50"
+ >
+ <strong
+ class="gl-text-body"
+ >
+ Test.Dependency
+ </strong>
+
+ <span
+ data-testid="target-framework"
+ >
+ (.NETStandard2.0)
+ </span>
+ </div>
+
+ <div
+ class="table-section section-50 gl-display-flex justify-content-md-end"
+ data-testid="version-pattern"
+ >
+ <span
+ class="gl-text-body"
+ >
+ 2.3.7
+ </span>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/history_element_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/history_element_spec.js.snap
new file mode 100644
index 00000000000..a1751d69c70
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/history_element_spec.js.snap
@@ -0,0 +1,38 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`History Element renders the correct markup 1`] = `
+<li
+ class="timeline-entry system-note note-wrapper gl-mb-6!"
+>
+ <div
+ class="timeline-entry-inner"
+ >
+ <div
+ class="timeline-icon"
+ >
+ <gl-icon-stub
+ name="pencil"
+ size="16"
+ />
+ </div>
+
+ <div
+ class="timeline-content"
+ >
+ <div
+ class="note-header"
+ >
+ <span>
+ <div
+ data-testid="default-slot"
+ />
+ </span>
+ </div>
+
+ <div
+ class="note-body"
+ />
+ </div>
+ </div>
+</li>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap
new file mode 100644
index 00000000000..10e54500797
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/maven_installation_spec.js.snap
@@ -0,0 +1,69 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MavenInstallation renders all the messages 1`] = `
+<div>
+ <h3
+ class="gl-font-lg"
+ >
+ Installation
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+
+ Maven XML
+
+ </h4>
+
+ <p>
+ <gl-sprintf-stub
+ message="Copy and paste this inside your %{codeStart}pom.xml%{codeEnd} %{codeStart}dependencies%{codeEnd} block."
+ />
+ </p>
+
+ <code-instruction-stub
+ copytext="Copy Maven XML"
+ instruction="foo/xml"
+ multiline="true"
+ trackingaction="copy_maven_xml"
+ />
+
+ <h4
+ class="gl-font-base"
+ >
+
+ Maven Command
+
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy Maven command"
+ instruction="foo/command"
+ trackingaction="copy_maven_command"
+ />
+
+ <h3
+ class="gl-font-lg"
+ >
+ Registry setup
+ </h3>
+
+ <p>
+ <gl-sprintf-stub
+ message="If you haven't already done so, you will need to add the below to your %{codeStart}pom.xml%{codeEnd} file."
+ />
+ </p>
+
+ <code-instruction-stub
+ copytext="Copy Maven registry XML"
+ instruction="foo/setup"
+ multiline="true"
+ trackingaction="copy_maven_setup_xml"
+ />
+
+ <gl-sprintf-stub
+ message="For more information on the Maven registry, %{linkStart}see the documentation%{linkEnd}."
+ />
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap
new file mode 100644
index 00000000000..58a509e6847
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/npm_installation_spec.js.snap
@@ -0,0 +1,69 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NpmInstallation renders all the messages 1`] = `
+<div>
+ <h3
+ class="gl-font-lg"
+ >
+ Installation
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+ npm command
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy npm command"
+ instruction="npm i @Test/package"
+ trackingaction="copy_npm_install_command"
+ />
+
+ <h4
+ class="gl-font-base"
+ >
+ yarn command
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy yarn command"
+ instruction="yarn add @Test/package"
+ trackingaction="copy_yarn_install_command"
+ />
+
+ <h3
+ class="gl-font-lg"
+ >
+ Registry setup
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+ npm command
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy npm setup command"
+ instruction="echo @Test:registry=undefined >> .npmrc"
+ trackingaction="copy_npm_setup_command"
+ />
+
+ <h4
+ class="gl-font-base"
+ >
+ yarn command
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy yarn setup command"
+ instruction="echo \\\\\\"@Test:registry\\\\\\" \\\\\\"undefined\\\\\\" >> .yarnrc"
+ trackingaction="copy_yarn_setup_command"
+ />
+
+ <gl-sprintf-stub
+ message="You may also need to setup authentication using an auth token. %{linkStart}See the documentation%{linkEnd} to find out more."
+ />
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap
new file mode 100644
index 00000000000..67810290c62
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/nuget_installation_spec.js.snap
@@ -0,0 +1,49 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`NugetInstallation renders all the messages 1`] = `
+<div>
+ <h3
+ class="gl-font-lg"
+ >
+ Installation
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+
+ NuGet Command
+
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy NuGet Command"
+ instruction="foo/command"
+ trackingaction="copy_nuget_install_command"
+ />
+
+ <h3
+ class="gl-font-lg"
+ >
+ Registry setup
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+
+ Add NuGet Source
+
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy NuGet Setup Command"
+ instruction="foo/setup"
+ trackingaction="copy_nuget_setup_command"
+ />
+
+ <gl-sprintf-stub
+ message="For more information on the NuGet registry, %{linkStart}see the documentation%{linkEnd}."
+ />
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap
new file mode 100644
index 00000000000..bdcd4a9e077
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/package_title_spec.js.snap
@@ -0,0 +1,172 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PackageTitle renders with tags 1`] = `
+<div
+ class="gl-flex-direction-column"
+>
+ <div
+ class="gl-display-flex"
+ >
+ <!---->
+
+ <div
+ class="gl-display-flex gl-flex-direction-column"
+ >
+ <h1
+ class="gl-font-size-h1 gl-mt-3 gl-mb-2"
+ >
+
+ Test package
+
+ </h1>
+
+ <div
+ class="gl-display-flex gl-align-items-center gl-text-gray-500"
+ >
+ <gl-icon-stub
+ class="gl-mr-3"
+ name="eye"
+ size="16"
+ />
+
+ <gl-sprintf-stub
+ message="v%{version} published %{timeAgo}"
+ />
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <gl-icon-stub
+ class="gl-text-gray-500 gl-mr-3"
+ name="package"
+ size="16"
+ />
+
+ <span
+ class="gl-font-weight-bold"
+ data-testid="package-type"
+ >
+ maven
+ </span>
+ </div>
+
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <package-tags-stub
+ tagdisplaylimit="1"
+ tags="[object Object],[object Object],[object Object],[object Object]"
+ />
+ </div>
+
+ <!---->
+
+ <!---->
+
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <gl-icon-stub
+ class="gl-text-gray-500 gl-mr-3"
+ name="disk"
+ size="16"
+ />
+
+ <span
+ class="gl-font-weight-bold"
+ data-testid="package-size"
+ >
+ 300 bytes
+ </span>
+ </div>
+ </div>
+</div>
+`;
+
+exports[`PackageTitle renders without tags 1`] = `
+<div
+ class="gl-flex-direction-column"
+>
+ <div
+ class="gl-display-flex"
+ >
+ <!---->
+
+ <div
+ class="gl-display-flex gl-flex-direction-column"
+ >
+ <h1
+ class="gl-font-size-h1 gl-mt-3 gl-mb-2"
+ >
+
+ Test package
+
+ </h1>
+
+ <div
+ class="gl-display-flex gl-align-items-center gl-text-gray-500"
+ >
+ <gl-icon-stub
+ class="gl-mr-3"
+ name="eye"
+ size="16"
+ />
+
+ <gl-sprintf-stub
+ message="v%{version} published %{timeAgo}"
+ />
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-display-flex gl-flex-wrap gl-align-items-center gl-mb-3"
+ >
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <gl-icon-stub
+ class="gl-text-gray-500 gl-mr-3"
+ name="package"
+ size="16"
+ />
+
+ <span
+ class="gl-font-weight-bold"
+ data-testid="package-type"
+ >
+ maven
+ </span>
+ </div>
+
+ <!---->
+
+ <!---->
+
+ <!---->
+
+ <div
+ class="gl-display-flex gl-align-items-center gl-mr-5"
+ >
+ <gl-icon-stub
+ class="gl-text-gray-500 gl-mr-3"
+ name="disk"
+ size="16"
+ />
+
+ <span
+ class="gl-font-weight-bold"
+ data-testid="package-size"
+ >
+ 300 bytes
+ </span>
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap
new file mode 100644
index 00000000000..5c1e74d73af
--- /dev/null
+++ b/spec/frontend/packages/details/components/__snapshots__/pypi_installation_spec.js.snap
@@ -0,0 +1,50 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PypiInstallation renders all the messages 1`] = `
+<div>
+ <h3
+ class="gl-font-lg"
+ >
+ Installation
+ </h3>
+
+ <h4
+ class="gl-font-base"
+ >
+
+ Pip Command
+
+ </h4>
+
+ <code-instruction-stub
+ copytext="Copy Pip command"
+ data-testid="pip-command"
+ instruction="pip install"
+ trackingaction="copy_pip_install_command"
+ />
+
+ <h3
+ class="gl-font-lg"
+ >
+ Registry setup
+ </h3>
+
+ <p>
+ <gl-sprintf-stub
+ message="If you haven't already done so, you will need to add the below to your %{codeStart}.pypirc%{codeEnd} file."
+ />
+ </p>
+
+ <code-instruction-stub
+ copytext="Copy .pypirc content"
+ data-testid="pypi-setup-content"
+ instruction="python setup"
+ multiline="true"
+ trackingaction="copy_pypi_setup_command"
+ />
+
+ <gl-sprintf-stub
+ message="For more information on the PyPi registry, %{linkStart}see the documentation%{linkEnd}."
+ />
+</div>
+`;
diff --git a/spec/frontend/packages/details/components/additional_metadata_spec.js b/spec/frontend/packages/details/components/additional_metadata_spec.js
new file mode 100644
index 00000000000..b2337b86740
--- /dev/null
+++ b/spec/frontend/packages/details/components/additional_metadata_spec.js
@@ -0,0 +1,119 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import DetailsRow from '~/registry/shared/components/details_row.vue';
+import component from '~/packages/details/components/additional_metadata.vue';
+
+import { mavenPackage, conanPackage, nugetPackage, npmPackage } from '../../mock_data';
+
+describe('Package Additional Metadata', () => {
+ let wrapper;
+ const defaultProps = {
+ packageEntity: { ...mavenPackage },
+ };
+
+ const mountComponent = props => {
+ wrapper = shallowMount(component, {
+ propsData: { ...defaultProps, ...props },
+ stubs: {
+ DetailsRow,
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findTitle = () => wrapper.find('[data-testid="title"]');
+ const findMainArea = () => wrapper.find('[data-testid="main"]');
+ const findNugetSource = () => wrapper.find('[data-testid="nuget-source"]');
+ const findNugetLicense = () => wrapper.find('[data-testid="nuget-license"]');
+ const findConanRecipe = () => wrapper.find('[data-testid="conan-recipe"]');
+ const findMavenApp = () => wrapper.find('[data-testid="maven-app"]');
+ const findMavenGroup = () => wrapper.find('[data-testid="maven-group"]');
+ const findElementLink = container => container.find(GlLink);
+
+ it('has the correct title', () => {
+ mountComponent();
+
+ const title = findTitle();
+
+ expect(title.exists()).toBe(true);
+ expect(title.text()).toBe('Additional Metadata');
+ });
+
+ describe.each`
+ packageEntity | visible | metadata
+ ${mavenPackage} | ${true} | ${'maven_metadatum'}
+ ${conanPackage} | ${true} | ${'conan_metadatum'}
+ ${nugetPackage} | ${true} | ${'nuget_metadatum'}
+ ${npmPackage} | ${false} | ${null}
+ `('Component visibility', ({ packageEntity, visible, metadata }) => {
+ it(`Is ${visible} that the component markup is visible when the package is ${packageEntity.package_type}`, () => {
+ mountComponent({ packageEntity });
+
+ expect(findTitle().exists()).toBe(visible);
+ expect(findMainArea().exists()).toBe(visible);
+ });
+
+ it(`The component is hidden if ${metadata} is missing`, () => {
+ mountComponent({ packageEntity: { ...packageEntity, [metadata]: null } });
+
+ expect(findTitle().exists()).toBe(false);
+ expect(findMainArea().exists()).toBe(false);
+ });
+ });
+
+ describe('nuget metadata', () => {
+ beforeEach(() => {
+ mountComponent({ packageEntity: nugetPackage });
+ });
+
+ it.each`
+ name | finderFunction | text | link | icon
+ ${'source'} | ${findNugetSource} | ${'Source project located at project-foo-url'} | ${'project_url'} | ${'project'}
+ ${'license'} | ${findNugetLicense} | ${'License information located at license-foo-url'} | ${'license_url'} | ${'license'}
+ `('$name element', ({ finderFunction, text, link, icon }) => {
+ const element = finderFunction();
+ expect(element.exists()).toBe(true);
+ expect(element.text()).toBe(text);
+ expect(element.props('icon')).toBe(icon);
+ expect(findElementLink(element).attributes('href')).toBe(nugetPackage.nuget_metadatum[link]);
+ });
+ });
+
+ describe('conan metadata', () => {
+ beforeEach(() => {
+ mountComponent({ packageEntity: conanPackage });
+ });
+
+ it.each`
+ name | finderFunction | text | icon
+ ${'recipe'} | ${findConanRecipe} | ${'Recipe: conan-package/1.0.0@conan+conan-package/stable'} | ${'information-o'}
+ `('$name element', ({ finderFunction, text, icon }) => {
+ const element = finderFunction();
+ expect(element.exists()).toBe(true);
+ expect(element.text()).toBe(text);
+ expect(element.props('icon')).toBe(icon);
+ });
+ });
+
+ describe('maven metadata', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it.each`
+ name | finderFunction | text | icon
+ ${'app'} | ${findMavenApp} | ${'App name: test-app'} | ${'information-o'}
+ ${'group'} | ${findMavenGroup} | ${'App group: com.test.app'} | ${'information-o'}
+ `('$name element', ({ finderFunction, text, icon }) => {
+ const element = finderFunction();
+ expect(element.exists()).toBe(true);
+ expect(element.text()).toBe(text);
+ expect(element.props('icon')).toBe(icon);
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/app_spec.js b/spec/frontend/packages/details/components/app_spec.js
new file mode 100644
index 00000000000..f535f3f5744
--- /dev/null
+++ b/spec/frontend/packages/details/components/app_spec.js
@@ -0,0 +1,281 @@
+import Vuex from 'vuex';
+import { mount, createLocalVue } from '@vue/test-utils';
+import { GlEmptyState, GlModal } from '@gitlab/ui';
+import stubChildren from 'helpers/stub_children';
+import Tracking from '~/tracking';
+import * as getters from '~/packages/details/store/getters';
+import PackagesApp from '~/packages/details/components/app.vue';
+import PackageTitle from '~/packages/details/components/package_title.vue';
+
+import * as SharedUtils from '~/packages/shared/utils';
+import { TrackingActions } from '~/packages/shared/constants';
+import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
+import PackageListRow from '~/packages/shared/components/package_list_row.vue';
+
+import DependencyRow from '~/packages/details/components/dependency_row.vue';
+import PackageHistory from '~/packages/details/components/package_history.vue';
+import AdditionalMetadata from '~/packages/details/components/additional_metadata.vue';
+import InstallationCommands from '~/packages/details/components/installation_commands.vue';
+
+import {
+ composerPackage,
+ conanPackage,
+ mavenPackage,
+ mavenFiles,
+ npmPackage,
+ npmFiles,
+ nugetPackage,
+} from '../../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('PackagesApp', () => {
+ let wrapper;
+ let store;
+ const fetchPackageVersions = jest.fn();
+
+ function createComponent({
+ packageEntity = mavenPackage,
+ packageFiles = mavenFiles,
+ isLoading = false,
+ oneColumnView = false,
+ } = {}) {
+ store = new Vuex.Store({
+ state: {
+ isLoading,
+ packageEntity,
+ packageFiles,
+ canDelete: true,
+ destroyPath: 'destroy-package-path',
+ emptySvgPath: 'empty-illustration',
+ npmPath: 'foo',
+ npmHelpPath: 'foo',
+ projectName: 'bar',
+ oneColumnView,
+ },
+ actions: {
+ fetchPackageVersions,
+ },
+ getters,
+ });
+
+ wrapper = mount(PackagesApp, {
+ localVue,
+ store,
+ stubs: {
+ ...stubChildren(PackagesApp),
+ GlButton: false,
+ GlModal: false,
+ GlTab: false,
+ GlTabs: false,
+ GlTable: false,
+ },
+ });
+ }
+
+ const packageTitle = () => wrapper.find(PackageTitle);
+ const emptyState = () => wrapper.find(GlEmptyState);
+ const allFileRows = () => wrapper.findAll('.js-file-row');
+ const firstFileDownloadLink = () => wrapper.find('.js-file-download');
+ const deleteButton = () => wrapper.find('.js-delete-button');
+ const deleteModal = () => wrapper.find(GlModal);
+ const modalDeleteButton = () => wrapper.find({ ref: 'modal-delete-button' });
+ const versionsTab = () => wrapper.find('.js-versions-tab > a');
+ const packagesLoader = () => wrapper.find(PackagesListLoader);
+ const packagesVersionRows = () => wrapper.findAll(PackageListRow);
+ const noVersionsMessage = () => wrapper.find('[data-testid="no-versions-message"]');
+ const dependenciesTab = () => wrapper.find('.js-dependencies-tab > a');
+ const dependenciesCountBadge = () => wrapper.find('[data-testid="dependencies-badge"]');
+ const noDependenciesMessage = () => wrapper.find('[data-testid="no-dependencies-message"]');
+ const dependencyRows = () => wrapper.findAll(DependencyRow);
+ const findPackageHistory = () => wrapper.find(PackageHistory);
+ const findAdditionalMetadata = () => wrapper.find(AdditionalMetadata);
+ const findInstallationCommands = () => wrapper.find(InstallationCommands);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the app and displays the package title', () => {
+ createComponent();
+
+ expect(packageTitle()).toExist();
+ });
+
+ it('renders an empty state component when no an invalid package is passed as a prop', () => {
+ createComponent({
+ packageEntity: {},
+ });
+
+ expect(emptyState()).toExist();
+ });
+
+ it('package history has the right props', () => {
+ createComponent();
+ expect(findPackageHistory().exists()).toBe(true);
+ expect(findPackageHistory().props('packageEntity')).toEqual(wrapper.vm.packageEntity);
+ expect(findPackageHistory().props('projectName')).toEqual(wrapper.vm.projectName);
+ });
+
+ it('additional metadata has the right props', () => {
+ createComponent();
+ expect(findAdditionalMetadata().exists()).toBe(true);
+ expect(findAdditionalMetadata().props('packageEntity')).toEqual(wrapper.vm.packageEntity);
+ });
+
+ it('installation commands has the right props', () => {
+ createComponent();
+ expect(findInstallationCommands().exists()).toBe(true);
+ expect(findInstallationCommands().props('packageEntity')).toEqual(wrapper.vm.packageEntity);
+ });
+
+ it('hides the files table if package type is COMPOSER', () => {
+ createComponent({ packageEntity: composerPackage });
+ expect(allFileRows().exists()).toBe(false);
+ });
+
+ it('renders a single file for an npm package as they only contain one file', () => {
+ createComponent({ packageEntity: npmPackage, packageFiles: npmFiles });
+
+ expect(allFileRows()).toExist();
+ expect(allFileRows()).toHaveLength(1);
+ });
+
+ it('renders multiple files for a package that contains more than one file', () => {
+ createComponent();
+
+ expect(allFileRows()).toExist();
+ expect(allFileRows()).toHaveLength(2);
+ });
+
+ it('allows the user to download a package file by rendering a download link', () => {
+ createComponent();
+
+ expect(allFileRows()).toExist();
+ expect(firstFileDownloadLink().vm.$attrs.href).toContain('download');
+ });
+
+ describe('deleting packages', () => {
+ beforeEach(() => {
+ createComponent();
+ deleteButton().trigger('click');
+ });
+
+ it('shows the delete confirmation modal when delete is clicked', () => {
+ expect(deleteModal()).toExist();
+ });
+ });
+
+ describe('versions', () => {
+ describe('api call', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('makes api request on first click of tab', () => {
+ versionsTab().trigger('click');
+
+ expect(fetchPackageVersions).toHaveBeenCalled();
+ });
+ });
+
+ it('displays the loader when state is loading', () => {
+ createComponent({ isLoading: true });
+
+ expect(packagesLoader().exists()).toBe(true);
+ });
+
+ it('displays the correct version count when the package has versions', () => {
+ createComponent({ packageEntity: npmPackage });
+
+ expect(packagesVersionRows()).toHaveLength(npmPackage.versions.length);
+ });
+
+ it('displays the no versions message when there are none', () => {
+ createComponent();
+
+ expect(noVersionsMessage().exists()).toBe(true);
+ });
+ });
+
+ describe('dependency links', () => {
+ it('does not show the dependency links for a non nuget package', () => {
+ createComponent();
+
+ expect(dependenciesTab().exists()).toBe(false);
+ });
+
+ it('shows the dependencies tab with 0 count when a nuget package with no dependencies', () => {
+ createComponent({
+ packageEntity: {
+ ...nugetPackage,
+ dependency_links: [],
+ },
+ });
+
+ return wrapper.vm.$nextTick(() => {
+ const dependenciesBadge = dependenciesCountBadge();
+
+ expect(dependenciesTab().exists()).toBe(true);
+ expect(dependenciesBadge.exists()).toBe(true);
+ expect(dependenciesBadge.text()).toBe('0');
+ expect(noDependenciesMessage().exists()).toBe(true);
+ });
+ });
+
+ it('renders the correct number of dependency rows for a nuget package', () => {
+ createComponent({ packageEntity: nugetPackage });
+
+ return wrapper.vm.$nextTick(() => {
+ const dependenciesBadge = dependenciesCountBadge();
+
+ expect(dependenciesTab().exists()).toBe(true);
+ expect(dependenciesBadge.exists()).toBe(true);
+ expect(dependenciesBadge.text()).toBe(nugetPackage.dependency_links.length.toString());
+ expect(dependencyRows()).toHaveLength(nugetPackage.dependency_links.length);
+ });
+ });
+ });
+
+ describe('tracking', () => {
+ let eventSpy;
+ let utilSpy;
+ const category = 'foo';
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category);
+ });
+
+ it('tracking category calls packageTypeToTrackCategory', () => {
+ createComponent({ packageEntity: conanPackage });
+ expect(wrapper.vm.tracking.category).toBe(category);
+ expect(utilSpy).toHaveBeenCalledWith('conan');
+ });
+
+ it(`delete button on delete modal call event with ${TrackingActions.DELETE_PACKAGE}`, () => {
+ createComponent({ packageEntity: conanPackage });
+ deleteButton().trigger('click');
+ return wrapper.vm.$nextTick().then(() => {
+ modalDeleteButton().trigger('click');
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ TrackingActions.DELETE_PACKAGE,
+ expect.any(Object),
+ );
+ });
+ });
+
+ it(`file download link call event with ${TrackingActions.PULL_PACKAGE}`, () => {
+ createComponent({ packageEntity: conanPackage });
+
+ firstFileDownloadLink().vm.$emit('click');
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ TrackingActions.PULL_PACKAGE,
+ expect.any(Object),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/code_instruction_spec.js b/spec/frontend/packages/details/components/code_instruction_spec.js
new file mode 100644
index 00000000000..724eddb9070
--- /dev/null
+++ b/spec/frontend/packages/details/components/code_instruction_spec.js
@@ -0,0 +1,110 @@
+import { mount } from '@vue/test-utils';
+import CodeInstruction from '~/packages/details/components/code_instruction.vue';
+import { TrackingLabels } from '~/packages/details/constants';
+import Tracking from '~/tracking';
+
+describe('Package code instruction', () => {
+ let wrapper;
+
+ const defaultProps = {
+ instruction: 'npm i @my-package',
+ copyText: 'Copy npm install command',
+ };
+
+ function createComponent(props = {}) {
+ wrapper = mount(CodeInstruction, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ }
+
+ const findInstructionInput = () => wrapper.find('.js-instruction-input');
+ const findInstructionPre = () => wrapper.find('.js-instruction-pre');
+ const findInstructionButton = () => wrapper.find('.js-instruction-button');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('single line', () => {
+ beforeEach(() => createComponent());
+
+ it('to match the default snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('multiline', () => {
+ beforeEach(() =>
+ createComponent({
+ instruction: 'this is some\nmultiline text',
+ copyText: 'Copy the command',
+ multiline: true,
+ }),
+ );
+
+ it('to match the snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('tracking', () => {
+ let eventSpy;
+ const trackingAction = 'test_action';
+ const label = TrackingLabels.CODE_INSTRUCTION;
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ });
+
+ it('should not track when no trackingAction is provided', () => {
+ createComponent();
+ findInstructionButton().trigger('click');
+
+ expect(eventSpy).toHaveBeenCalledTimes(0);
+ });
+
+ describe('when trackingAction is provided for single line', () => {
+ beforeEach(() =>
+ createComponent({
+ trackingAction,
+ }),
+ );
+
+ it('should track when copying from the input', () => {
+ findInstructionInput().trigger('copy');
+
+ expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, {
+ label,
+ });
+ });
+
+ it('should track when the copy button is pressed', () => {
+ findInstructionButton().trigger('click');
+
+ expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, {
+ label,
+ });
+ });
+ });
+
+ describe('when trackingAction is provided for multiline', () => {
+ beforeEach(() =>
+ createComponent({
+ trackingAction,
+ multiline: true,
+ }),
+ );
+
+ it('should track when copying from the multiline pre element', () => {
+ findInstructionPre().trigger('copy');
+
+ expect(eventSpy).toHaveBeenCalledWith(undefined, trackingAction, {
+ label,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/composer_installation_spec.js b/spec/frontend/packages/details/components/composer_installation_spec.js
new file mode 100644
index 00000000000..7679d721391
--- /dev/null
+++ b/spec/frontend/packages/details/components/composer_installation_spec.js
@@ -0,0 +1,95 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import { registryUrl as composerHelpPath } from 'jest/packages/details/mock_data';
+import { composerPackage as packageEntity } from 'jest/packages/mock_data';
+import ComposerInstallation from '~/packages/details/components/composer_installation.vue';
+import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import { TrackingActions } from '~/packages/details/constants';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('ComposerInstallation', () => {
+ let wrapper;
+
+ const composerRegistryIncludeStr = 'foo/registry';
+ const composerPackageIncludeStr = 'foo/package';
+
+ const store = new Vuex.Store({
+ state: {
+ packageEntity,
+ composerHelpPath,
+ },
+ getters: {
+ composerRegistryInclude: () => composerRegistryIncludeStr,
+ composerPackageInclude: () => composerPackageIncludeStr,
+ },
+ });
+
+ const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
+ const findRegistryIncludeTitle = () => wrapper.find('[data-testid="registry-include-title"]');
+ const findPackageIncludeTitle = () => wrapper.find('[data-testid="package-include-title"]');
+ const findHelpText = () => wrapper.find('[data-testid="help-text"]');
+ const findHelpLink = () => wrapper.find(GlLink);
+
+ function createComponent() {
+ wrapper = shallowMount(ComposerInstallation, {
+ localVue,
+ store,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('registry include command', () => {
+ it('uses code_instructions', () => {
+ const registryIncludeCommand = findCodeInstructions().at(0);
+ expect(registryIncludeCommand.exists()).toBe(true);
+ expect(registryIncludeCommand.props()).toMatchObject({
+ instruction: composerRegistryIncludeStr,
+ copyText: 'Copy registry include',
+ trackingAction: TrackingActions.COPY_COMPOSER_REGISTRY_INCLUDE_COMMAND,
+ });
+ });
+
+ it('has the correct title', () => {
+ expect(findRegistryIncludeTitle().text()).toBe('composer.json registry include');
+ });
+ });
+
+ describe('package include command', () => {
+ it('uses code_instructions', () => {
+ const registryIncludeCommand = findCodeInstructions().at(1);
+ expect(registryIncludeCommand.exists()).toBe(true);
+ expect(registryIncludeCommand.props()).toMatchObject({
+ instruction: composerPackageIncludeStr,
+ copyText: 'Copy require package include',
+ trackingAction: TrackingActions.COPY_COMPOSER_PACKAGE_INCLUDE_COMMAND,
+ });
+ });
+
+ it('has the correct title', () => {
+ expect(findPackageIncludeTitle().text()).toBe('composer.json require package include');
+ });
+
+ it('has the correct help text', () => {
+ expect(findHelpText().text()).toBe(
+ 'For more information on Composer packages in GitLab, see the documentation.',
+ );
+ expect(findHelpLink().attributes()).toMatchObject({
+ href: composerHelpPath,
+ target: '_blank',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/conan_installation_spec.js b/spec/frontend/packages/details/components/conan_installation_spec.js
new file mode 100644
index 00000000000..5b31e38dad5
--- /dev/null
+++ b/spec/frontend/packages/details/components/conan_installation_spec.js
@@ -0,0 +1,68 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import ConanInstallation from '~/packages/details/components/conan_installation.vue';
+import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import { conanPackage as packageEntity } from '../../mock_data';
+import { registryUrl as conanPath } from '../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('ConanInstallation', () => {
+ let wrapper;
+
+ const conanInstallationCommandStr = 'foo/command';
+ const conanSetupCommandStr = 'foo/setup';
+
+ const store = new Vuex.Store({
+ state: {
+ packageEntity,
+ conanPath,
+ },
+ getters: {
+ conanInstallationCommand: () => conanInstallationCommandStr,
+ conanSetupCommand: () => conanSetupCommandStr,
+ },
+ });
+
+ const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
+
+ function createComponent() {
+ wrapper = shallowMount(ConanInstallation, {
+ localVue,
+ store,
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) wrapper.destroy();
+ });
+
+ it('renders all the messages', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('installation commands', () => {
+ it('renders the correct command', () => {
+ expect(
+ findCodeInstructions()
+ .at(0)
+ .props('instruction'),
+ ).toBe(conanInstallationCommandStr);
+ });
+ });
+
+ describe('setup commands', () => {
+ it('renders the correct command', () => {
+ expect(
+ findCodeInstructions()
+ .at(1)
+ .props('instruction'),
+ ).toBe(conanSetupCommandStr);
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/dependency_row_spec.js b/spec/frontend/packages/details/components/dependency_row_spec.js
new file mode 100644
index 00000000000..7d3ee92908d
--- /dev/null
+++ b/spec/frontend/packages/details/components/dependency_row_spec.js
@@ -0,0 +1,62 @@
+import { shallowMount } from '@vue/test-utils';
+import DependencyRow from '~/packages/details/components/dependency_row.vue';
+import { dependencyLinks } from '../../mock_data';
+
+describe('DependencyRow', () => {
+ let wrapper;
+
+ const { withoutFramework, withoutVersion, fullLink } = dependencyLinks;
+
+ function createComponent({ dependencyLink = fullLink } = {}) {
+ wrapper = shallowMount(DependencyRow, {
+ propsData: {
+ dependency: dependencyLink,
+ },
+ });
+ }
+
+ const dependencyVersion = () => wrapper.find('[data-testid="version-pattern"]');
+ const dependencyFramework = () => wrapper.find('[data-testid="target-framework"]');
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('renders', () => {
+ it('full dependency', () => {
+ createComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('version', () => {
+ it('does not render any version information when not supplied', () => {
+ createComponent({ dependencyLink: withoutVersion });
+
+ expect(dependencyVersion().exists()).toBe(false);
+ });
+
+ it('does render version info when it exists', () => {
+ createComponent();
+
+ expect(dependencyVersion().exists()).toBe(true);
+ expect(dependencyVersion().text()).toBe(fullLink.version_pattern);
+ });
+ });
+
+ describe('target framework', () => {
+ it('does not render any framework information when not supplied', () => {
+ createComponent({ dependencyLink: withoutFramework });
+
+ expect(dependencyFramework().exists()).toBe(false);
+ });
+
+ it('does render framework info when it exists', () => {
+ createComponent();
+
+ expect(dependencyFramework().exists()).toBe(true);
+ expect(dependencyFramework().text()).toBe(`(${fullLink.target_framework})`);
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/history_element_spec.js b/spec/frontend/packages/details/components/history_element_spec.js
new file mode 100644
index 00000000000..e8746fc93f5
--- /dev/null
+++ b/spec/frontend/packages/details/components/history_element_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import component from '~/packages/details/components/history_element.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+
+describe('History Element', () => {
+ let wrapper;
+ const defaultProps = {
+ icon: 'pencil',
+ };
+
+ const mountComponent = () => {
+ wrapper = shallowMount(component, {
+ propsData: { ...defaultProps },
+ stubs: {
+ TimelineEntryItem,
+ },
+ slots: {
+ default: '<div data-testid="default-slot"></div>',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findTimelineEntry = () => wrapper.find(TimelineEntryItem);
+ const findGlIcon = () => wrapper.find(GlIcon);
+ const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
+
+ it('renders the correct markup', () => {
+ mountComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('has a default slot', () => {
+ mountComponent();
+
+ expect(findDefaultSlot().exists()).toBe(true);
+ });
+ it('has a timeline entry', () => {
+ mountComponent();
+
+ expect(findTimelineEntry().exists()).toBe(true);
+ });
+ it('has an icon', () => {
+ mountComponent();
+
+ const icon = findGlIcon();
+
+ expect(icon.exists()).toBe(true);
+ expect(icon.attributes('name')).toBe(defaultProps.icon);
+ });
+});
diff --git a/spec/frontend/packages/details/components/installations_commands_spec.js b/spec/frontend/packages/details/components/installations_commands_spec.js
new file mode 100644
index 00000000000..60da34ebcd9
--- /dev/null
+++ b/spec/frontend/packages/details/components/installations_commands_spec.js
@@ -0,0 +1,57 @@
+import { shallowMount } from '@vue/test-utils';
+import InstallationCommands from '~/packages/details/components/installation_commands.vue';
+
+import NpmInstallation from '~/packages/details/components/npm_installation.vue';
+import MavenInstallation from '~/packages/details/components/maven_installation.vue';
+import ConanInstallation from '~/packages/details/components/conan_installation.vue';
+import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
+import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
+import ComposerInstallation from '~/packages/details/components/composer_installation.vue';
+
+import {
+ conanPackage,
+ mavenPackage,
+ npmPackage,
+ nugetPackage,
+ pypiPackage,
+ composerPackage,
+} from '../../mock_data';
+
+describe('InstallationCommands', () => {
+ let wrapper;
+
+ function createComponent(propsData) {
+ wrapper = shallowMount(InstallationCommands, {
+ propsData,
+ });
+ }
+
+ const npmInstallation = () => wrapper.find(NpmInstallation);
+ const mavenInstallation = () => wrapper.find(MavenInstallation);
+ const conanInstallation = () => wrapper.find(ConanInstallation);
+ const nugetInstallation = () => wrapper.find(NugetInstallation);
+ const pypiInstallation = () => wrapper.find(PypiInstallation);
+ const composerInstallation = () => wrapper.find(ComposerInstallation);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('installation instructions', () => {
+ describe.each`
+ packageEntity | selector
+ ${conanPackage} | ${conanInstallation}
+ ${mavenPackage} | ${mavenInstallation}
+ ${npmPackage} | ${npmInstallation}
+ ${nugetPackage} | ${nugetInstallation}
+ ${pypiPackage} | ${pypiInstallation}
+ ${composerPackage} | ${composerInstallation}
+ `('renders', ({ packageEntity, selector }) => {
+ it(`${packageEntity.package_type} instructions exist`, () => {
+ createComponent({ packageEntity });
+
+ expect(selector()).toExist();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/maven_installation_spec.js b/spec/frontend/packages/details/components/maven_installation_spec.js
new file mode 100644
index 00000000000..5d0007294b6
--- /dev/null
+++ b/spec/frontend/packages/details/components/maven_installation_spec.js
@@ -0,0 +1,91 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { registryUrl as mavenPath } from 'jest/packages/details/mock_data';
+import { mavenPackage as packageEntity } from 'jest/packages/mock_data';
+import MavenInstallation from '~/packages/details/components/maven_installation.vue';
+import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import { TrackingActions } from '~/packages/details/constants';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('MavenInstallation', () => {
+ let wrapper;
+
+ const xmlCodeBlock = 'foo/xml';
+ const mavenCommandStr = 'foo/command';
+ const mavenSetupXml = 'foo/setup';
+
+ const store = new Vuex.Store({
+ state: {
+ packageEntity,
+ mavenPath,
+ },
+ getters: {
+ mavenInstallationXml: () => xmlCodeBlock,
+ mavenInstallationCommand: () => mavenCommandStr,
+ mavenSetupXml: () => mavenSetupXml,
+ },
+ });
+
+ const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
+
+ function createComponent() {
+ wrapper = shallowMount(MavenInstallation, {
+ localVue,
+ store,
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) wrapper.destroy();
+ });
+
+ it('renders all the messages', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('installation commands', () => {
+ it('renders the correct xml block', () => {
+ expect(
+ findCodeInstructions()
+ .at(0)
+ .props(),
+ ).toMatchObject({
+ instruction: xmlCodeBlock,
+ multiline: true,
+ trackingAction: TrackingActions.COPY_MAVEN_XML,
+ });
+ });
+
+ it('renders the correct maven command', () => {
+ expect(
+ findCodeInstructions()
+ .at(1)
+ .props(),
+ ).toMatchObject({
+ instruction: mavenCommandStr,
+ multiline: false,
+ trackingAction: TrackingActions.COPY_MAVEN_COMMAND,
+ });
+ });
+ });
+
+ describe('setup commands', () => {
+ it('renders the correct xml block', () => {
+ expect(
+ findCodeInstructions()
+ .at(2)
+ .props(),
+ ).toMatchObject({
+ instruction: mavenSetupXml,
+ multiline: true,
+ trackingAction: TrackingActions.COPY_MAVEN_SETUP,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/npm_installation_spec.js b/spec/frontend/packages/details/components/npm_installation_spec.js
new file mode 100644
index 00000000000..f47bac57a66
--- /dev/null
+++ b/spec/frontend/packages/details/components/npm_installation_spec.js
@@ -0,0 +1,99 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { npmPackage as packageEntity } from 'jest/packages/mock_data';
+import { registryUrl as nugetPath } from 'jest/packages/details/mock_data';
+import NpmInstallation from '~/packages/details/components/npm_installation.vue';
+import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import { TrackingActions } from '~/packages/details/constants';
+import { npmInstallationCommand, npmSetupCommand } from '~/packages/details/store/getters';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('NpmInstallation', () => {
+ let wrapper;
+
+ const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
+
+ function createComponent() {
+ const store = new Vuex.Store({
+ state: {
+ packageEntity,
+ nugetPath,
+ },
+ getters: {
+ npmInstallationCommand,
+ npmSetupCommand,
+ },
+ });
+
+ wrapper = shallowMount(NpmInstallation, {
+ localVue,
+ store,
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) wrapper.destroy();
+ });
+
+ it('renders all the messages', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('installation commands', () => {
+ it('renders the correct npm command', () => {
+ expect(
+ findCodeInstructions()
+ .at(0)
+ .props(),
+ ).toMatchObject({
+ instruction: 'npm i @Test/package',
+ multiline: false,
+ trackingAction: TrackingActions.COPY_NPM_INSTALL_COMMAND,
+ });
+ });
+
+ it('renders the correct yarn command', () => {
+ expect(
+ findCodeInstructions()
+ .at(1)
+ .props(),
+ ).toMatchObject({
+ instruction: 'yarn add @Test/package',
+ multiline: false,
+ trackingAction: TrackingActions.COPY_YARN_INSTALL_COMMAND,
+ });
+ });
+ });
+
+ describe('setup commands', () => {
+ it('renders the correct npm command', () => {
+ expect(
+ findCodeInstructions()
+ .at(2)
+ .props(),
+ ).toMatchObject({
+ instruction: 'echo @Test:registry=undefined >> .npmrc',
+ multiline: false,
+ trackingAction: TrackingActions.COPY_NPM_SETUP_COMMAND,
+ });
+ });
+
+ it('renders the correct yarn command', () => {
+ expect(
+ findCodeInstructions()
+ .at(3)
+ .props(),
+ ).toMatchObject({
+ instruction: 'echo \\"@Test:registry\\" \\"undefined\\" >> .yarnrc',
+ multiline: false,
+ trackingAction: TrackingActions.COPY_YARN_SETUP_COMMAND,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/nuget_installation_spec.js b/spec/frontend/packages/details/components/nuget_installation_spec.js
new file mode 100644
index 00000000000..a23bf9a18a1
--- /dev/null
+++ b/spec/frontend/packages/details/components/nuget_installation_spec.js
@@ -0,0 +1,75 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { nugetPackage as packageEntity } from 'jest/packages/mock_data';
+import { registryUrl as nugetPath } from 'jest/packages/details/mock_data';
+import NugetInstallation from '~/packages/details/components/nuget_installation.vue';
+import CodeInstructions from '~/packages/details/components/code_instruction.vue';
+import { TrackingActions } from '~/packages/details/constants';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('NugetInstallation', () => {
+ let wrapper;
+
+ const nugetInstallationCommandStr = 'foo/command';
+ const nugetSetupCommandStr = 'foo/setup';
+
+ const store = new Vuex.Store({
+ state: {
+ packageEntity,
+ nugetPath,
+ },
+ getters: {
+ nugetInstallationCommand: () => nugetInstallationCommandStr,
+ nugetSetupCommand: () => nugetSetupCommandStr,
+ },
+ });
+
+ const findCodeInstructions = () => wrapper.findAll(CodeInstructions);
+
+ function createComponent() {
+ wrapper = shallowMount(NugetInstallation, {
+ localVue,
+ store,
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ if (wrapper) wrapper.destroy();
+ });
+
+ it('renders all the messages', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('installation commands', () => {
+ it('renders the correct command', () => {
+ expect(
+ findCodeInstructions()
+ .at(0)
+ .props(),
+ ).toMatchObject({
+ instruction: nugetInstallationCommandStr,
+ trackingAction: TrackingActions.COPY_NUGET_INSTALL_COMMAND,
+ });
+ });
+ });
+
+ describe('setup commands', () => {
+ it('renders the correct command', () => {
+ expect(
+ findCodeInstructions()
+ .at(1)
+ .props(),
+ ).toMatchObject({
+ instruction: nugetSetupCommandStr,
+ trackingAction: TrackingActions.COPY_NUGET_SETUP_COMMAND,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/package_history_spec.js b/spec/frontend/packages/details/components/package_history_spec.js
new file mode 100644
index 00000000000..e293e119585
--- /dev/null
+++ b/spec/frontend/packages/details/components/package_history_spec.js
@@ -0,0 +1,106 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlLink, GlSprintf } from '@gitlab/ui';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import component from '~/packages/details/components/package_history.vue';
+
+import { mavenPackage, mockPipelineInfo } from '../../mock_data';
+
+describe('Package History', () => {
+ let wrapper;
+ const defaultProps = {
+ projectName: 'baz project',
+ packageEntity: { ...mavenPackage },
+ };
+
+ const mountComponent = props => {
+ wrapper = shallowMount(component, {
+ propsData: { ...defaultProps, ...props },
+ stubs: {
+ HistoryElement: '<div data-testid="history-element"><slot></slot></div>',
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findHistoryElement = testId => wrapper.find(`[data-testid="${testId}"]`);
+ const findElementLink = container => container.find(GlLink);
+ const findElementTimeAgo = container => container.find(TimeAgoTooltip);
+ const findTitle = () => wrapper.find('[data-testid="title"]');
+ const findTimeline = () => wrapper.find('[data-testid="timeline"]');
+
+ it('has the correct title', () => {
+ mountComponent();
+
+ const title = findTitle();
+
+ expect(title.exists()).toBe(true);
+ expect(title.text()).toBe('History');
+ });
+
+ it('has a timeline container', () => {
+ mountComponent();
+
+ const title = findTimeline();
+
+ expect(title.exists()).toBe(true);
+ expect(title.classes()).toEqual(
+ expect.arrayContaining(['timeline', 'main-notes-list', 'notes']),
+ );
+ });
+
+ describe.each`
+ name | icon | text | timeAgoTooltip | link
+ ${'created-on'} | ${'clock'} | ${'Test package version 1.0.0 was created'} | ${mavenPackage.created_at} | ${null}
+ ${'updated-at'} | ${'pencil'} | ${'Test package version 1.0.0 was updated'} | ${mavenPackage.updated_at} | ${null}
+ ${'commit'} | ${'commit'} | ${'Commit sha-baz on branch branch-name'} | ${null} | ${mockPipelineInfo.project.commit_url}
+ ${'pipeline'} | ${'pipeline'} | ${'Pipeline #1 triggered by foo'} | ${mockPipelineInfo.created_at} | ${mockPipelineInfo.project.pipeline_url}
+ ${'published'} | ${'package'} | ${'Published to the baz project Package Registry'} | ${mavenPackage.created_at} | ${null}
+ `('history element $name', ({ name, icon, text, timeAgoTooltip, link }) => {
+ let element;
+
+ beforeEach(() => {
+ mountComponent({ packageEntity: { ...mavenPackage, pipeline: mockPipelineInfo } });
+ element = findHistoryElement(name);
+ });
+
+ it('has the correct icon', () => {
+ expect(element.props('icon')).toBe(icon);
+ });
+
+ it('has the correct text', () => {
+ expect(element.text()).toBe(text);
+ });
+
+ it('time-ago tooltip', () => {
+ const timeAgo = findElementTimeAgo(element);
+ const exist = Boolean(timeAgoTooltip);
+
+ expect(timeAgo.exists()).toBe(exist);
+ if (exist) {
+ expect(timeAgo.props('time')).toBe(timeAgoTooltip);
+ }
+ });
+
+ it('link', () => {
+ const linkElement = findElementLink(element);
+ const exist = Boolean(link);
+
+ expect(linkElement.exists()).toBe(exist);
+ if (exist) {
+ expect(linkElement.attributes('href')).toBe(link);
+ }
+ });
+ });
+
+ describe('when pipelineInfo is missing', () => {
+ it.each(['commit', 'pipeline'])('%s history element is hidden', name => {
+ mountComponent();
+ expect(findHistoryElement(name).exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/package_title_spec.js b/spec/frontend/packages/details/components/package_title_spec.js
new file mode 100644
index 00000000000..a30dc4b8aba
--- /dev/null
+++ b/spec/frontend/packages/details/components/package_title_spec.js
@@ -0,0 +1,168 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import PackageTitle from '~/packages/details/components/package_title.vue';
+import PackageTags from '~/packages/shared/components/package_tags.vue';
+import {
+ conanPackage,
+ mavenFiles,
+ mavenPackage,
+ mockTags,
+ npmFiles,
+ npmPackage,
+ nugetPackage,
+} from '../../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('PackageTitle', () => {
+ let wrapper;
+ let store;
+
+ function createComponent({
+ packageEntity = mavenPackage,
+ packageFiles = mavenFiles,
+ icon = null,
+ } = {}) {
+ store = new Vuex.Store({
+ state: {
+ packageEntity,
+ packageFiles,
+ },
+ getters: {
+ packageTypeDisplay: ({ packageEntity: { package_type: type } }) => type,
+ packagePipeline: ({ packageEntity: { pipeline = null } }) => pipeline,
+ packageIcon: () => icon,
+ },
+ });
+
+ wrapper = shallowMount(PackageTitle, {
+ localVue,
+ store,
+ });
+ }
+
+ const packageIcon = () => wrapper.find('[data-testid="package-icon"]');
+ const packageType = () => wrapper.find('[data-testid="package-type"]');
+ const packageSize = () => wrapper.find('[data-testid="package-size"]');
+ const pipelineProject = () => wrapper.find('[data-testid="pipeline-project"]');
+ const packageRef = () => wrapper.find('[data-testid="package-ref"]');
+ const packageTags = () => wrapper.find(PackageTags);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('renders', () => {
+ it('without tags', () => {
+ createComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('with tags', () => {
+ createComponent({ packageEntity: { ...mavenPackage, tags: mockTags } });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('package icon', () => {
+ const fakeSrc = 'a-fake-src';
+
+ it('shows an icon when provided one from vuex', () => {
+ createComponent({ icon: fakeSrc });
+
+ expect(packageIcon().exists()).toBe(true);
+ });
+
+ it('has the correct src attribute', () => {
+ createComponent({ icon: fakeSrc });
+
+ expect(packageIcon().props('src')).toBe(fakeSrc);
+ });
+
+ it('does not show an icon when not provided one', () => {
+ createComponent();
+
+ expect(packageIcon().exists()).toBe(false);
+ });
+ });
+
+ describe.each`
+ packageEntity | expectedResult
+ ${conanPackage} | ${'conan'}
+ ${mavenPackage} | ${'maven'}
+ ${npmPackage} | ${'npm'}
+ ${nugetPackage} | ${'nuget'}
+ `(`package type`, ({ packageEntity, expectedResult }) => {
+ beforeEach(() => createComponent({ packageEntity }));
+
+ it(`${packageEntity.package_type} should render from Vuex getters ${expectedResult}`, () => {
+ expect(packageType().text()).toBe(expectedResult);
+ });
+ });
+
+ describe('calculates the package size', () => {
+ it('correctly calulates when there is only 1 file', () => {
+ createComponent({ packageEntity: npmPackage, packageFiles: npmFiles });
+
+ expect(packageSize().text()).toBe('200 bytes');
+ });
+
+ it('correctly calulates when there are multiple files', () => {
+ createComponent();
+
+ expect(packageSize().text()).toBe('300 bytes');
+ });
+ });
+
+ describe('package tags', () => {
+ it('displays the package-tags component when the package has tags', () => {
+ createComponent({
+ packageEntity: {
+ ...npmPackage,
+ tags: mockTags,
+ },
+ });
+
+ expect(packageTags().exists()).toBe(true);
+ });
+
+ it('does not display the package-tags component when there are no tags', () => {
+ createComponent();
+
+ expect(packageTags().exists()).toBe(false);
+ });
+ });
+
+ describe('package ref', () => {
+ it('does not display the ref if missing', () => {
+ createComponent();
+
+ expect(packageRef().exists()).toBe(false);
+ });
+
+ it('correctly shows the package ref if there is one', () => {
+ createComponent({ packageEntity: npmPackage });
+
+ expect(packageRef().contains('gl-icon-stub')).toBe(true);
+ expect(packageRef().text()).toBe(npmPackage.pipeline.ref);
+ });
+ });
+
+ describe('pipeline project', () => {
+ it('does not display the project if missing', () => {
+ createComponent();
+
+ expect(pipelineProject().exists()).toBe(false);
+ });
+
+ it('correctly shows the pipeline project if there is one', () => {
+ createComponent({ packageEntity: npmPackage });
+
+ expect(pipelineProject().text()).toBe(npmPackage.pipeline.project.name);
+ expect(pipelineProject().attributes('href')).toBe(npmPackage.pipeline.project.web_url);
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/components/pypi_installation_spec.js b/spec/frontend/packages/details/components/pypi_installation_spec.js
new file mode 100644
index 00000000000..da30b4ba565
--- /dev/null
+++ b/spec/frontend/packages/details/components/pypi_installation_spec.js
@@ -0,0 +1,60 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { pypiPackage as packageEntity } from 'jest/packages/mock_data';
+import PypiInstallation from '~/packages/details/components/pypi_installation.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('PypiInstallation', () => {
+ let wrapper;
+
+ const pipCommandStr = 'pip install';
+ const pypiSetupStr = 'python setup';
+
+ const store = new Vuex.Store({
+ state: {
+ packageEntity,
+ pypiHelpPath: 'foo',
+ },
+ getters: {
+ pypiPipCommand: () => pipCommandStr,
+ pypiSetupCommand: () => pypiSetupStr,
+ },
+ });
+
+ const pipCommand = () => wrapper.find('[data-testid="pip-command"]');
+ const setupInstruction = () => wrapper.find('[data-testid="pypi-setup-content"]');
+
+ function createComponent() {
+ wrapper = shallowMount(PypiInstallation, {
+ localVue,
+ store,
+ });
+ }
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders all the messages', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('installation commands', () => {
+ it('renders the correct pip command', () => {
+ expect(pipCommand().props('instruction')).toBe(pipCommandStr);
+ });
+ });
+
+ describe('setup commands', () => {
+ it('renders the correct setup block', () => {
+ expect(setupInstruction().props('instruction')).toBe(pypiSetupStr);
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/mock_data.js b/spec/frontend/packages/details/mock_data.js
new file mode 100644
index 00000000000..d43abcedb2e
--- /dev/null
+++ b/spec/frontend/packages/details/mock_data.js
@@ -0,0 +1,47 @@
+export const registryUrl = 'foo/registry';
+
+export const mavenMetadata = {
+ app_group: 'com.test.package.app',
+ app_name: 'test-package-app',
+ app_version: '1.0.0',
+};
+
+export const generateMavenCommand = ({
+ app_group: appGroup = '',
+ app_name: appName = '',
+ app_version: appVersion = '',
+}) => `mvn dependency:get -Dartifact=${appGroup}:${appName}:${appVersion}`;
+
+export const generateXmlCodeBlock = ({
+ app_group: appGroup = '',
+ app_name: appName = '',
+ app_version: appVersion = '',
+}) => `<dependency>
+ <groupId>${appGroup}</groupId>
+ <artifactId>${appName}</artifactId>
+ <version>${appVersion}</version>
+</dependency>`;
+
+export const generateMavenSetupXml = () => `<repositories>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>${registryUrl}</url>
+ </repository>
+</repositories>
+
+<distributionManagement>
+ <repository>
+ <id>gitlab-maven</id>
+ <url>${registryUrl}</url>
+ </repository>
+
+ <snapshotRepository>
+ <id>gitlab-maven</id>
+ <url>${registryUrl}</url>
+ </snapshotRepository>
+</distributionManagement>`;
+
+export const pypiSetupCommandStr = `[gitlab]
+repository = foo
+username = __token__
+password = <your personal access token>`;
diff --git a/spec/frontend/packages/details/store/actions_spec.js b/spec/frontend/packages/details/store/actions_spec.js
new file mode 100644
index 00000000000..6dfb2b63f85
--- /dev/null
+++ b/spec/frontend/packages/details/store/actions_spec.js
@@ -0,0 +1,76 @@
+import testAction from 'helpers/vuex_action_helper';
+import Api from '~/api';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import fetchPackageVersions from '~/packages/details/store/actions';
+import * as types from '~/packages/details/store/mutation_types';
+import { FETCH_PACKAGE_VERSIONS_ERROR } from '~/packages/details/constants';
+import { npmPackage as packageEntity } from '../../mock_data';
+
+jest.mock('~/flash.js');
+jest.mock('~/api.js');
+
+describe('Actions Package details store', () => {
+ describe('fetchPackageVersions', () => {
+ it('should fetch the package versions', done => {
+ Api.projectPackage = jest.fn().mockResolvedValue({ data: packageEntity });
+
+ testAction(
+ fetchPackageVersions,
+ undefined,
+ { packageEntity },
+ [
+ { type: types.SET_LOADING, payload: true },
+ { type: types.SET_PACKAGE_VERSIONS, payload: packageEntity.versions },
+ { type: types.SET_LOADING, payload: false },
+ ],
+ [],
+ () => {
+ expect(Api.projectPackage).toHaveBeenCalledWith(
+ packageEntity.project_id,
+ packageEntity.id,
+ );
+ done();
+ },
+ );
+ });
+
+ it("does not set the versions if they don't exist", done => {
+ Api.projectPackage = jest.fn().mockResolvedValue({ data: { packageEntity, versions: null } });
+
+ testAction(
+ fetchPackageVersions,
+ undefined,
+ { packageEntity },
+ [{ type: types.SET_LOADING, payload: true }, { type: types.SET_LOADING, payload: false }],
+ [],
+ () => {
+ expect(Api.projectPackage).toHaveBeenCalledWith(
+ packageEntity.project_id,
+ packageEntity.id,
+ );
+ done();
+ },
+ );
+ });
+
+ it('should create flash on API error', done => {
+ Api.projectPackage = jest.fn().mockRejectedValue();
+
+ testAction(
+ fetchPackageVersions,
+ undefined,
+ { packageEntity },
+ [{ type: types.SET_LOADING, payload: true }, { type: types.SET_LOADING, payload: false }],
+ [],
+ () => {
+ expect(Api.projectPackage).toHaveBeenCalledWith(
+ packageEntity.project_id,
+ packageEntity.id,
+ );
+ expect(createFlash).toHaveBeenCalledWith(FETCH_PACKAGE_VERSIONS_ERROR);
+ done();
+ },
+ );
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/store/getters_spec.js b/spec/frontend/packages/details/store/getters_spec.js
new file mode 100644
index 00000000000..307976d4124
--- /dev/null
+++ b/spec/frontend/packages/details/store/getters_spec.js
@@ -0,0 +1,237 @@
+import {
+ conanInstallationCommand,
+ conanSetupCommand,
+ packagePipeline,
+ packageTypeDisplay,
+ packageIcon,
+ mavenInstallationXml,
+ mavenInstallationCommand,
+ mavenSetupXml,
+ npmInstallationCommand,
+ npmSetupCommand,
+ nugetInstallationCommand,
+ nugetSetupCommand,
+ pypiPipCommand,
+ pypiSetupCommand,
+ composerRegistryInclude,
+ composerPackageInclude,
+} from '~/packages/details/store/getters';
+import {
+ conanPackage,
+ npmPackage,
+ nugetPackage,
+ mockPipelineInfo,
+ mavenPackage as packageWithoutBuildInfo,
+ pypiPackage,
+} from '../../mock_data';
+import {
+ generateMavenCommand,
+ generateXmlCodeBlock,
+ generateMavenSetupXml,
+ registryUrl,
+ pypiSetupCommandStr,
+} from '../mock_data';
+import { generateConanRecipe } from '~/packages/details/utils';
+import { NpmManager } from '~/packages/details/constants';
+
+describe('Getters PackageDetails Store', () => {
+ let state;
+
+ const defaultState = {
+ packageEntity: packageWithoutBuildInfo,
+ conanPath: registryUrl,
+ mavenPath: registryUrl,
+ npmPath: registryUrl,
+ nugetPath: registryUrl,
+ pypiPath: registryUrl,
+ };
+
+ const setupState = (testState = {}) => {
+ state = {
+ ...defaultState,
+ ...testState,
+ };
+ };
+
+ const recipe = generateConanRecipe(conanPackage);
+ const conanInstallationCommandStr = `conan install ${recipe} --remote=gitlab`;
+ const conanSetupCommandStr = `conan remote add gitlab ${registryUrl}`;
+
+ const mavenCommandStr = generateMavenCommand(packageWithoutBuildInfo.maven_metadatum);
+ const mavenInstallationXmlBlock = generateXmlCodeBlock(packageWithoutBuildInfo.maven_metadatum);
+ const mavenSetupXmlBlock = generateMavenSetupXml();
+
+ const npmInstallStr = `npm i ${npmPackage.name}`;
+ const npmSetupStr = `echo @Test:registry=${registryUrl} >> .npmrc`;
+ const yarnInstallStr = `yarn add ${npmPackage.name}`;
+ const yarnSetupStr = `echo \\"@Test:registry\\" \\"${registryUrl}\\" >> .yarnrc`;
+
+ const nugetInstallationCommandStr = `nuget install ${nugetPackage.name} -Source "GitLab"`;
+ const nugetSetupCommandStr = `nuget source Add -Name "GitLab" -Source "${registryUrl}" -UserName <your_username> -Password <your_token>`;
+
+ const pypiPipCommandStr = `pip install ${pypiPackage.name} --index-url ${registryUrl}`;
+ const composerRegistryIncludeStr = '{"type":"composer","url":"foo"}';
+ const composerPackageIncludeStr = JSON.stringify({
+ [packageWithoutBuildInfo.name]: packageWithoutBuildInfo.version,
+ });
+
+ describe('packagePipeline', () => {
+ it('should return the pipeline info when pipeline exists', () => {
+ setupState({
+ packageEntity: {
+ ...npmPackage,
+ pipeline: mockPipelineInfo,
+ },
+ });
+
+ expect(packagePipeline(state)).toEqual(mockPipelineInfo);
+ });
+
+ it('should return null when build_info does not exist', () => {
+ setupState();
+
+ expect(packagePipeline(state)).toBe(null);
+ });
+ });
+
+ describe('packageTypeDisplay', () => {
+ describe.each`
+ packageEntity | expectedResult
+ ${conanPackage} | ${'Conan'}
+ ${packageWithoutBuildInfo} | ${'Maven'}
+ ${npmPackage} | ${'NPM'}
+ ${nugetPackage} | ${'NuGet'}
+ ${pypiPackage} | ${'PyPi'}
+ `(`package type`, ({ packageEntity, expectedResult }) => {
+ beforeEach(() => setupState({ packageEntity }));
+
+ it(`${packageEntity.package_type} should show as ${expectedResult}`, () => {
+ expect(packageTypeDisplay(state)).toBe(expectedResult);
+ });
+ });
+ });
+
+ describe('packageIcon', () => {
+ describe('nuget packages', () => {
+ it('should return nuget package icon', () => {
+ setupState({ packageEntity: nugetPackage });
+
+ expect(packageIcon(state)).toBe(nugetPackage.nuget_metadatum.icon_url);
+ });
+
+ it('should return null when nuget package does not have an icon', () => {
+ setupState({ packageEntity: { ...nugetPackage, nuget_metadatum: {} } });
+
+ expect(packageIcon(state)).toBe(null);
+ });
+ });
+
+ it('should not find icons for other package types', () => {
+ setupState({ packageEntity: npmPackage });
+
+ expect(packageIcon(state)).toBe(null);
+ });
+ });
+
+ describe('conan string getters', () => {
+ it('gets the correct conanInstallationCommand', () => {
+ setupState({ packageEntity: conanPackage });
+
+ expect(conanInstallationCommand(state)).toBe(conanInstallationCommandStr);
+ });
+
+ it('gets the correct conanSetupCommand', () => {
+ setupState({ packageEntity: conanPackage });
+
+ expect(conanSetupCommand(state)).toBe(conanSetupCommandStr);
+ });
+ });
+
+ describe('maven string getters', () => {
+ it('gets the correct mavenInstallationXml', () => {
+ setupState();
+
+ expect(mavenInstallationXml(state)).toBe(mavenInstallationXmlBlock);
+ });
+
+ it('gets the correct mavenInstallationCommand', () => {
+ setupState();
+
+ expect(mavenInstallationCommand(state)).toBe(mavenCommandStr);
+ });
+
+ it('gets the correct mavenSetupXml', () => {
+ setupState();
+
+ expect(mavenSetupXml(state)).toBe(mavenSetupXmlBlock);
+ });
+ });
+
+ describe('npm string getters', () => {
+ it('gets the correct npmInstallationCommand for NPM', () => {
+ setupState({ packageEntity: npmPackage });
+
+ expect(npmInstallationCommand(state)(NpmManager.NPM)).toBe(npmInstallStr);
+ });
+
+ it('gets the correct npmSetupCommand for NPM', () => {
+ setupState({ packageEntity: npmPackage });
+
+ expect(npmSetupCommand(state)(NpmManager.NPM)).toBe(npmSetupStr);
+ });
+
+ it('gets the correct npmInstallationCommand for Yarn', () => {
+ setupState({ packageEntity: npmPackage });
+
+ expect(npmInstallationCommand(state)(NpmManager.YARN)).toBe(yarnInstallStr);
+ });
+
+ it('gets the correct npmSetupCommand for Yarn', () => {
+ setupState({ packageEntity: npmPackage });
+
+ expect(npmSetupCommand(state)(NpmManager.YARN)).toBe(yarnSetupStr);
+ });
+ });
+
+ describe('nuget string getters', () => {
+ it('gets the correct nugetInstallationCommand', () => {
+ setupState({ packageEntity: nugetPackage });
+
+ expect(nugetInstallationCommand(state)).toBe(nugetInstallationCommandStr);
+ });
+
+ it('gets the correct nugetSetupCommand', () => {
+ setupState({ packageEntity: nugetPackage });
+
+ expect(nugetSetupCommand(state)).toBe(nugetSetupCommandStr);
+ });
+ });
+
+ describe('pypi string getters', () => {
+ it('gets the correct pypiPipCommand', () => {
+ setupState({ packageEntity: pypiPackage });
+
+ expect(pypiPipCommand(state)).toBe(pypiPipCommandStr);
+ });
+
+ it('gets the correct pypiSetupCommand', () => {
+ setupState({ pypiSetupPath: 'foo' });
+
+ expect(pypiSetupCommand(state)).toBe(pypiSetupCommandStr);
+ });
+ });
+
+ describe('composer string getters', () => {
+ it('gets the correct composerRegistryInclude command', () => {
+ setupState({ composerPath: 'foo' });
+
+ expect(composerRegistryInclude(state)).toBe(composerRegistryIncludeStr);
+ });
+
+ it('gets the correct composerPackageInclude command', () => {
+ setupState();
+
+ expect(composerPackageInclude(state)).toBe(composerPackageIncludeStr);
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/store/mutations_spec.js b/spec/frontend/packages/details/store/mutations_spec.js
new file mode 100644
index 00000000000..501a56dcdde
--- /dev/null
+++ b/spec/frontend/packages/details/store/mutations_spec.js
@@ -0,0 +1,31 @@
+import mutations from '~/packages/details/store/mutations';
+import * as types from '~/packages/details/store/mutation_types';
+import { npmPackage as packageEntity } from '../../mock_data';
+
+describe('Mutations package details Store', () => {
+ let mockState;
+
+ beforeEach(() => {
+ mockState = {
+ packageEntity,
+ };
+ });
+
+ describe('SET_LOADING', () => {
+ it('should set loading', () => {
+ mutations[types.SET_LOADING](mockState, true);
+
+ expect(mockState.isLoading).toEqual(true);
+ });
+ });
+
+ describe('SET_PACKAGE_VERSIONS', () => {
+ it('should set the package entity versions', () => {
+ const fakeVersions = [1, 2, 3];
+
+ mutations[types.SET_PACKAGE_VERSIONS](mockState, fakeVersions);
+
+ expect(mockState.packageEntity.versions).toEqual(fakeVersions);
+ });
+ });
+});
diff --git a/spec/frontend/packages/details/utils_spec.js b/spec/frontend/packages/details/utils_spec.js
new file mode 100644
index 00000000000..087888016ee
--- /dev/null
+++ b/spec/frontend/packages/details/utils_spec.js
@@ -0,0 +1,24 @@
+import { generateConanRecipe } from '~/packages/details/utils';
+import { conanPackage } from '../mock_data';
+
+describe('Package detail utils', () => {
+ describe('generateConanRecipe', () => {
+ it('correctly generates the conan recipe', () => {
+ const recipe = generateConanRecipe(conanPackage);
+
+ expect(recipe).toEqual(conanPackage.recipe);
+ });
+
+ it('returns an empty recipe when no information is supplied', () => {
+ const recipe = generateConanRecipe({});
+
+ expect(recipe).toEqual('/@/');
+ });
+
+ it('recipe returns empty strings for missing metadata', () => {
+ const recipe = generateConanRecipe({ name: 'foo', version: '0.0.1' });
+
+ expect(recipe).toBe('foo/0.0.1@/');
+ });
+ });
+});
diff --git a/spec/frontend/packages/list/coming_soon/helpers_spec.js b/spec/frontend/packages/list/coming_soon/helpers_spec.js
new file mode 100644
index 00000000000..4a996bfad76
--- /dev/null
+++ b/spec/frontend/packages/list/coming_soon/helpers_spec.js
@@ -0,0 +1,36 @@
+import * as comingSoon from '~/packages/list/coming_soon/helpers';
+import { fakeIssues, asGraphQLResponse, asViewModel } from './mock_data';
+
+jest.mock('~/api.js');
+
+describe('Coming Soon Helpers', () => {
+ const [noLabels, acceptingMergeRequestLabel, workflowLabel] = fakeIssues;
+
+ describe('toViewModel', () => {
+ it('formats a GraphQL response correctly', () => {
+ expect(comingSoon.toViewModel(asGraphQLResponse)).toEqual(asViewModel);
+ });
+ });
+
+ describe('findWorkflowLabel', () => {
+ it('finds a workflow label', () => {
+ expect(comingSoon.findWorkflowLabel(workflowLabel.labels)).toEqual(workflowLabel.labels[0]);
+ });
+
+ it("returns undefined when there isn't one", () => {
+ expect(comingSoon.findWorkflowLabel(noLabels.labels)).toBeUndefined();
+ });
+ });
+
+ describe('findAcceptingContributionsLabel', () => {
+ it('finds the correct label when it exists', () => {
+ expect(comingSoon.findAcceptingContributionsLabel(acceptingMergeRequestLabel.labels)).toEqual(
+ acceptingMergeRequestLabel.labels[0],
+ );
+ });
+
+ it("returns undefined when there isn't one", () => {
+ expect(comingSoon.findAcceptingContributionsLabel(noLabels.labels)).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/frontend/packages/list/coming_soon/mock_data.js b/spec/frontend/packages/list/coming_soon/mock_data.js
new file mode 100644
index 00000000000..bb4568e4bd5
--- /dev/null
+++ b/spec/frontend/packages/list/coming_soon/mock_data.js
@@ -0,0 +1,90 @@
+export const fakeIssues = [
+ {
+ id: 1,
+ iid: 1,
+ title: 'issue one',
+ webUrl: 'foo',
+ },
+ {
+ id: 2,
+ iid: 2,
+ title: 'issue two',
+ labels: [{ title: 'Accepting merge requests', color: '#69d100' }],
+ milestone: {
+ title: '12.10',
+ },
+ webUrl: 'foo',
+ },
+ {
+ id: 3,
+ iid: 3,
+ title: 'issue three',
+ labels: [{ title: 'workflow::In dev', color: '#428bca' }],
+ webUrl: 'foo',
+ },
+ {
+ id: 4,
+ iid: 4,
+ title: 'issue four',
+ labels: [
+ { title: 'Accepting merge requests', color: '#69d100' },
+ { title: 'workflow::In dev', color: '#428bca' },
+ ],
+ webUrl: 'foo',
+ },
+];
+
+export const asGraphQLResponse = {
+ project: {
+ issues: {
+ nodes: fakeIssues.map(x => ({
+ ...x,
+ labels: {
+ nodes: x.labels,
+ },
+ })),
+ },
+ },
+};
+
+export const asViewModel = [
+ {
+ ...fakeIssues[0],
+ labels: [],
+ },
+ {
+ ...fakeIssues[1],
+ labels: [
+ {
+ title: 'Accepting merge requests',
+ color: '#69d100',
+ scoped: false,
+ },
+ ],
+ },
+ {
+ ...fakeIssues[2],
+ labels: [
+ {
+ title: 'workflow::In dev',
+ color: '#428bca',
+ scoped: true,
+ },
+ ],
+ },
+ {
+ ...fakeIssues[3],
+ labels: [
+ {
+ title: 'workflow::In dev',
+ color: '#428bca',
+ scoped: true,
+ },
+ {
+ title: 'Accepting merge requests',
+ color: '#69d100',
+ scoped: false,
+ },
+ ],
+ },
+];
diff --git a/spec/frontend/packages/list/coming_soon/packages_coming_soon_spec.js b/spec/frontend/packages/list/coming_soon/packages_coming_soon_spec.js
new file mode 100644
index 00000000000..c4cdadc45e6
--- /dev/null
+++ b/spec/frontend/packages/list/coming_soon/packages_coming_soon_spec.js
@@ -0,0 +1,138 @@
+import { GlEmptyState, GlSkeletonLoader, GlLabel } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
+import VueApollo, { ApolloQuery } from 'vue-apollo';
+import ComingSoon from '~/packages/list/coming_soon/packages_coming_soon.vue';
+import { TrackingActions } from '~/packages/shared/constants';
+import { asViewModel } from './mock_data';
+import Tracking from '~/tracking';
+
+jest.mock('~/packages/list/coming_soon/helpers.js');
+
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('packages_coming_soon', () => {
+ let wrapper;
+
+ const findSkeletonLoader = () => wrapper.find(GlSkeletonLoader);
+ const findAllIssues = () => wrapper.findAll('[data-testid="issue-row"]');
+ const findIssuesData = () =>
+ findAllIssues().wrappers.map(x => {
+ const titleLink = x.find('[data-testid="issue-title-link"]');
+ const milestone = x.find('[data-testid="milestone"]');
+ const issueIdLink = x.find('[data-testid="issue-id-link"]');
+ const labels = x.findAll(GlLabel);
+
+ const issueId = Number(issueIdLink.text().substr(1));
+
+ return {
+ id: issueId,
+ iid: issueId,
+ title: titleLink.text(),
+ webUrl: titleLink.attributes('href'),
+ labels: labels.wrappers.map(label => ({
+ color: label.props('backgroundColor'),
+ title: label.props('title'),
+ scoped: label.props('scoped'),
+ })),
+ ...(milestone.exists() ? { milestone: { title: milestone.text() } } : {}),
+ };
+ });
+ const findIssueTitleLink = () => wrapper.find('[data-testid="issue-title-link"]');
+ const findIssueIdLink = () => wrapper.find('[data-testid="issue-id-link"]');
+ const findEmptyState = () => wrapper.find(GlEmptyState);
+
+ const mountComponent = (testParams = {}) => {
+ const $apolloData = {
+ loading: testParams.isLoading || false,
+ };
+
+ wrapper = mount(ComingSoon, {
+ localVue,
+ propsData: {
+ illustration: 'foo',
+ projectPath: 'foo',
+ suggestedContributionsPath: 'foo',
+ },
+ stubs: {
+ ApolloQuery,
+ GlLink: true,
+ },
+ mocks: {
+ $apolloData,
+ },
+ });
+
+ // Mock the GraphQL query result
+ wrapper.find(ApolloQuery).setData({
+ result: {
+ data: testParams.issues || asViewModel,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when loading', () => {
+ beforeEach(() => mountComponent({ isLoading: true }));
+
+ it('renders the skeleton loader', () => {
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+ });
+
+ describe('when there are no issues', () => {
+ beforeEach(() => mountComponent({ issues: [] }));
+
+ it('renders the empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+ });
+
+ describe('when there are issues', () => {
+ beforeEach(() => mountComponent());
+
+ it('renders each issue', () => {
+ expect(findIssuesData()).toEqual(asViewModel);
+ });
+ });
+
+ describe('tracking', () => {
+ const firstIssue = asViewModel[0];
+ let eventSpy;
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ mountComponent();
+ });
+
+ it('tracks when mounted', () => {
+ expect(eventSpy).toHaveBeenCalledWith(undefined, TrackingActions.COMING_SOON_REQUESTED, {});
+ });
+
+ it('tracks when an issue title link is clicked', () => {
+ eventSpy.mockClear();
+
+ findIssueTitleLink().vm.$emit('click');
+
+ expect(eventSpy).toHaveBeenCalledWith(undefined, TrackingActions.COMING_SOON_LIST, {
+ label: firstIssue.title,
+ value: firstIssue.iid,
+ });
+ });
+
+ it('tracks when an issue id link is clicked', () => {
+ eventSpy.mockClear();
+
+ findIssueIdLink().vm.$emit('click');
+
+ expect(eventSpy).toHaveBeenCalledWith(undefined, TrackingActions.COMING_SOON_LIST, {
+ label: firstIssue.title,
+ value: firstIssue.iid,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/list/components/__snapshots__/packages_filter_spec.js.snap b/spec/frontend/packages/list/components/__snapshots__/packages_filter_spec.js.snap
new file mode 100644
index 00000000000..ed77f25916f
--- /dev/null
+++ b/spec/frontend/packages/list/components/__snapshots__/packages_filter_spec.js.snap
@@ -0,0 +1,14 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`packages_filter renders 1`] = `
+<gl-search-box-by-click-stub
+ clearable="true"
+ clearbuttontitle="Clear"
+ clearrecentsearchestext="Clear recent searches"
+ closebuttontitle="Close"
+ norecentsearchestext="You don't have any recent searches"
+ placeholder="Filter by name"
+ recentsearchesheader="Recent searches"
+ value=""
+/>
+`;
diff --git a/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
new file mode 100644
index 00000000000..2b7a4c83bed
--- /dev/null
+++ b/spec/frontend/packages/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -0,0 +1,457 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`packages_list_app renders 1`] = `
+<b-tabs-stub
+ activenavitemclass="gl-tab-nav-item-active gl-tab-nav-item-active-indigo"
+ class="gl-tabs"
+ contentclass=",gl-tab-content"
+ navclass="gl-tabs-nav"
+ nofade="true"
+ nonavstyle="true"
+ tag="div"
+>
+ <template>
+
+ <b-tab-stub
+ tag="div"
+ title="All"
+ titlelinkclass="gl-tab-nav-item"
+ >
+ <template>
+ <div>
+ <section
+ class="row empty-state text-center"
+ >
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt="There are no packages yet"
+ class="gl-max-w-full"
+ src="helpSvg"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ >
+ <h1
+ class="h4"
+ >
+ There are no packages yet
+ </h1>
+
+ <p>
+ Learn how to
+ <b-link-stub
+ class="gl-link"
+ event="click"
+ href="helpUrl"
+ routertag="a"
+ target="_blank"
+ >
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+
+ <div>
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+ </template>
+ </b-tab-stub>
+ <b-tab-stub
+ tag="div"
+ title="Composer"
+ titlelinkclass="gl-tab-nav-item"
+ >
+ <template>
+ <div>
+ <section
+ class="row empty-state text-center"
+ >
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt="There are no Composer packages yet"
+ class="gl-max-w-full"
+ src="helpSvg"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ >
+ <h1
+ class="h4"
+ >
+ There are no Composer packages yet
+ </h1>
+
+ <p>
+ Learn how to
+ <b-link-stub
+ class="gl-link"
+ event="click"
+ href="helpUrl"
+ routertag="a"
+ target="_blank"
+ >
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+
+ <div>
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+ </template>
+ </b-tab-stub>
+ <b-tab-stub
+ tag="div"
+ title="Conan"
+ titlelinkclass="gl-tab-nav-item"
+ >
+ <template>
+ <div>
+ <section
+ class="row empty-state text-center"
+ >
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt="There are no Conan packages yet"
+ class="gl-max-w-full"
+ src="helpSvg"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ >
+ <h1
+ class="h4"
+ >
+ There are no Conan packages yet
+ </h1>
+
+ <p>
+ Learn how to
+ <b-link-stub
+ class="gl-link"
+ event="click"
+ href="helpUrl"
+ routertag="a"
+ target="_blank"
+ >
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+
+ <div>
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+ </template>
+ </b-tab-stub>
+ <b-tab-stub
+ tag="div"
+ title="Maven"
+ titlelinkclass="gl-tab-nav-item"
+ >
+ <template>
+ <div>
+ <section
+ class="row empty-state text-center"
+ >
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt="There are no Maven packages yet"
+ class="gl-max-w-full"
+ src="helpSvg"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ >
+ <h1
+ class="h4"
+ >
+ There are no Maven packages yet
+ </h1>
+
+ <p>
+ Learn how to
+ <b-link-stub
+ class="gl-link"
+ event="click"
+ href="helpUrl"
+ routertag="a"
+ target="_blank"
+ >
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+
+ <div>
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+ </template>
+ </b-tab-stub>
+ <b-tab-stub
+ tag="div"
+ title="NPM"
+ titlelinkclass="gl-tab-nav-item"
+ >
+ <template>
+ <div>
+ <section
+ class="row empty-state text-center"
+ >
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt="There are no NPM packages yet"
+ class="gl-max-w-full"
+ src="helpSvg"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ >
+ <h1
+ class="h4"
+ >
+ There are no NPM packages yet
+ </h1>
+
+ <p>
+ Learn how to
+ <b-link-stub
+ class="gl-link"
+ event="click"
+ href="helpUrl"
+ routertag="a"
+ target="_blank"
+ >
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+
+ <div>
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+ </template>
+ </b-tab-stub>
+ <b-tab-stub
+ tag="div"
+ title="NuGet"
+ titlelinkclass="gl-tab-nav-item"
+ >
+ <template>
+ <div>
+ <section
+ class="row empty-state text-center"
+ >
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt="There are no NuGet packages yet"
+ class="gl-max-w-full"
+ src="helpSvg"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ >
+ <h1
+ class="h4"
+ >
+ There are no NuGet packages yet
+ </h1>
+
+ <p>
+ Learn how to
+ <b-link-stub
+ class="gl-link"
+ event="click"
+ href="helpUrl"
+ routertag="a"
+ target="_blank"
+ >
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+
+ <div>
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+ </template>
+ </b-tab-stub>
+ <b-tab-stub
+ tag="div"
+ title="PyPi"
+ titlelinkclass="gl-tab-nav-item"
+ >
+ <template>
+ <div>
+ <section
+ class="row empty-state text-center"
+ >
+ <div
+ class="col-12"
+ >
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt="There are no PyPi packages yet"
+ class="gl-max-w-full"
+ src="helpSvg"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
+ >
+ <div
+ class="text-content gl-mx-auto gl-my-0 gl-p-5"
+ >
+ <h1
+ class="h4"
+ >
+ There are no PyPi packages yet
+ </h1>
+
+ <p>
+ Learn how to
+ <b-link-stub
+ class="gl-link"
+ event="click"
+ href="helpUrl"
+ routertag="a"
+ target="_blank"
+ >
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+
+ <div>
+ <!---->
+
+ <!---->
+ </div>
+ </div>
+ </div>
+ </section>
+ </div>
+ </template>
+ </b-tab-stub>
+
+ <!---->
+ </template>
+ <template>
+ <div
+ class="d-flex align-self-center ml-md-auto py-1 py-md-0"
+ >
+ <package-filter-stub
+ class="mr-1"
+ />
+
+ <package-sort-stub />
+ </div>
+ </template>
+</b-tabs-stub>
+`;
diff --git a/spec/frontend/packages/list/components/packages_filter_spec.js b/spec/frontend/packages/list/components/packages_filter_spec.js
new file mode 100644
index 00000000000..b186b5f5e48
--- /dev/null
+++ b/spec/frontend/packages/list/components/packages_filter_spec.js
@@ -0,0 +1,50 @@
+import Vuex from 'vuex';
+import { GlSearchBoxByClick } from '@gitlab/ui';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import PackagesFilter from '~/packages/list/components/packages_filter.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('packages_filter', () => {
+ let wrapper;
+ let store;
+
+ const findGlSearchBox = () => wrapper.find(GlSearchBoxByClick);
+
+ const mountComponent = () => {
+ store = new Vuex.Store();
+ store.dispatch = jest.fn();
+
+ wrapper = shallowMount(PackagesFilter, {
+ localVue,
+ store,
+ });
+ };
+
+ beforeEach(mountComponent);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('emits events', () => {
+ it('sets the filter value in the store on input', () => {
+ const searchString = 'foo';
+ findGlSearchBox().vm.$emit('input', searchString);
+
+ expect(store.dispatch).toHaveBeenCalledWith('setFilter', searchString);
+ });
+
+ it('emits the filter event when search box is submitted', () => {
+ findGlSearchBox().vm.$emit('submit');
+
+ expect(wrapper.emitted('filter')).toBeTruthy();
+ });
+ });
+});
diff --git a/spec/frontend/packages/list/components/packages_list_app_spec.js b/spec/frontend/packages/list/components/packages_list_app_spec.js
new file mode 100644
index 00000000000..31bab3886c1
--- /dev/null
+++ b/spec/frontend/packages/list/components/packages_list_app_spec.js
@@ -0,0 +1,148 @@
+import Vuex from 'vuex';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlEmptyState, GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui';
+import PackageListApp from '~/packages/list/components/packages_list_app.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('packages_list_app', () => {
+ let wrapper;
+ let store;
+
+ const PackageList = {
+ name: 'package-list',
+ template: '<div><slot name="empty-state"></slot></div>',
+ };
+ const GlLoadingIcon = { name: 'gl-loading-icon', template: '<div>loading</div>' };
+
+ const emptyListHelpUrl = 'helpUrl';
+ const findEmptyState = () => wrapper.find(GlEmptyState);
+ const findListComponent = () => wrapper.find(PackageList);
+ const findTabComponent = (index = 0) => wrapper.findAll(GlTab).at(index);
+
+ const createStore = (filterQuery = '') => {
+ store = new Vuex.Store({
+ state: {
+ isLoading: false,
+ config: {
+ resourceId: 'project_id',
+ emptyListIllustration: 'helpSvg',
+ emptyListHelpUrl,
+ },
+ filterQuery,
+ },
+ });
+ store.dispatch = jest.fn();
+ };
+
+ const mountComponent = () => {
+ wrapper = shallowMount(PackageListApp, {
+ localVue,
+ store,
+ stubs: {
+ GlEmptyState,
+ GlLoadingIcon,
+ PackageList,
+ GlTab,
+ GlTabs,
+ GlSprintf,
+ GlLink,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createStore();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders', () => {
+ mountComponent();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('empty state', () => {
+ it('generate the correct empty list link', () => {
+ mountComponent();
+
+ const link = findListComponent().find(GlLink);
+
+ expect(link.attributes('href')).toBe(emptyListHelpUrl);
+ expect(link.text()).toBe('publish and share your packages');
+ });
+
+ it('includes the right content on the default tab', () => {
+ mountComponent();
+
+ const heading = findEmptyState().find('h1');
+
+ expect(heading.text()).toBe('There are no packages yet');
+ });
+ });
+
+ it('call requestPackagesList on page:changed', () => {
+ mountComponent();
+
+ const list = findListComponent();
+ list.vm.$emit('page:changed', 1);
+ expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList', { page: 1 });
+ });
+
+ it('call requestDeletePackage on package:delete', () => {
+ mountComponent();
+
+ const list = findListComponent();
+ list.vm.$emit('package:delete', 'foo');
+ expect(store.dispatch).toHaveBeenCalledWith('requestDeletePackage', 'foo');
+ });
+
+ it('calls requestPackagesList on sort:changed', () => {
+ mountComponent();
+
+ const list = findListComponent();
+ list.vm.$emit('sort:changed');
+ expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
+ });
+
+ it('does not call requestPackagesList two times on render', () => {
+ mountComponent();
+
+ expect(store.dispatch).toHaveBeenCalledTimes(1);
+ });
+
+ describe('tab change', () => {
+ it('calls requestPackagesList when all tab is clicked', () => {
+ mountComponent();
+
+ findTabComponent().trigger('click');
+
+ expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
+ });
+
+ it('calls requestPackagesList when a package type tab is clicked', () => {
+ mountComponent();
+
+ findTabComponent(1).trigger('click');
+
+ expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
+ });
+ });
+
+ describe('filter without results', () => {
+ beforeEach(() => {
+ createStore('foo');
+ mountComponent();
+ });
+
+ it('should show specific empty message', () => {
+ expect(findEmptyState().text()).toContain('Sorry, your filter produced no results');
+ expect(findEmptyState().text()).toContain(
+ 'To widen your search, change or remove the filters above',
+ );
+ });
+ });
+});
diff --git a/spec/frontend/packages/list/components/packages_list_spec.js b/spec/frontend/packages/list/components/packages_list_spec.js
new file mode 100644
index 00000000000..a90d5056212
--- /dev/null
+++ b/spec/frontend/packages/list/components/packages_list_spec.js
@@ -0,0 +1,219 @@
+import Vuex from 'vuex';
+import { last } from 'lodash';
+import { GlTable, GlPagination, GlModal } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
+import stubChildren from 'helpers/stub_children';
+import Tracking from '~/tracking';
+import PackagesList from '~/packages/list/components/packages_list.vue';
+import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
+import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
+import * as SharedUtils from '~/packages/shared/utils';
+import { TrackingActions } from '~/packages/shared/constants';
+import { packageList } from '../../mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('packages_list', () => {
+ let wrapper;
+ let store;
+
+ const GlSortingItem = { name: 'sorting-item-stub', template: '<div><slot></slot></div>' };
+ const EmptySlotStub = { name: 'empty-slot-stub', template: '<div>bar</div>' };
+
+ const findPackagesListLoader = () => wrapper.find(PackagesListLoader);
+ const findPackageListPagination = () => wrapper.find(GlPagination);
+ const findPackageListDeleteModal = () => wrapper.find(GlModal);
+ const findEmptySlot = () => wrapper.find({ name: 'empty-slot-stub' });
+ const findPackagesListRow = () => wrapper.find(PackagesListRow);
+
+ const createStore = (isGroupPage, packages, isLoading) => {
+ const state = {
+ isLoading,
+ packages,
+ pagination: {
+ perPage: 1,
+ total: 1,
+ page: 1,
+ },
+ config: {
+ isGroupPage,
+ },
+ sorting: {
+ orderBy: 'version',
+ sort: 'desc',
+ },
+ };
+ store = new Vuex.Store({
+ state,
+ getters: {
+ getList: () => packages,
+ },
+ });
+ store.dispatch = jest.fn();
+ };
+
+ const mountComponent = ({
+ isGroupPage = false,
+ packages = packageList,
+ isLoading = false,
+ ...options
+ } = {}) => {
+ createStore(isGroupPage, packages, isLoading);
+
+ wrapper = mount(PackagesList, {
+ localVue,
+ store,
+ stubs: {
+ ...stubChildren(PackagesList),
+ GlTable,
+ GlSortingItem,
+ GlModal,
+ },
+ ...options,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when is loading', () => {
+ beforeEach(() => {
+ mountComponent({
+ packages: [],
+ isLoading: true,
+ });
+ });
+
+ it('shows skeleton loader when loading', () => {
+ expect(findPackagesListLoader().exists()).toBe(true);
+ });
+ });
+
+ describe('when is not loading', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('does not show skeleton loader when not loading', () => {
+ expect(findPackagesListLoader().exists()).toBe(false);
+ });
+ });
+
+ describe('layout', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('contains a pagination component', () => {
+ const sorting = findPackageListPagination();
+ expect(sorting.exists()).toBe(true);
+ });
+
+ it('contains a modal component', () => {
+ const sorting = findPackageListDeleteModal();
+ expect(sorting.exists()).toBe(true);
+ });
+ });
+
+ describe('when the user can destroy the package', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('setItemToBeDeleted sets itemToBeDeleted and open the modal', () => {
+ const mockModalShow = jest.spyOn(wrapper.vm.$refs.packageListDeleteModal, 'show');
+ const item = last(wrapper.vm.list);
+
+ findPackagesListRow().vm.$emit('packageToDelete', item);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.vm.itemToBeDeleted).toEqual(item);
+ expect(mockModalShow).toHaveBeenCalled();
+ });
+ });
+
+ it('deleteItemConfirmation resets itemToBeDeleted', () => {
+ wrapper.setData({ itemToBeDeleted: 1 });
+ wrapper.vm.deleteItemConfirmation();
+ expect(wrapper.vm.itemToBeDeleted).toEqual(null);
+ });
+
+ it('deleteItemConfirmation emit package:delete', () => {
+ const itemToBeDeleted = { id: 2 };
+ wrapper.setData({ itemToBeDeleted });
+ wrapper.vm.deleteItemConfirmation();
+ return wrapper.vm.$nextTick(() => {
+ expect(wrapper.emitted('package:delete')[0]).toEqual([itemToBeDeleted]);
+ });
+ });
+
+ it('deleteItemCanceled resets itemToBeDeleted', () => {
+ wrapper.setData({ itemToBeDeleted: 1 });
+ wrapper.vm.deleteItemCanceled();
+ expect(wrapper.vm.itemToBeDeleted).toEqual(null);
+ });
+ });
+
+ describe('when the list is empty', () => {
+ beforeEach(() => {
+ mountComponent({
+ packages: [],
+ slots: {
+ 'empty-state': EmptySlotStub,
+ },
+ });
+ });
+
+ it('show the empty slot', () => {
+ const emptySlot = findEmptySlot();
+ expect(emptySlot.exists()).toBe(true);
+ });
+ });
+
+ describe('pagination component', () => {
+ let pagination;
+ let modelEvent;
+
+ beforeEach(() => {
+ mountComponent();
+ pagination = findPackageListPagination();
+ // retrieve the event used by v-model, a more sturdy approach than hardcoding it
+ modelEvent = pagination.vm.$options.model.event;
+ });
+
+ it('emits page:changed events when the page changes', () => {
+ pagination.vm.$emit(modelEvent, 2);
+ expect(wrapper.emitted('page:changed')).toEqual([[2]]);
+ });
+ });
+
+ describe('tracking', () => {
+ let eventSpy;
+ let utilSpy;
+ const category = 'foo';
+
+ beforeEach(() => {
+ mountComponent();
+ eventSpy = jest.spyOn(Tracking, 'event');
+ utilSpy = jest.spyOn(SharedUtils, 'packageTypeToTrackCategory').mockReturnValue(category);
+ wrapper.setData({ itemToBeDeleted: { package_type: 'conan' } });
+ });
+
+ it('tracking category calls packageTypeToTrackCategory', () => {
+ expect(wrapper.vm.tracking.category).toBe(category);
+ expect(utilSpy).toHaveBeenCalledWith('conan');
+ });
+
+ it('deleteItemConfirmation calls event', () => {
+ wrapper.vm.deleteItemConfirmation();
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ TrackingActions.DELETE_PACKAGE,
+ expect.any(Object),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/packages/list/components/packages_sort_spec.js b/spec/frontend/packages/list/components/packages_sort_spec.js
new file mode 100644
index 00000000000..ff3e8e19413
--- /dev/null
+++ b/spec/frontend/packages/list/components/packages_sort_spec.js
@@ -0,0 +1,92 @@
+import Vuex from 'vuex';
+import { GlSorting } from '@gitlab/ui';
+import { mount, createLocalVue } from '@vue/test-utils';
+import stubChildren from 'helpers/stub_children';
+import PackagesSort from '~/packages/list/components/packages_sort.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('packages_sort', () => {
+ let wrapper;
+ let store;
+ let sorting;
+ let sortingItems;
+
+ const GlSortingItem = { name: 'sorting-item-stub', template: '<div><slot></slot></div>' };
+
+ const findPackageListSorting = () => wrapper.find(GlSorting);
+ const findSortingItems = () => wrapper.findAll(GlSortingItem);
+
+ const createStore = isGroupPage => {
+ const state = {
+ config: {
+ isGroupPage,
+ },
+ sorting: {
+ orderBy: 'version',
+ sort: 'desc',
+ },
+ };
+ store = new Vuex.Store({
+ state,
+ });
+ store.dispatch = jest.fn();
+ };
+
+ const mountComponent = (isGroupPage = false) => {
+ createStore(isGroupPage);
+
+ wrapper = mount(PackagesSort, {
+ localVue,
+ store,
+ stubs: {
+ ...stubChildren(PackagesSort),
+ GlSortingItem,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when is in projects', () => {
+ beforeEach(() => {
+ mountComponent();
+ sorting = findPackageListSorting();
+ sortingItems = findSortingItems();
+ });
+
+ it('has all the sortable items', () => {
+ expect(sortingItems).toHaveLength(wrapper.vm.sortableFields.length);
+ });
+
+ it('on sort change set sorting in vuex and emit event', () => {
+ sorting.vm.$emit('sortDirectionChange');
+ expect(store.dispatch).toHaveBeenCalledWith('setSorting', { sort: 'asc' });
+ expect(wrapper.emitted('sort:changed')).toBeTruthy();
+ });
+
+ it('on sort item click set sorting and emit event', () => {
+ const item = sortingItems.at(0);
+ const { orderBy } = wrapper.vm.sortableFields[0];
+ item.vm.$emit('click');
+ expect(store.dispatch).toHaveBeenCalledWith('setSorting', { orderBy });
+ expect(wrapper.emitted('sort:changed')).toBeTruthy();
+ });
+ });
+
+ describe('when is in group', () => {
+ beforeEach(() => {
+ mountComponent(true);
+ sorting = findPackageListSorting();
+ sortingItems = findSortingItems();
+ });
+
+ it('has all the sortable items', () => {
+ expect(sortingItems).toHaveLength(wrapper.vm.sortableFields.length);
+ });
+ });
+});
diff --git a/spec/frontend/packages/list/stores/actions_spec.js b/spec/frontend/packages/list/stores/actions_spec.js
new file mode 100644
index 00000000000..faa629cc01f
--- /dev/null
+++ b/spec/frontend/packages/list/stores/actions_spec.js
@@ -0,0 +1,240 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
+import Api from '~/api';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import * as actions from '~/packages/list/stores/actions';
+import * as types from '~/packages/list/stores/mutation_types';
+import { MISSING_DELETE_PATH_ERROR, DELETE_PACKAGE_ERROR_MESSAGE } from '~/packages/list/constants';
+
+jest.mock('~/flash.js');
+jest.mock('~/api.js');
+
+describe('Actions Package list store', () => {
+ const headers = 'bar';
+ let mock;
+
+ beforeEach(() => {
+ Api.projectPackages = jest.fn().mockResolvedValue({ data: 'foo', headers });
+ Api.groupPackages = jest.fn().mockResolvedValue({ data: 'baz', headers });
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('requestPackagesList', () => {
+ const sorting = {
+ sort: 'asc',
+ orderBy: 'version',
+ };
+ it('should fetch the project packages list when isGroupPage is false', done => {
+ testAction(
+ actions.requestPackagesList,
+ undefined,
+ { config: { isGroupPage: false, resourceId: 1 }, sorting },
+ [],
+ [
+ { type: 'setLoading', payload: true },
+ { type: 'receivePackagesListSuccess', payload: { data: 'foo', headers } },
+ { type: 'setLoading', payload: false },
+ ],
+ () => {
+ expect(Api.projectPackages).toHaveBeenCalledWith(1, {
+ params: { page: 1, per_page: 20, sort: sorting.sort, order_by: sorting.orderBy },
+ });
+ done();
+ },
+ );
+ });
+
+ it('should fetch the group packages list when isGroupPage is true', done => {
+ testAction(
+ actions.requestPackagesList,
+ undefined,
+ { config: { isGroupPage: true, resourceId: 2 }, sorting },
+ [],
+ [
+ { type: 'setLoading', payload: true },
+ { type: 'receivePackagesListSuccess', payload: { data: 'baz', headers } },
+ { type: 'setLoading', payload: false },
+ ],
+ () => {
+ expect(Api.groupPackages).toHaveBeenCalledWith(2, {
+ params: { page: 1, per_page: 20, sort: sorting.sort, order_by: sorting.orderBy },
+ });
+ done();
+ },
+ );
+ });
+
+ it('should fetch packages of a certain type when selectedType is present', done => {
+ const packageType = 'maven';
+
+ testAction(
+ actions.requestPackagesList,
+ undefined,
+ {
+ config: { isGroupPage: false, resourceId: 1 },
+ sorting,
+ selectedType: { type: packageType },
+ },
+ [],
+ [
+ { type: 'setLoading', payload: true },
+ { type: 'receivePackagesListSuccess', payload: { data: 'foo', headers } },
+ { type: 'setLoading', payload: false },
+ ],
+ () => {
+ expect(Api.projectPackages).toHaveBeenCalledWith(1, {
+ params: {
+ page: 1,
+ per_page: 20,
+ sort: sorting.sort,
+ order_by: sorting.orderBy,
+ package_type: packageType,
+ },
+ });
+ done();
+ },
+ );
+ });
+
+ it('should create flash on API error', done => {
+ Api.projectPackages = jest.fn().mockRejectedValue();
+ testAction(
+ actions.requestPackagesList,
+ undefined,
+ { config: { isGroupPage: false, resourceId: 2 }, sorting },
+ [],
+ [{ type: 'setLoading', payload: true }, { type: 'setLoading', payload: false }],
+ () => {
+ expect(createFlash).toHaveBeenCalled();
+ done();
+ },
+ );
+ });
+ });
+
+ describe('receivePackagesListSuccess', () => {
+ it('should set received packages', done => {
+ const data = 'foo';
+
+ testAction(
+ actions.receivePackagesListSuccess,
+ { data, headers },
+ null,
+ [
+ { type: types.SET_PACKAGE_LIST_SUCCESS, payload: data },
+ { type: types.SET_PAGINATION, payload: headers },
+ ],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setInitialState', () => {
+ it('should commit setInitialState', done => {
+ testAction(
+ actions.setInitialState,
+ '1',
+ null,
+ [{ type: types.SET_INITIAL_STATE, payload: '1' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setLoading', () => {
+ it('should commit set main loading', done => {
+ testAction(
+ actions.setLoading,
+ true,
+ null,
+ [{ type: types.SET_MAIN_LOADING, payload: true }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('requestDeletePackage', () => {
+ const payload = {
+ _links: {
+ delete_api_path: 'foo',
+ },
+ };
+ it('should perform a delete operation on _links.delete_api_path', done => {
+ mock.onDelete(payload._links.delete_api_path).replyOnce(200);
+ Api.projectPackages = jest.fn().mockResolvedValue({ data: 'foo' });
+
+ testAction(
+ actions.requestDeletePackage,
+ payload,
+ { pagination: { page: 1 } },
+ [],
+ [
+ { type: 'setLoading', payload: true },
+ { type: 'requestPackagesList', payload: { page: 1 } },
+ ],
+ done,
+ );
+ });
+
+ it('should stop the loading and call create flash on api error', done => {
+ mock.onDelete(payload._links.delete_api_path).replyOnce(400);
+ testAction(
+ actions.requestDeletePackage,
+ payload,
+ null,
+ [],
+ [{ type: 'setLoading', payload: true }, { type: 'setLoading', payload: false }],
+ () => {
+ expect(createFlash).toHaveBeenCalled();
+ done();
+ },
+ );
+ });
+
+ it.each`
+ property | actionPayload
+ ${'_links'} | ${{}}
+ ${'delete_api_path'} | ${{ _links: {} }}
+ `('should reject and createFlash when $property is missing', ({ actionPayload }, done) => {
+ testAction(actions.requestDeletePackage, actionPayload, null, [], []).catch(e => {
+ expect(e).toEqual(new Error(MISSING_DELETE_PATH_ERROR));
+ expect(createFlash).toHaveBeenCalledWith(DELETE_PACKAGE_ERROR_MESSAGE);
+ done();
+ });
+ });
+ });
+
+ describe('setSorting', () => {
+ it('should commit SET_SORTING', done => {
+ testAction(
+ actions.setSorting,
+ 'foo',
+ null,
+ [{ type: types.SET_SORTING, payload: 'foo' }],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('setFilter', () => {
+ it('should commit SET_FILTER', done => {
+ testAction(
+ actions.setFilter,
+ 'foo',
+ null,
+ [{ type: types.SET_FILTER, payload: 'foo' }],
+ [],
+ done,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/packages/list/stores/getters_spec.js b/spec/frontend/packages/list/stores/getters_spec.js
new file mode 100644
index 00000000000..080bbc21d9f
--- /dev/null
+++ b/spec/frontend/packages/list/stores/getters_spec.js
@@ -0,0 +1,36 @@
+import getList from '~/packages/list/stores/getters';
+import { packageList } from '../../mock_data';
+
+describe('Getters registry list store', () => {
+ let state;
+
+ const setState = ({ isGroupPage = false } = {}) => {
+ state = {
+ packages: packageList,
+ config: {
+ isGroupPage,
+ },
+ };
+ };
+
+ beforeEach(() => setState());
+
+ afterEach(() => {
+ state = null;
+ });
+
+ describe('getList', () => {
+ it('returns a list of packages', () => {
+ const result = getList(state);
+
+ expect(result).toHaveLength(packageList.length);
+ expect(result[0].name).toBe('Test package');
+ });
+
+ it('adds projectPathName', () => {
+ const result = getList(state);
+
+ expect(result[0].projectPathName).toMatchInlineSnapshot(`"foo / bar / baz"`);
+ });
+ });
+});
diff --git a/spec/frontend/packages/list/stores/mutations_spec.js b/spec/frontend/packages/list/stores/mutations_spec.js
new file mode 100644
index 00000000000..563a3dabbb3
--- /dev/null
+++ b/spec/frontend/packages/list/stores/mutations_spec.js
@@ -0,0 +1,95 @@
+import mutations from '~/packages/list/stores/mutations';
+import * as types from '~/packages/list/stores/mutation_types';
+import createState from '~/packages/list/stores/state';
+import * as commonUtils from '~/lib/utils/common_utils';
+import { npmPackage, mavenPackage } from '../../mock_data';
+
+describe('Mutations Registry Store', () => {
+ let mockState;
+ beforeEach(() => {
+ mockState = createState();
+ });
+
+ describe('SET_INITIAL_STATE', () => {
+ it('should set the initial state', () => {
+ const config = {
+ resourceId: '1',
+ pageType: 'groups',
+ userCanDelete: '',
+ emptyListIllustration: 'foo',
+ emptyListHelpUrl: 'baz',
+ comingSoonJson: '{ "project_path": "gitlab-org/gitlab-test" }',
+ };
+
+ const expectedState = {
+ ...mockState,
+ config: {
+ ...config,
+ isGroupPage: true,
+ canDestroyPackage: true,
+ },
+ };
+ mutations[types.SET_INITIAL_STATE](mockState, config);
+
+ expect(mockState.projectId).toEqual(expectedState.projectId);
+ });
+ });
+
+ describe('SET_PACKAGE_LIST_SUCCESS', () => {
+ it('should set a packages list', () => {
+ const payload = [npmPackage, mavenPackage];
+ const expectedState = { ...mockState, packages: payload };
+ mutations[types.SET_PACKAGE_LIST_SUCCESS](mockState, payload);
+
+ expect(mockState.packages).toEqual(expectedState.packages);
+ });
+ });
+
+ describe('SET_MAIN_LOADING', () => {
+ it('should set main loading', () => {
+ mutations[types.SET_MAIN_LOADING](mockState, true);
+
+ expect(mockState.isLoading).toEqual(true);
+ });
+ });
+
+ describe('SET_PAGINATION', () => {
+ const mockPagination = { perPage: 10, page: 1 };
+ beforeEach(() => {
+ commonUtils.normalizeHeaders = jest.fn().mockReturnValue('baz');
+ commonUtils.parseIntPagination = jest.fn().mockReturnValue(mockPagination);
+ });
+ it('should set a parsed pagination', () => {
+ mutations[types.SET_PAGINATION](mockState, 'foo');
+ expect(commonUtils.normalizeHeaders).toHaveBeenCalledWith('foo');
+ expect(commonUtils.parseIntPagination).toHaveBeenCalledWith('baz');
+ expect(mockState.pagination).toEqual(mockPagination);
+ });
+ });
+
+ describe('SET_SORTING', () => {
+ it('should merge the sorting object with sort value', () => {
+ mutations[types.SET_SORTING](mockState, { sort: 'desc' });
+ expect(mockState.sorting).toEqual({ ...mockState.sorting, sort: 'desc' });
+ });
+
+ it('should merge the sorting object with order_by value', () => {
+ mutations[types.SET_SORTING](mockState, { orderBy: 'foo' });
+ expect(mockState.sorting).toEqual({ ...mockState.sorting, orderBy: 'foo' });
+ });
+ });
+
+ describe('SET_SELECTED_TYPE', () => {
+ it('should set the selected type', () => {
+ mutations[types.SET_SELECTED_TYPE](mockState, { type: 'maven' });
+ expect(mockState.selectedType).toEqual({ type: 'maven' });
+ });
+ });
+
+ describe('SET_FILTER', () => {
+ it('should set the filter query', () => {
+ mutations[types.SET_FILTER](mockState, 'foo');
+ expect(mockState.filterQuery).toEqual('foo');
+ });
+ });
+});
diff --git a/spec/frontend/packages/list/utils_spec.js b/spec/frontend/packages/list/utils_spec.js
new file mode 100644
index 00000000000..5bcc3784752
--- /dev/null
+++ b/spec/frontend/packages/list/utils_spec.js
@@ -0,0 +1,39 @@
+import { getNewPaginationPage } from '~/packages/list/utils';
+
+describe('Packages list utils', () => {
+ describe('packageTypeDisplay', () => {
+ it('returns the current page when total items exceeds pagniation', () => {
+ expect(getNewPaginationPage(2, 20, 21)).toBe(2);
+ });
+
+ it('returns the previous page when total items is lower than or equal to pagination', () => {
+ expect(getNewPaginationPage(2, 20, 20)).toBe(1);
+ });
+
+ it('returns the first page when totalItems is lower than or equal to perPage', () => {
+ expect(getNewPaginationPage(4, 20, 20)).toBe(1);
+ });
+
+ describe('works when a different perPage is used', () => {
+ it('returns the current page', () => {
+ expect(getNewPaginationPage(2, 10, 11)).toBe(2);
+ });
+
+ it('returns the previous page', () => {
+ expect(getNewPaginationPage(2, 10, 10)).toBe(1);
+ });
+ });
+
+ describe.each`
+ currentPage | totalItems | expectedResult
+ ${1} | ${20} | ${1}
+ ${2} | ${20} | ${1}
+ ${3} | ${40} | ${2}
+ ${4} | ${60} | ${3}
+ `(`works across numerious pages`, ({ currentPage, totalItems, expectedResult }) => {
+ it(`when currentPage is ${currentPage} return to the previous page ${expectedResult}`, () => {
+ expect(getNewPaginationPage(currentPage, 20, totalItems)).toBe(expectedResult);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/mock_data.js b/spec/frontend/packages/mock_data.js
new file mode 100644
index 00000000000..86205b0744c
--- /dev/null
+++ b/spec/frontend/packages/mock_data.js
@@ -0,0 +1,170 @@
+const _links = {
+ web_path: 'foo',
+ delete_api_path: 'bar',
+};
+
+export const mockPipelineInfo = {
+ id: 1,
+ ref: 'branch-name',
+ sha: 'sha-baz',
+ user: {
+ name: 'foo',
+ },
+ project: {
+ name: 'foo-project',
+ web_url: 'foo-project-link',
+ commit_url: 'foo-commit-link',
+ pipeline_url: 'foo-pipeline-link',
+ },
+ created_at: '2015-12-10',
+};
+
+export const mavenPackage = {
+ created_at: '2015-12-10',
+ id: 1,
+ maven_metadatum: {
+ app_group: 'com.test.app',
+ app_name: 'test-app',
+ app_version: '1.0-SNAPSHOT',
+ },
+ name: 'Test package',
+ package_type: 'maven',
+ project_path: 'foo/bar/baz',
+ project_id: 1,
+ updated_at: '2015-12-10',
+ version: '1.0.0',
+ _links,
+};
+
+export const mavenFiles = [
+ {
+ created_at: '2015-12-10',
+ file_name: 'File one',
+ id: 1,
+ size: 100,
+ download_path: '/-/package_files/1/download',
+ },
+ {
+ created_at: '2015-12-10',
+ file_name: 'File two',
+ id: 2,
+ size: 200,
+ download_path: '/-/package_files/2/download',
+ },
+];
+
+export const npmPackage = {
+ created_at: '2015-12-10',
+ id: 2,
+ name: '@Test/package',
+ package_type: 'npm',
+ project_path: 'foo/bar/baz',
+ project_id: 1,
+ updated_at: '2015-12-10',
+ version: '',
+ versions: [],
+ _links,
+ pipeline: mockPipelineInfo,
+};
+
+export const npmFiles = [
+ {
+ created_at: '2015-12-10',
+ file_name: '@test/test-package-1.0.0.tgz',
+ id: 2,
+ size: 200,
+ download_path: '/-/package_files/2/download',
+ },
+];
+
+export const conanPackage = {
+ conan_metadatum: {
+ package_channel: 'stable',
+ package_username: 'conan+conan-package',
+ },
+ created_at: '2015-12-10',
+ id: 3,
+ name: 'conan-package',
+ project_path: 'foo/bar/baz',
+ package_files: [],
+ package_type: 'conan',
+ project_id: 1,
+ recipe: 'conan-package/1.0.0@conan+conan-package/stable',
+ updated_at: '2015-12-10',
+ version: '1.0.0',
+ _links,
+};
+
+export const dependencyLinks = {
+ withoutFramework: { name: 'Moqi', version_pattern: '2.5.6' },
+ withoutVersion: { name: 'Castle.Core', version_pattern: '' },
+ fullLink: {
+ name: 'Test.Dependency',
+ version_pattern: '2.3.7',
+ target_framework: '.NETStandard2.0',
+ },
+ anotherFullLink: {
+ name: 'Newtonsoft.Json',
+ version_pattern: '12.0.3',
+ target_framework: '.NETStandard2.0',
+ },
+};
+
+export const nugetPackage = {
+ created_at: '2015-12-10',
+ id: 4,
+ name: 'NugetPackage1',
+ package_files: [],
+ package_type: 'nuget',
+ project_id: 1,
+ tags: [],
+ updated_at: '2015-12-10',
+ version: '1.0.0',
+ dependency_links: Object.values(dependencyLinks),
+ nuget_metadatum: {
+ icon_url: 'fake-icon',
+ project_url: 'project-foo-url',
+ license_url: 'license-foo-url',
+ },
+};
+
+export const pypiPackage = {
+ created_at: '2015-12-10',
+ id: 5,
+ name: 'PyPiPackage',
+ package_files: [],
+ package_type: 'pypi',
+ project_id: 1,
+ tags: [],
+ updated_at: '2015-12-10',
+ version: '1.0.0',
+};
+
+export const composerPackage = {
+ created_at: '2015-12-10',
+ id: 5,
+ name: 'ComposerPackage',
+ package_files: [],
+ package_type: 'composer',
+ project_id: 1,
+ tags: [],
+ updated_at: '2015-12-10',
+ version: '1.0.0',
+};
+
+export const mockTags = [
+ {
+ name: 'foo-1',
+ },
+ {
+ name: 'foo-2',
+ },
+ {
+ name: 'foo-3',
+ },
+ {
+ name: 'foo-4',
+ },
+];
+
+export const packageList = [mavenPackage, { ...npmPackage, tags: mockTags }, conanPackage];
diff --git a/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
new file mode 100644
index 00000000000..eab8d7b67cc
--- /dev/null
+++ b/spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap
@@ -0,0 +1,101 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`packages_list_row renders 1`] = `
+<div
+ class="gl-responsive-table-row"
+ data-qa-selector="packages-row"
+>
+ <div
+ class="table-section section-50 d-flex flex-md-column justify-content-between flex-wrap"
+ >
+ <div
+ class="d-flex align-items-center mr-2"
+ >
+ <gl-link-stub
+ class="text-dark font-weight-bold mb-md-1"
+ data-qa-selector="package_link"
+ href="foo"
+ >
+
+ Test package
+
+ </gl-link-stub>
+
+ <!---->
+ </div>
+
+ <div
+ class="d-flex text-secondary text-truncate mt-md-2"
+ >
+ <span>
+ 1.0.0
+ </span>
+
+ <!---->
+
+ <div
+ class="d-flex align-items-center"
+ >
+ <gl-icon-stub
+ class="text-secondary ml-2 mr-1"
+ name="review-list"
+ size="16"
+ />
+
+ <gl-link-stub
+ class="text-secondary"
+ data-testid="packages-row-project"
+ href="/foo/bar/baz"
+ >
+
+ </gl-link-stub>
+ </div>
+
+ <div
+ class="d-flex align-items-center"
+ data-testid="package-type"
+ >
+ <gl-icon-stub
+ class="text-secondary ml-2 mr-1"
+ name="package"
+ size="16"
+ />
+
+ <span>
+ Maven
+ </span>
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="table-section d-flex flex-md-column justify-content-between align-items-md-end section-40"
+ >
+ <publish-method-stub
+ packageentity="[object Object]"
+ />
+
+ <div
+ class="text-secondary order-0 order-md-1 mt-md-2"
+ >
+ <gl-sprintf-stub
+ message="Created %{timestamp}"
+ />
+ </div>
+ </div>
+
+ <div
+ class="table-section section-10 d-flex justify-content-end"
+ >
+ <gl-button-stub
+ aria-label="Remove package"
+ category="primary"
+ data-testid="action-delete"
+ icon="remove"
+ size="medium"
+ title="Remove package"
+ variant="danger"
+ />
+ </div>
+</div>
+`;
diff --git a/spec/frontend/packages/shared/components/__snapshots__/publish_method_spec.js.snap b/spec/frontend/packages/shared/components/__snapshots__/publish_method_spec.js.snap
new file mode 100644
index 00000000000..5ecca63d41d
--- /dev/null
+++ b/spec/frontend/packages/shared/components/__snapshots__/publish_method_spec.js.snap
@@ -0,0 +1,39 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`publish_method renders 1`] = `
+<div
+ class="d-flex align-items-center text-secondary order-1 order-md-0 mb-md-1"
+>
+ <gl-icon-stub
+ class="mr-1"
+ name="git-merge"
+ size="16"
+ />
+
+ <strong
+ class="mr-1 text-dark"
+ >
+ branch-name
+ </strong>
+
+ <gl-icon-stub
+ class="mr-1"
+ name="commit"
+ size="16"
+ />
+
+ <gl-link-stub
+ class="mr-1"
+ href="../commit/sha-baz"
+ >
+ sha-baz
+ </gl-link-stub>
+
+ <clipboard-button-stub
+ cssclass="border-0 text-secondary py-0 px-1"
+ text="sha-baz"
+ title="Copy commit SHA"
+ tooltipplacement="top"
+ />
+</div>
+`;
diff --git a/spec/frontend/packages/shared/components/package_list_row_spec.js b/spec/frontend/packages/shared/components/package_list_row_spec.js
new file mode 100644
index 00000000000..c0ae972d519
--- /dev/null
+++ b/spec/frontend/packages/shared/components/package_list_row_spec.js
@@ -0,0 +1,106 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import PackagesListRow from '~/packages/shared/components/package_list_row.vue';
+import PackageTags from '~/packages/shared/components/package_tags.vue';
+import { packageList } from '../../mock_data';
+
+describe('packages_list_row', () => {
+ let wrapper;
+ let store;
+
+ const [packageWithoutTags, packageWithTags] = packageList;
+
+ const findPackageTags = () => wrapper.find(PackageTags);
+ const findProjectLink = () => wrapper.find('[data-testid="packages-row-project"]');
+ const findDeleteButton = () => wrapper.find('[data-testid="action-delete"]');
+ const findPackageType = () => wrapper.find('[data-testid="package-type"]');
+
+ const mountComponent = ({
+ isGroup = false,
+ packageEntity = packageWithoutTags,
+ shallow = true,
+ showPackageType = true,
+ disableDelete = false,
+ } = {}) => {
+ const mountFunc = shallow ? shallowMount : mount;
+
+ wrapper = mountFunc(PackagesListRow, {
+ store,
+ propsData: {
+ packageLink: 'foo',
+ packageEntity,
+ isGroup,
+ showPackageType,
+ disableDelete,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders', () => {
+ mountComponent();
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('tags', () => {
+ it('renders package tags when a package has tags', () => {
+ mountComponent({ isGroup: false, packageEntity: packageWithTags });
+
+ expect(findPackageTags().exists()).toBe(true);
+ });
+
+ it('does not render when there are no tags', () => {
+ mountComponent();
+
+ expect(findPackageTags().exists()).toBe(false);
+ });
+ });
+
+ describe('when is is group', () => {
+ beforeEach(() => {
+ mountComponent({ isGroup: true });
+ });
+
+ it('has project field', () => {
+ expect(findProjectLink().exists()).toBe(true);
+ });
+ });
+
+ describe('showPackageType', () => {
+ it('shows the type when set', () => {
+ mountComponent();
+
+ expect(findPackageType().exists()).toBe(true);
+ });
+
+ it('does not show the type when not set', () => {
+ mountComponent({ showPackageType: false });
+
+ expect(findPackageType().exists()).toBe(false);
+ });
+ });
+
+ describe('deleteAvailable', () => {
+ it('does not show when not set', () => {
+ mountComponent({ disableDelete: true });
+
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+ });
+
+ describe('delete event', () => {
+ beforeEach(() => mountComponent({ packageEntity: packageWithoutTags, shallow: false }));
+
+ it('emits the packageToDelete event when the delete button is clicked', () => {
+ findDeleteButton().trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.emitted('packageToDelete')).toBeTruthy();
+ expect(wrapper.emitted('packageToDelete')[0]).toEqual([packageWithoutTags]);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/packages/shared/components/package_tags_spec.js b/spec/frontend/packages/shared/components/package_tags_spec.js
new file mode 100644
index 00000000000..cc49a9a9244
--- /dev/null
+++ b/spec/frontend/packages/shared/components/package_tags_spec.js
@@ -0,0 +1,115 @@
+import { mount } from '@vue/test-utils';
+import PackageTags from '~/packages/shared/components/package_tags.vue';
+import { mockTags } from '../../mock_data';
+
+describe('PackageTags', () => {
+ let wrapper;
+
+ function createComponent(tags = [], props = {}) {
+ const propsData = {
+ tags,
+ ...props,
+ };
+
+ wrapper = mount(PackageTags, {
+ propsData,
+ });
+ }
+
+ const tagLabel = () => wrapper.find('[data-testid="tagLabel"]');
+ const tagBadges = () => wrapper.findAll('[data-testid="tagBadge"]');
+ const moreBadge = () => wrapper.find('[data-testid="moreBadge"]');
+
+ afterEach(() => {
+ if (wrapper) wrapper.destroy();
+ });
+
+ describe('tag label', () => {
+ it('shows the tag label by default', () => {
+ createComponent();
+
+ expect(tagLabel().exists()).toBe(true);
+ });
+
+ it('hides when hideLabel prop is set to true', () => {
+ createComponent(mockTags, { hideLabel: true });
+
+ expect(tagLabel().exists()).toBe(false);
+ });
+ });
+
+ it('renders the correct number of tags', () => {
+ createComponent(mockTags.slice(0, 2));
+
+ expect(tagBadges()).toHaveLength(2);
+ expect(moreBadge().exists()).toBe(false);
+ });
+
+ it('does not render more than the configured tagDisplayLimit', () => {
+ createComponent(mockTags);
+
+ expect(tagBadges()).toHaveLength(2);
+ });
+
+ it('renders the more tags badge if there are more than the configured limit', () => {
+ createComponent(mockTags);
+
+ expect(tagBadges()).toHaveLength(2);
+ expect(moreBadge().exists()).toBe(true);
+ expect(moreBadge().text()).toContain('2');
+ });
+
+ it('renders the configured tagDisplayLimit when set in props', () => {
+ createComponent(mockTags, { tagDisplayLimit: 1 });
+
+ expect(tagBadges()).toHaveLength(1);
+ expect(moreBadge().exists()).toBe(true);
+ expect(moreBadge().text()).toContain('3');
+ });
+
+ describe('tagBadgeStyle', () => {
+ const defaultStyle = ['badge', 'badge-info', 'gl-display-none'];
+
+ it('shows tag badge when there is only one', () => {
+ createComponent([mockTags[0]]);
+
+ const expectedStyle = [...defaultStyle, 'gl-display-flex', 'gl-ml-3'];
+
+ expect(
+ tagBadges()
+ .at(0)
+ .classes(),
+ ).toEqual(expect.arrayContaining(expectedStyle));
+ });
+
+ it('shows tag badge for medium or heigher resolutions', () => {
+ createComponent(mockTags);
+
+ const expectedStyle = [...defaultStyle, 'd-md-flex'];
+
+ expect(
+ tagBadges()
+ .at(1)
+ .classes(),
+ ).toEqual(expect.arrayContaining(expectedStyle));
+ });
+
+ it('correctly prepends left and appends right when there is more than one tag', () => {
+ createComponent(mockTags, {
+ tagDisplayLimit: 4,
+ });
+
+ const expectedStyleWithoutAppend = [...defaultStyle, 'd-md-flex'];
+ const expectedStyleWithAppend = [...expectedStyleWithoutAppend, 'gl-mr-2'];
+
+ const allBadges = tagBadges();
+
+ expect(allBadges.at(0).classes()).toEqual(
+ expect.arrayContaining([...expectedStyleWithAppend, 'gl-ml-3']),
+ );
+ expect(allBadges.at(1).classes()).toEqual(expect.arrayContaining(expectedStyleWithAppend));
+ expect(allBadges.at(2).classes()).toEqual(expect.arrayContaining(expectedStyleWithAppend));
+ expect(allBadges.at(3).classes()).toEqual(expect.arrayContaining(expectedStyleWithoutAppend));
+ });
+ });
+});
diff --git a/spec/frontend/packages/shared/components/packages_list_loader_spec.js b/spec/frontend/packages/shared/components/packages_list_loader_spec.js
new file mode 100644
index 00000000000..c8c2e2a4ba4
--- /dev/null
+++ b/spec/frontend/packages/shared/components/packages_list_loader_spec.js
@@ -0,0 +1,42 @@
+import { mount } from '@vue/test-utils';
+import PackagesListLoader from '~/packages/shared/components/packages_list_loader.vue';
+
+describe('PackagesListLoader', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(PackagesListLoader, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const getShapes = () => wrapper.vm.desktopShapes;
+ const findSquareButton = () => wrapper.find({ ref: 'button-loader' });
+
+ beforeEach(createComponent);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when used for projects', () => {
+ it('should return 5 rects with last one being a square', () => {
+ expect(getShapes()).toHaveLength(5);
+ expect(findSquareButton().exists()).toBe(true);
+ });
+ });
+
+ describe('when used for groups', () => {
+ beforeEach(() => {
+ createComponent({ isGroup: true });
+ });
+
+ it('should return 5 rects with no square', () => {
+ expect(getShapes()).toHaveLength(5);
+ expect(findSquareButton().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/packages/shared/components/publish_method_spec.js b/spec/frontend/packages/shared/components/publish_method_spec.js
new file mode 100644
index 00000000000..bb9287c1204
--- /dev/null
+++ b/spec/frontend/packages/shared/components/publish_method_spec.js
@@ -0,0 +1,50 @@
+import { shallowMount } from '@vue/test-utils';
+import PublishMethod from '~/packages/shared/components/publish_method.vue';
+import { packageList } from '../../mock_data';
+
+describe('publish_method', () => {
+ let wrapper;
+
+ const [packageWithoutPipeline, packageWithPipeline] = packageList;
+
+ const findPipelineRef = () => wrapper.find({ ref: 'pipeline-ref' });
+ const findPipelineSha = () => wrapper.find({ ref: 'pipeline-sha' });
+ const findManualPublish = () => wrapper.find({ ref: 'manual-ref' });
+
+ const mountComponent = (packageEntity = {}, isGroup = false) => {
+ wrapper = shallowMount(PublishMethod, {
+ propsData: {
+ packageEntity,
+ isGroup,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders', () => {
+ mountComponent(packageWithPipeline);
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ describe('pipeline information', () => {
+ it('displays branch and commit when pipeline info exists', () => {
+ mountComponent(packageWithPipeline);
+
+ expect(findPipelineRef().exists()).toBe(true);
+ expect(findPipelineSha().exists()).toBe(true);
+ });
+
+ it('does not show any pipeline details when no information exists', () => {
+ mountComponent(packageWithoutPipeline);
+
+ expect(findPipelineRef().exists()).toBe(false);
+ expect(findPipelineSha().exists()).toBe(false);
+ expect(findManualPublish().exists()).toBe(true);
+ expect(findManualPublish().text()).toBe('Manually Published');
+ });
+ });
+});
diff --git a/spec/frontend/packages/shared/utils_spec.js b/spec/frontend/packages/shared/utils_spec.js
new file mode 100644
index 00000000000..1fe90a4827f
--- /dev/null
+++ b/spec/frontend/packages/shared/utils_spec.js
@@ -0,0 +1,66 @@
+import {
+ packageTypeToTrackCategory,
+ beautifyPath,
+ getPackageTypeLabel,
+ getCommitLink,
+} from '~/packages/shared/utils';
+import { PackageType, TrackingCategories } from '~/packages/shared/constants';
+import { packageList } from '../mock_data';
+
+describe('Packages shared utils', () => {
+ describe('packageTypeToTrackCategory', () => {
+ it('prepend UI to package category', () => {
+ expect(packageTypeToTrackCategory()).toMatchInlineSnapshot(`"UI::undefined"`);
+ });
+
+ it.each(Object.keys(PackageType))('returns a correct category string for %s', packageKey => {
+ const packageName = PackageType[packageKey];
+ expect(packageTypeToTrackCategory(packageName)).toBe(
+ `UI::${TrackingCategories[packageName]}`,
+ );
+ });
+ });
+
+ describe('beautifyPath', () => {
+ it('returns a string with spaces around /', () => {
+ expect(beautifyPath('foo/bar')).toBe('foo / bar');
+ });
+ it('does not fail for empty string', () => {
+ expect(beautifyPath()).toBe('');
+ });
+ });
+
+ describe('getPackageTypeLabel', () => {
+ describe.each`
+ packageType | expectedResult
+ ${'conan'} | ${'Conan'}
+ ${'maven'} | ${'Maven'}
+ ${'npm'} | ${'NPM'}
+ ${'nuget'} | ${'NuGet'}
+ ${'pypi'} | ${'PyPi'}
+ ${'composer'} | ${'Composer'}
+ ${'foo'} | ${null}
+ `(`package type`, ({ packageType, expectedResult }) => {
+ it(`${packageType} should show as ${expectedResult}`, () => {
+ expect(getPackageTypeLabel(packageType)).toBe(expectedResult);
+ });
+ });
+ });
+
+ describe('getCommitLink', () => {
+ it('returns a relative link when isGroup is false', () => {
+ const link = getCommitLink(packageList[0], false);
+
+ expect(link).toContain('../commit');
+ });
+
+ describe('when isGroup is true', () => {
+ it('returns an absolute link matching project path', () => {
+ const mavenPackage = packageList[0];
+ const link = getCommitLink(mavenPackage, true);
+
+ expect(link).toContain(`/${mavenPackage.project_path}/commit`);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pager_spec.js b/spec/frontend/pager_spec.js
index 47056c2804c..8b60f872bfd 100644
--- a/spec/frontend/pager_spec.js
+++ b/spec/frontend/pager_spec.js
@@ -1,9 +1,9 @@
import $ from 'jquery';
import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'jest/helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import Pager from '~/pager';
import { removeParams } from '~/lib/utils/url_utility';
-import { TEST_HOST } from 'jest/helpers/test_constants';
jest.mock('~/lib/utils/url_utility', () => ({
removeParams: jest.fn().mockName('removeParams'),
diff --git a/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
index fb7a07b7bc7..c662fb7ba4a 100644
--- a/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
+++ b/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
-import { redirectTo } from '~/lib/utils/url_utility';
import mountComponent from 'helpers/vue_mount_component_helper';
+import { TEST_HOST } from 'jest/helpers/test_constants';
+import { redirectTo } from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
-import { TEST_HOST } from 'jest/helpers/test_constants';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
diff --git a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap
index 82589e5147c..fc37a545511 100644
--- a/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap
+++ b/spec/frontend/pages/admin/users/components/__snapshots__/delete_user_modal_spec.js.snap
@@ -37,29 +37,35 @@ exports[`User Operation confirmation modal renders modal with form included 1`]
value=""
/>
</form>
- <gl-deprecated-button-stub
- size="md"
- variant="secondary"
+ <gl-button-stub
+ category="primary"
+ icon=""
+ size="medium"
+ variant="default"
>
Cancel
- </gl-deprecated-button-stub>
+ </gl-button-stub>
- <gl-deprecated-button-stub
+ <gl-button-stub
+ category="primary"
disabled="true"
- size="md"
+ icon=""
+ size="medium"
variant="warning"
>
secondaryAction
- </gl-deprecated-button-stub>
+ </gl-button-stub>
- <gl-deprecated-button-stub
+ <gl-button-stub
+ category="primary"
disabled="true"
- size="md"
+ icon=""
+ size="medium"
variant="danger"
>
action
- </gl-deprecated-button-stub>
+ </gl-button-stub>
</div>
`;
diff --git a/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js b/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js
index 16b0bd305cd..3efefa8137f 100644
--- a/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js
+++ b/spec/frontend/pages/admin/users/components/delete_user_modal_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDeprecatedButton, GlFormInput } from '@gitlab/ui';
+import { GlButton, GlFormInput } from '@gitlab/ui';
import DeleteUserModal from '~/pages/admin/users/components/delete_user_modal.vue';
import ModalStub from './stubs/modal_stub';
@@ -13,7 +13,7 @@ describe('User Operation confirmation modal', () => {
const findButton = variant =>
wrapper
- .findAll(GlDeprecatedButton)
+ .findAll(GlButton)
.filter(w => w.attributes('variant') === variant)
.at(0);
const findForm = () => wrapper.find('form');
diff --git a/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js b/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js
new file mode 100644
index 00000000000..b3a297ac2c5
--- /dev/null
+++ b/spec/frontend/pages/dashboard/projects/index/components/customize_homepage_banner_spec.js
@@ -0,0 +1,50 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlBanner } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import CustomizeHomepageBanner from '~/pages/dashboard/projects/index/components/customize_homepage_banner.vue';
+import axios from '~/lib/utils/axios_utils';
+
+const svgPath = '/illustrations/background';
+const provide = {
+ svgPath,
+ preferencesBehaviorPath: 'some/behavior/path',
+ calloutsPath: 'call/out/path',
+ calloutsFeatureId: 'some-feature-id',
+};
+
+const createComponent = () => {
+ return shallowMount(CustomizeHomepageBanner, { provide });
+};
+
+describe('CustomizeHomepageBanner', () => {
+ let mockAxios;
+ let wrapper;
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ mockAxios.restore();
+ });
+
+ it('should render the banner when not dismissed', () => {
+ expect(wrapper.contains(GlBanner)).toBe(true);
+ });
+
+ it('should close the banner when dismiss is clicked', async () => {
+ mockAxios.onPost(provide.calloutsPath).replyOnce(200);
+ expect(wrapper.contains(GlBanner)).toBe(true);
+ wrapper.find(GlBanner).vm.$emit('close');
+
+ await wrapper.vm.$nextTick();
+ expect(wrapper.contains(GlBanner)).toBe(false);
+ });
+
+ it('includes the body text from options', () => {
+ expect(wrapper.html()).toContain(wrapper.vm.$options.i18n.body);
+ });
+});
diff --git a/spec/frontend/pages/labels/components/promote_label_modal_spec.js b/spec/frontend/pages/labels/components/promote_label_modal_spec.js
index d4aabcc02f4..1fa12cf1365 100644
--- a/spec/frontend/pages/labels/components/promote_label_modal_spec.js
+++ b/spec/frontend/pages/labels/components/promote_label_modal_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
+import { TEST_HOST } from 'jest/helpers/test_constants';
import promoteLabelModal from '~/pages/projects/labels/components/promote_label_modal.vue';
import eventHub from '~/pages/projects/labels/event_hub';
import axios from '~/lib/utils/axios_utils';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('Promote label modal', () => {
let vm;
diff --git a/spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js
index c376cf02594..1d9a964c3c3 100644
--- a/spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js
+++ b/spec/frontend/pages/milestones/shared/components/delete_milestone_modal_spec.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
-import { redirectTo } from '~/lib/utils/url_utility';
import mountComponent from 'helpers/vue_mount_component_helper';
+import { TEST_HOST } from 'jest/helpers/test_constants';
+import { redirectTo } from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
-import { TEST_HOST } from 'jest/helpers/test_constants';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
diff --git a/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js b/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
index 87d32a67d47..e8a6e259837 100644
--- a/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
+++ b/spec/frontend/pages/milestones/shared/components/promote_milestone_modal_spec.js
@@ -1,9 +1,9 @@
import Vue from 'vue';
import mountComponent from 'helpers/vue_mount_component_helper';
+import { TEST_HOST } from 'jest/helpers/test_constants';
import promoteMilestoneModal from '~/pages/milestones/shared/components/promote_milestone_modal.vue';
import eventHub from '~/pages/milestones/shared/event_hub';
import axios from '~/lib/utils/axios_utils';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('Promote milestone modal', () => {
let vm;
diff --git a/spec/frontend/pages/profiles/show/emoji_menu_spec.js b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
index 00320fb4601..08fc0b92424 100644
--- a/spec/frontend/pages/profiles/show/emoji_menu_spec.js
+++ b/spec/frontend/pages/profiles/show/emoji_menu_spec.js
@@ -55,7 +55,7 @@ describe('EmojiMenu', () => {
});
});
- it('does not make an axios requst', done => {
+ it('does not make an axios request', done => {
jest.spyOn(axios, 'request').mockReturnValue();
emojiMenu.addAward(dummyVotesBlock(), dummyAwardUrl, dummyEmoji, false, () => {
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
index 979dff78eba..2ec608569e3 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_groups_list_spec.js
@@ -1,14 +1,14 @@
import AxiosMockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlSearchBoxByType } from '@gitlab/ui';
import { nextTick } from 'vue';
-import createFlash from '~/flash';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import ForkGroupsList from '~/pages/projects/forks/new/components/fork_groups_list.vue';
import ForkGroupsListItem from '~/pages/projects/forks/new/components/fork_groups_list_item.vue';
-import waitForPromises from 'helpers/wait_for_promises';
-jest.mock('~/flash', () => jest.fn());
+jest.mock('~/flash');
describe('Fork groups list component', () => {
let wrapper;
diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
index 94089ea922b..211f4ea20f5 100644
--- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
+++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
@@ -9,10 +9,10 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
<!---->
- <gl-dropdown-stub
+ <gl-deprecated-dropdown-stub
text="rspec"
>
- <gl-dropdown-item-stub
+ <gl-deprecated-dropdown-item-stub
value="rspec"
>
<div
@@ -32,8 +32,8 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
</span>
</div>
- </gl-dropdown-item-stub>
- <gl-dropdown-item-stub
+ </gl-deprecated-dropdown-item-stub>
+ <gl-deprecated-dropdown-item-stub
value="cypress"
>
<div
@@ -49,8 +49,8 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
</span>
</div>
- </gl-dropdown-item-stub>
- <gl-dropdown-item-stub
+ </gl-deprecated-dropdown-item-stub>
+ <gl-deprecated-dropdown-item-stub
value="karma"
>
<div
@@ -66,8 +66,8 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
</span>
</div>
- </gl-dropdown-item-stub>
- </gl-dropdown-stub>
+ </gl-deprecated-dropdown-item-stub>
+ </gl-deprecated-dropdown-stub>
</div>
<gl-area-chart-stub
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index 30c7ff78c6e..54a080fb62b 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -1,12 +1,12 @@
import MockAdapter from 'axios-mock-adapter';
import { shallowMount } from '@vue/test-utils';
-import { GlAlert, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlAlert, GlIcon, GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
import { GlAreaChart } from '@gitlab/ui/dist/charts';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import CodeCoverage from '~/pages/projects/graphs/components/code_coverage.vue';
import { codeCoverageMockData, sortedDataByDates } from './mock_data';
-import waitForPromises from 'helpers/wait_for_promises';
import httpStatusCodes from '~/lib/utils/http_status';
describe('Code Coverage', () => {
@@ -17,7 +17,7 @@ describe('Code Coverage', () => {
const findAlert = () => wrapper.find(GlAlert);
const findAreaChart = () => wrapper.find(GlAreaChart);
- const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findAllDropdownItems = () => wrapper.findAll(GlDeprecatedDropdownItem);
const findFirstDropdownItem = () => findAllDropdownItems().at(0);
const findSecondDropdownItem = () => findAllDropdownItems().at(1);
@@ -124,7 +124,7 @@ describe('Code Coverage', () => {
});
it('renders the dropdown with all custom names as options', () => {
- expect(wrapper.contains(GlDropdown)).toBeDefined();
+ expect(wrapper.contains(GlDeprecatedDropdown)).toBeDefined();
expect(findAllDropdownItems()).toHaveLength(codeCoverageMockData.length);
expect(findFirstDropdownItem().text()).toBe(codeCoverageMockData[0].group_name);
});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
index 8917251d285..4c73225b54c 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
@@ -1,5 +1,5 @@
import $ from 'jquery';
-import GLDropdown from '~/gl_dropdown'; // eslint-disable-line no-unused-vars
+import '~/gl_dropdown';
import TimezoneDropdown, {
formatUtcOffset,
formatTimezone,
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index 1f7eec567b8..a50ceed5d09 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -6,6 +6,8 @@ import {
visibilityLevelDescriptions,
visibilityOptions,
} from '~/pages/projects/shared/permissions/constants';
+import projectFeatureSetting from '~/pages/projects/shared/permissions/components/project_feature_setting.vue';
+import projectFeatureToggle from '~/vue_shared/components/toggle_button.vue';
const defaultProps = {
currentSettings: {
@@ -65,7 +67,13 @@ describe('Settings Panel', () => {
return mountComponent({ ...extraProps, currentSettings: currentSettingsProps });
};
- const findLFSSettingsMessage = () => wrapper.find({ ref: 'git-lfs-settings' }).find('p');
+ const findLFSSettingsRow = () => wrapper.find({ ref: 'git-lfs-settings' });
+ const findLFSSettingsMessage = () => findLFSSettingsRow().find('p');
+ const findLFSFeatureToggle = () => findLFSSettingsRow().find(projectFeatureToggle);
+
+ const findRepositoryFeatureProjectRow = () => wrapper.find({ ref: 'repository-settings' });
+ const findRepositoryFeatureSetting = () =>
+ findRepositoryFeatureProjectRow().find(projectFeatureSetting);
beforeEach(() => {
wrapper = mountComponent();
@@ -154,7 +162,7 @@ describe('Settings Panel', () => {
it('should set the repository help text when the visibility level is set to private', () => {
wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE });
- expect(wrapper.find({ ref: 'repository-settings' }).props().helpText).toEqual(
+ expect(findRepositoryFeatureProjectRow().props().helpText).toBe(
'View and edit files in this project',
);
});
@@ -162,7 +170,7 @@ describe('Settings Panel', () => {
it('should set the repository help text with a read access warning when the visibility level is set to non-private', () => {
wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PUBLIC });
- expect(wrapper.find({ ref: 'repository-settings' }).props().helpText).toEqual(
+ expect(findRepositoryFeatureProjectRow().props().helpText).toBe(
'View and edit files in this project. Non-project members will only have read access',
);
});
@@ -176,7 +184,7 @@ describe('Settings Panel', () => {
wrapper
.find('[name="project[project_feature_attributes][merge_requests_access_level]"]')
.props().disabledInput,
- ).toEqual(false);
+ ).toBe(false);
});
it('should disable the merge requests access level input when the repository is disabled', () => {
@@ -186,7 +194,7 @@ describe('Settings Panel', () => {
wrapper
.find('[name="project[project_feature_attributes][merge_requests_access_level]"]')
.props().disabledInput,
- ).toEqual(true);
+ ).toBe(true);
});
});
@@ -197,7 +205,7 @@ describe('Settings Panel', () => {
expect(
wrapper.find('[name="project[project_feature_attributes][forking_access_level]"]').props()
.disabledInput,
- ).toEqual(false);
+ ).toBe(false);
});
it('should disable the forking access level input when the repository is disabled', () => {
@@ -206,7 +214,7 @@ describe('Settings Panel', () => {
expect(
wrapper.find('[name="project[project_feature_attributes][forking_access_level]"]').props()
.disabledInput,
- ).toEqual(true);
+ ).toBe(true);
});
});
@@ -217,7 +225,7 @@ describe('Settings Panel', () => {
expect(
wrapper.find('[name="project[project_feature_attributes][builds_access_level]"]').props()
.disabledInput,
- ).toEqual(false);
+ ).toBe(false);
});
it('should disable the builds access level input when the repository is disabled', () => {
@@ -226,7 +234,7 @@ describe('Settings Panel', () => {
expect(
wrapper.find('[name="project[project_feature_attributes][builds_access_level]"]').props()
.disabledInput,
- ).toEqual(true);
+ ).toBe(true);
});
});
@@ -287,7 +295,7 @@ describe('Settings Panel', () => {
expect(
wrapper.find('[name="project[container_registry_enabled]"]').props().disabledInput,
- ).toEqual(false);
+ ).toBe(false);
});
it('should disable the container registry input when the repository is disabled', () => {
@@ -298,7 +306,7 @@ describe('Settings Panel', () => {
expect(
wrapper.find('[name="project[container_registry_enabled]"]').props().disabledInput,
- ).toEqual(true);
+ ).toBe(true);
});
});
@@ -307,7 +315,7 @@ describe('Settings Panel', () => {
wrapper.setProps({ lfsAvailable: true });
return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'git-lfs-settings' }).exists()).toEqual(true);
+ expect(findLFSSettingsRow().exists()).toBe(true);
});
});
@@ -315,14 +323,12 @@ describe('Settings Panel', () => {
wrapper.setProps({ lfsAvailable: false });
return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'git-lfs-settings' }).exists()).toEqual(false);
+ expect(findLFSSettingsRow().exists()).toBe(false);
});
});
it('should set the LFS settings help path', () => {
- expect(wrapper.find({ ref: 'git-lfs-settings' }).props().helpPath).toBe(
- defaultProps.lfsHelpPath,
- );
+ expect(findLFSSettingsRow().props().helpPath).toBe(defaultProps.lfsHelpPath);
});
it('should enable the LFS input when the repository is enabled', () => {
@@ -331,7 +337,7 @@ describe('Settings Panel', () => {
{ lfsAvailable: true },
);
- expect(wrapper.find('[name="project[lfs_enabled]"]').props().disabledInput).toEqual(false);
+ expect(findLFSFeatureToggle().props().disabledInput).toBe(false);
});
it('should disable the LFS input when the repository is disabled', () => {
@@ -340,7 +346,27 @@ describe('Settings Panel', () => {
{ lfsAvailable: true },
);
- expect(wrapper.find('[name="project[lfs_enabled]"]').props().disabledInput).toEqual(true);
+ expect(findLFSFeatureToggle().props().disabledInput).toBe(true);
+ });
+
+ it('should not change lfsEnabled when disabling the repository', async () => {
+ // mount over shallowMount, because we are aiming to test rendered state of toggle
+ wrapper = mountComponent({ currentSettings: { lfsEnabled: true } }, mount);
+
+ const repositoryFeatureToggleButton = findRepositoryFeatureSetting().find('button');
+ const lfsFeatureToggleButton = findLFSFeatureToggle().find('button');
+ const isToggleButtonChecked = toggleButton => toggleButton.classes('is-checked');
+
+ // assert the initial state
+ expect(isToggleButtonChecked(lfsFeatureToggleButton)).toBe(true);
+ expect(isToggleButtonChecked(repositoryFeatureToggleButton)).toBe(true);
+
+ repositoryFeatureToggleButton.trigger('click');
+ await wrapper.vm.$nextTick();
+
+ expect(isToggleButtonChecked(repositoryFeatureToggleButton)).toBe(false);
+ // LFS toggle should still be checked
+ expect(isToggleButtonChecked(lfsFeatureToggleButton)).toBe(true);
});
describe.each`
@@ -364,14 +390,14 @@ describe('Settings Panel', () => {
expect(message.text()).toContain(
'LFS objects from this repository are still available to forks',
);
- expect(link.text()).toEqual('How do I remove them?');
- expect(link.attributes('href')).toEqual(
+ expect(link.text()).toBe('How do I remove them?');
+ expect(link.attributes('href')).toBe(
'/help/topics/git/lfs/index#removing-objects-from-lfs',
);
});
} else {
it('does not show warning message', () => {
- expect(findLFSSettingsMessage().exists()).toEqual(false);
+ expect(findLFSSettingsMessage().exists()).toBe(false);
});
}
},
@@ -383,7 +409,7 @@ describe('Settings Panel', () => {
wrapper.setProps({ packagesAvailable: true });
return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'package-settings' }).exists()).toEqual(true);
+ expect(wrapper.find({ ref: 'package-settings' }).exists()).toBe(true);
});
});
@@ -391,7 +417,7 @@ describe('Settings Panel', () => {
wrapper.setProps({ packagesAvailable: false });
return wrapper.vm.$nextTick(() => {
- expect(wrapper.find({ ref: 'package-settings' }).exists()).toEqual(false);
+ expect(wrapper.find({ ref: 'package-settings' }).exists()).toBe(false);
});
});
@@ -411,9 +437,7 @@ describe('Settings Panel', () => {
{ packagesAvailable: true },
);
- expect(wrapper.find('[name="project[packages_enabled]"]').props().disabledInput).toEqual(
- false,
- );
+ expect(wrapper.find('[name="project[packages_enabled]"]').props().disabledInput).toBe(false);
});
it('should disable the packages input when the repository is disabled', () => {
@@ -422,9 +446,7 @@ describe('Settings Panel', () => {
{ packagesAvailable: true },
);
- expect(wrapper.find('[name="project[packages_enabled]"]').props().disabledInput).toEqual(
- true,
- );
+ expect(wrapper.find('[name="project[packages_enabled]"]').props().disabledInput).toBe(true);
});
});
@@ -503,7 +525,7 @@ describe('Settings Panel', () => {
});
it('should contain help text', () => {
- expect(wrapper.find({ ref: 'metrics-visibility-settings' }).props().helpText).toEqual(
+ expect(wrapper.find({ ref: 'metrics-visibility-settings' }).props().helpText).toBe(
'With Metrics Dashboard you can visualize this project performance metrics',
);
});
@@ -514,7 +536,7 @@ describe('Settings Panel', () => {
const metricsSettingsRow = wrapper.find({ ref: 'metrics-visibility-settings' });
expect(wrapper.vm.metricsOptionsDropdownEnabled).toBe(true);
- expect(metricsSettingsRow.find('select').attributes('disabled')).toEqual('disabled');
+ expect(metricsSettingsRow.find('select').attributes('disabled')).toBe('disabled');
});
});
});
diff --git a/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js b/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
index 738498edbd3..589ec0ae047 100644
--- a/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
+++ b/spec/frontend/pages/sessions/new/signin_tabs_memoizer_spec.js
@@ -1,8 +1,6 @@
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import AccessorUtilities from '~/lib/utils/accessor';
import SigninTabsMemoizer from '~/pages/sessions/new/signin_tabs_memoizer';
-import trackData from '~/pages/sessions/new/index';
-import Tracking from '~/tracking';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
useLocalStorageSpy();
@@ -99,50 +97,6 @@ describe('SigninTabsMemoizer', () => {
});
});
- describe('trackData', () => {
- beforeEach(() => {
- jest.spyOn(Tracking, 'event').mockImplementation(() => {});
- });
-
- describe('with tracking data', () => {
- beforeEach(() => {
- gon.tracking_data = {
- category: 'Growth::Acquisition::Experiment::SignUpFlow',
- action: 'start',
- label: 'uuid',
- property: 'control_group',
- };
- trackData();
- });
-
- it('should track data when the "click" event of the register tab is triggered', () => {
- document.querySelector('a[href="#register-pane"]').click();
-
- expect(Tracking.event).toHaveBeenCalledWith(
- 'Growth::Acquisition::Experiment::SignUpFlow',
- 'start',
- {
- label: 'uuid',
- property: 'control_group',
- },
- );
- });
- });
-
- describe('without tracking data', () => {
- beforeEach(() => {
- gon.tracking_data = undefined;
- trackData();
- });
-
- it('should not track data when the "click" event of the register tab is triggered', () => {
- document.querySelector('a[href="#register-pane"]').click();
-
- expect(Tracking.event).not.toHaveBeenCalled();
- });
- });
- });
-
describe('saveData', () => {
beforeEach(() => {
memo = {
diff --git a/spec/frontend/pdf/page_spec.js b/spec/frontend/pdf/page_spec.js
index 4e24b0696ec..f9d94781265 100644
--- a/spec/frontend/pdf/page_spec.js
+++ b/spec/frontend/pdf/page_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import PageComponent from '~/pdf/page/index.vue';
import mountComponent from 'helpers/vue_mount_component_helper';
+import PageComponent from '~/pdf/page/index.vue';
jest.mock('pdfjs-dist/webpack', () => {
return { default: jest.requireActual('pdfjs-dist/build/pdf') };
diff --git a/spec/frontend/performance_bar/components/detailed_metric_spec.js b/spec/frontend/performance_bar/components/detailed_metric_spec.js
index f040dcfdea4..b9dc4c9588c 100644
--- a/spec/frontend/performance_bar/components/detailed_metric_spec.js
+++ b/spec/frontend/performance_bar/components/detailed_metric_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
+import { trimText } from 'helpers/text_helper';
import DetailedMetric from '~/performance_bar/components/detailed_metric.vue';
import RequestWarning from '~/performance_bar/components/request_warning.vue';
-import { trimText } from 'helpers/text_helper';
describe('detailedMetric', () => {
let wrapper;
diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js
index 97985ba3a07..578fd8d836a 100644
--- a/spec/frontend/persistent_user_callout_spec.js
+++ b/spec/frontend/persistent_user_callout_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import PersistentUserCallout from '~/persistent_user_callout';
-import Flash from '~/flash';
+import { deprecatedCreateFlash as Flash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
new file mode 100644
index 00000000000..d1e6b6b938a
--- /dev/null
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -0,0 +1,108 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { GlNewDropdown, GlNewDropdownItem, GlForm } from '@gitlab/ui';
+import Api from '~/api';
+import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
+import { mockRefs, mockParams, mockPostParams, mockProjectId } from '../mock_data';
+
+describe('Pipeline New Form', () => {
+ let wrapper;
+
+ const dummySubmitEvent = {
+ preventDefault() {},
+ };
+
+ const findForm = () => wrapper.find(GlForm);
+ const findDropdown = () => wrapper.find(GlNewDropdown);
+ const findDropdownItems = () => wrapper.findAll(GlNewDropdownItem);
+ const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
+ const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
+ const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
+
+ const createComponent = (term = '', props = {}, method = shallowMount) => {
+ wrapper = method(PipelineNewForm, {
+ propsData: {
+ projectId: mockProjectId,
+ pipelinesPath: '',
+ refs: mockRefs,
+ defaultBranch: 'master',
+ settingsLink: '',
+ ...props,
+ },
+ data() {
+ return {
+ searchTerm: term,
+ };
+ },
+ });
+ };
+
+ beforeEach(() => {
+ jest.spyOn(Api, 'createPipeline').mockResolvedValue({ data: { web_url: '/' } });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('Dropdown with branches and tags', () => {
+ it('displays dropdown with all branches and tags', () => {
+ createComponent();
+ expect(findDropdownItems().length).toBe(mockRefs.length);
+ });
+
+ it('when user enters search term the list is filtered', () => {
+ createComponent('master');
+
+ expect(findDropdownItems().length).toBe(1);
+ expect(
+ findDropdownItems()
+ .at(0)
+ .text(),
+ ).toBe('master');
+ });
+ });
+
+ describe('Form', () => {
+ beforeEach(() => {
+ createComponent('', mockParams, mount);
+ });
+ it('displays the correct values for the provided query params', () => {
+ expect(findDropdown().props('text')).toBe('tag-1');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findVariableRows().length).toBe(3);
+ });
+ });
+
+ it('does not display remove icon for last row', () => {
+ expect(findRemoveIcons().length).toBe(2);
+ });
+
+ it('removes ci variable row on remove icon button click', () => {
+ findRemoveIcons()
+ .at(1)
+ .trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findVariableRows().length).toBe(2);
+ });
+ });
+
+ it('creates a pipeline on submit', () => {
+ findForm().vm.$emit('submit', dummySubmitEvent);
+
+ expect(Api.createPipeline).toHaveBeenCalledWith(mockProjectId, mockPostParams);
+ });
+
+ it('creates blank variable on input change event', () => {
+ findKeyInputs()
+ .at(2)
+ .trigger('change');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findVariableRows().length).toBe(4);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_new/mock_data.js b/spec/frontend/pipeline_new/mock_data.js
new file mode 100644
index 00000000000..55ec1fb5afc
--- /dev/null
+++ b/spec/frontend/pipeline_new/mock_data.js
@@ -0,0 +1,21 @@
+export const mockRefs = ['master', 'branch-1', 'tag-1'];
+
+export const mockParams = {
+ refParam: 'tag-1',
+ variableParams: {
+ test_var: 'test_var_val',
+ },
+ fileParams: {
+ test_file: 'test_file_val',
+ },
+};
+
+export const mockProjectId = '21';
+
+export const mockPostParams = {
+ ref: 'tag-1',
+ variables: [
+ { key: 'test_var', value: 'test_var_val', variable_type: 'env_var' },
+ { key: 'test_file', value: 'test_file_val', variable_type: 'file' },
+ ],
+};
diff --git a/spec/frontend/pipelines/components/dag/dag_spec.js b/spec/frontend/pipelines/components/dag/dag_spec.js
index 7dea6d819b9..989f6c17197 100644
--- a/spec/frontend/pipelines/components/dag/dag_spec.js
+++ b/spec/frontend/pipelines/components/dag/dag_spec.js
@@ -1,7 +1,4 @@
import { mount, shallowMount } from '@vue/test-utils';
-import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
-import waitForPromises from 'helpers/wait_for_promises';
import { GlAlert, GlEmptyState } from '@gitlab/ui';
import Dag from '~/pipelines/components/dag/dag.vue';
import DagGraph from '~/pipelines/components/dag/dag_graph.vue';
@@ -11,13 +8,11 @@ import {
ADD_NOTE,
REMOVE_NOTE,
REPLACE_NOTES,
- DEFAULT,
PARSE_FAILURE,
- LOAD_FAILURE,
UNSUPPORTED_DATA,
} from '~/pipelines/components/dag//constants';
import {
- mockBaseData,
+ mockParsedGraphQLNodes,
tooSmallGraph,
unparseableGraph,
graphWithoutDependencies,
@@ -27,7 +22,6 @@ import {
describe('Pipeline DAG graph wrapper', () => {
let wrapper;
- let mock;
const getAlert = () => wrapper.find(GlAlert);
const getAllAlerts = () => wrapper.findAll(GlAlert);
const getGraph = () => wrapper.find(DagGraph);
@@ -35,45 +29,46 @@ describe('Pipeline DAG graph wrapper', () => {
const getErrorText = type => wrapper.vm.$options.errorTexts[type];
const getEmptyState = () => wrapper.find(GlEmptyState);
- const dataPath = '/root/test/pipelines/90/dag.json';
-
- const createComponent = (propsData = {}, method = shallowMount) => {
+ const createComponent = ({
+ graphData = mockParsedGraphQLNodes,
+ provideOverride = {},
+ method = shallowMount,
+ } = {}) => {
if (wrapper?.destroy) {
wrapper.destroy();
}
wrapper = method(Dag, {
- propsData: {
+ provide: {
+ pipelineProjectPath: 'root/abc-dag',
+ pipelineIid: '1',
emptySvgPath: '/my-svg',
dagDocPath: '/my-doc',
- ...propsData,
+ ...provideOverride,
},
data() {
return {
+ graphData,
showFailureAlert: false,
};
},
});
};
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
afterEach(() => {
- mock.restore();
wrapper.destroy();
wrapper = null;
});
- describe('when there is no dataUrl', () => {
+ describe('when a query argument is undefined', () => {
beforeEach(() => {
- createComponent({ graphUrl: undefined });
+ createComponent({
+ provideOverride: { pipelineProjectPath: undefined },
+ graphData: null,
+ });
});
- it('shows the DEFAULT alert and not the graph', () => {
- expect(getAlert().exists()).toBe(true);
- expect(getAlert().text()).toBe(getErrorText(DEFAULT));
+ it('does not render the graph', async () => {
expect(getGraph().exists()).toBe(false);
});
@@ -82,36 +77,12 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
- describe('when there is a dataUrl', () => {
- describe('but the data fetch fails', () => {
+ describe('when all query variables are defined', () => {
+ describe('but the parse fails', () => {
beforeEach(async () => {
- mock.onGet(dataPath).replyOnce(500);
- createComponent({ graphUrl: dataPath });
-
- await wrapper.vm.$nextTick();
-
- return waitForPromises();
- });
-
- it('shows the LOAD_FAILURE alert and not the graph', () => {
- expect(getAlert().exists()).toBe(true);
- expect(getAlert().text()).toBe(getErrorText(LOAD_FAILURE));
- expect(getGraph().exists()).toBe(false);
- });
-
- it('does not render the empty state', () => {
- expect(getEmptyState().exists()).toBe(false);
- });
- });
-
- describe('the data fetch succeeds but the parse fails', () => {
- beforeEach(async () => {
- mock.onGet(dataPath).replyOnce(200, unparseableGraph);
- createComponent({ graphUrl: dataPath });
-
- await wrapper.vm.$nextTick();
-
- return waitForPromises();
+ createComponent({
+ graphData: unparseableGraph,
+ });
});
it('shows the PARSE_FAILURE alert and not the graph', () => {
@@ -125,19 +96,12 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
- describe('and the data fetch and parse succeeds', () => {
+ describe('parse succeeds', () => {
beforeEach(async () => {
- mock.onGet(dataPath).replyOnce(200, mockBaseData);
- createComponent({ graphUrl: dataPath }, mount);
-
- await wrapper.vm.$nextTick();
-
- return waitForPromises();
+ createComponent({ method: mount });
});
- it('shows the graph and the beta alert', () => {
- expect(getAllAlerts().length).toBe(1);
- expect(getAlert().text()).toContain('This feature is currently in beta.');
+ it('shows the graph', () => {
expect(getGraph().exists()).toBe(true);
});
@@ -146,14 +110,11 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
- describe('the data fetch and parse succeeds, but the resulting graph is too small', () => {
+ describe('parse succeeds, but the resulting graph is too small', () => {
beforeEach(async () => {
- mock.onGet(dataPath).replyOnce(200, tooSmallGraph);
- createComponent({ graphUrl: dataPath });
-
- await wrapper.vm.$nextTick();
-
- return waitForPromises();
+ createComponent({
+ graphData: tooSmallGraph,
+ });
});
it('shows the UNSUPPORTED_DATA alert and not the graph', () => {
@@ -167,19 +128,16 @@ describe('Pipeline DAG graph wrapper', () => {
});
});
- describe('the data fetch succeeds but the returned data is empty', () => {
+ describe('the returned data is empty', () => {
beforeEach(async () => {
- mock.onGet(dataPath).replyOnce(200, graphWithoutDependencies);
- createComponent({ graphUrl: dataPath }, mount);
-
- await wrapper.vm.$nextTick();
-
- return waitForPromises();
+ createComponent({
+ method: mount,
+ graphData: graphWithoutDependencies,
+ });
});
it('does not render an error alert or the graph', () => {
- expect(getAllAlerts().length).toBe(1);
- expect(getAlert().text()).toContain('This feature is currently in beta.');
+ expect(getAllAlerts().length).toBe(0);
expect(getGraph().exists()).toBe(false);
});
@@ -191,12 +149,7 @@ describe('Pipeline DAG graph wrapper', () => {
describe('annotations', () => {
beforeEach(async () => {
- mock.onGet(dataPath).replyOnce(200, mockBaseData);
- createComponent({ graphUrl: dataPath }, mount);
-
- await wrapper.vm.$nextTick();
-
- return waitForPromises();
+ createComponent();
});
it('toggles on link mouseover and mouseout', async () => {
diff --git a/spec/frontend/pipelines/components/dag/drawing_utils_spec.js b/spec/frontend/pipelines/components/dag/drawing_utils_spec.js
index a50163411ed..37a7d07485b 100644
--- a/spec/frontend/pipelines/components/dag/drawing_utils_spec.js
+++ b/spec/frontend/pipelines/components/dag/drawing_utils_spec.js
@@ -1,9 +1,9 @@
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
import { parseData } from '~/pipelines/components/dag/parsing_utils';
-import { mockBaseData } from './mock_data';
+import { mockParsedGraphQLNodes } from './mock_data';
describe('DAG visualization drawing utilities', () => {
- const parsed = parseData(mockBaseData.stages);
+ const parsed = parseData(mockParsedGraphQLNodes);
const layoutSettings = {
width: 200,
diff --git a/spec/frontend/pipelines/components/dag/mock_data.js b/spec/frontend/pipelines/components/dag/mock_data.js
index 3b39b9cd21c..e7e93804195 100644
--- a/spec/frontend/pipelines/components/dag/mock_data.js
+++ b/spec/frontend/pipelines/components/dag/mock_data.js
@@ -1,127 +1,56 @@
-/*
- It is important that the simple base include parallel jobs
- as well as non-parallel jobs with spaces in the name to prevent
- us relying on spaces as an indicator.
-*/
-export const mockBaseData = {
- stages: [
- {
- name: 'test',
- groups: [
- {
- name: 'jest',
- size: 2,
- jobs: [{ name: 'jest 1/2', needs: ['frontend fixtures'] }, { name: 'jest 2/2' }],
- },
- {
- name: 'rspec',
- size: 1,
- jobs: [{ name: 'rspec', needs: ['frontend fixtures'] }],
- },
- ],
- },
- {
- name: 'fixtures',
- groups: [
- {
- name: 'frontend fixtures',
- size: 1,
- jobs: [{ name: 'frontend fixtures' }],
- },
- ],
- },
- {
- name: 'un-needed',
- groups: [
- {
- name: 'un-needed',
- size: 1,
- jobs: [{ name: 'un-needed' }],
- },
- ],
- },
- ],
-};
-
-export const tooSmallGraph = {
- stages: [
- {
- name: 'test',
- groups: [
- {
- name: 'jest',
- size: 2,
- jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
- },
- {
- name: 'rspec',
- size: 1,
- jobs: [{ name: 'rspec', needs: ['frontend fixtures'] }],
- },
- ],
- },
- {
- name: 'fixtures',
- groups: [
- {
- name: 'frontend fixtures',
- size: 1,
- jobs: [{ name: 'frontend fixtures' }],
- },
- ],
- },
- {
- name: 'un-needed',
- groups: [
- {
- name: 'un-needed',
- size: 1,
- jobs: [{ name: 'un-needed' }],
- },
- ],
- },
- ],
-};
+export const tooSmallGraph = [
+ {
+ category: 'test',
+ name: 'jest',
+ size: 2,
+ jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
+ },
+ {
+ category: 'test',
+ name: 'rspec',
+ size: 1,
+ jobs: [{ name: 'rspec', needs: ['frontend fixtures'] }],
+ },
+ {
+ category: 'fixtures',
+ name: 'frontend fixtures',
+ size: 1,
+ jobs: [{ name: 'frontend fixtures' }],
+ },
+ {
+ category: 'un-needed',
+ name: 'un-needed',
+ size: 1,
+ jobs: [{ name: 'un-needed' }],
+ },
+];
-export const graphWithoutDependencies = {
- stages: [
- {
- name: 'test',
- groups: [
- {
- name: 'jest',
- size: 2,
- jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
- },
- {
- name: 'rspec',
- size: 1,
- jobs: [{ name: 'rspec' }],
- },
- ],
- },
- {
- name: 'fixtures',
- groups: [
- {
- name: 'frontend fixtures',
- size: 1,
- jobs: [{ name: 'frontend fixtures' }],
- },
- ],
- },
- {
- name: 'un-needed',
- groups: [
- {
- name: 'un-needed',
- size: 1,
- jobs: [{ name: 'un-needed' }],
- },
- ],
- },
- ],
-};
+export const graphWithoutDependencies = [
+ {
+ category: 'test',
+ name: 'jest',
+ size: 2,
+ jobs: [{ name: 'jest 1/2' }, { name: 'jest 2/2' }],
+ },
+ {
+ category: 'test',
+ name: 'rspec',
+ size: 1,
+ jobs: [{ name: 'rspec' }],
+ },
+ {
+ category: 'fixtures',
+ name: 'frontend fixtures',
+ size: 1,
+ jobs: [{ name: 'frontend fixtures' }],
+ },
+ {
+ category: 'un-needed',
+ name: 'un-needed',
+ size: 1,
+ jobs: [{ name: 'un-needed' }],
+ },
+];
export const unparseableGraph = [
{
@@ -468,3 +397,264 @@ export const multiNote = {
},
},
};
+
+/*
+ It is important that the base include parallel jobs
+ as well as non-parallel jobs with spaces in the name to prevent
+ us relying on spaces as an indicator.
+*/
+
+export const mockParsedGraphQLNodes = [
+ {
+ category: 'build',
+ name: 'build_a',
+ size: 1,
+ jobs: [
+ {
+ name: 'build_a',
+ needs: [],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'build',
+ name: 'build_b',
+ size: 1,
+ jobs: [
+ {
+ name: 'build_b',
+ needs: [],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'test',
+ name: 'test_a',
+ size: 1,
+ jobs: [
+ {
+ name: 'test_a',
+ needs: ['build_a'],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'test',
+ name: 'test_b',
+ size: 1,
+ jobs: [
+ {
+ name: 'test_b',
+ needs: [],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'test',
+ name: 'test_c',
+ size: 1,
+ jobs: [
+ {
+ name: 'test_c',
+ needs: [],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'test',
+ name: 'test_d',
+ size: 1,
+ jobs: [
+ {
+ name: 'test_d',
+ needs: [],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'post-test',
+ name: 'post_test_a',
+ size: 1,
+ jobs: [
+ {
+ name: 'post_test_a',
+ needs: [],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'post-test',
+ name: 'post_test_b',
+ size: 1,
+ jobs: [
+ {
+ name: 'post_test_b',
+ needs: [],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'post-test',
+ name: 'post_test_c',
+ size: 1,
+ jobs: [
+ {
+ name: 'post_test_c',
+ needs: ['test_b', 'test_a'],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'staging',
+ name: 'staging_a',
+ size: 1,
+ jobs: [
+ {
+ name: 'staging_a',
+ needs: ['post_test_a'],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'staging',
+ name: 'staging_b',
+ size: 1,
+ jobs: [
+ {
+ name: 'staging_b',
+ needs: ['post_test_b'],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'staging',
+ name: 'staging_c',
+ size: 1,
+ jobs: [
+ {
+ name: 'staging_c',
+ needs: [],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'staging',
+ name: 'staging_d',
+ size: 1,
+ jobs: [
+ {
+ name: 'staging_d',
+ needs: [],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'staging',
+ name: 'staging_e',
+ size: 1,
+ jobs: [
+ {
+ name: 'staging_e',
+ needs: [],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'canary',
+ name: 'canary_a',
+ size: 1,
+ jobs: [
+ {
+ name: 'canary_a',
+ needs: ['staging_b', 'staging_a'],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'canary',
+ name: 'canary_b',
+ size: 1,
+ jobs: [
+ {
+ name: 'canary_b',
+ needs: [],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'canary',
+ name: 'canary_c',
+ size: 1,
+ jobs: [
+ {
+ name: 'canary_c',
+ needs: ['staging_b'],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'production',
+ name: 'production_a',
+ size: 1,
+ jobs: [
+ {
+ name: 'production_a',
+ needs: ['canary_a'],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'production',
+ name: 'production_b',
+ size: 1,
+ jobs: [
+ {
+ name: 'production_b',
+ needs: [],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'production',
+ name: 'production_c',
+ size: 1,
+ jobs: [
+ {
+ name: 'production_c',
+ needs: [],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+ {
+ category: 'production',
+ name: 'production_d',
+ size: 1,
+ jobs: [
+ {
+ name: 'production_d',
+ needs: ['canary_c'],
+ },
+ ],
+ __typename: 'CiGroup',
+ },
+];
diff --git a/spec/frontend/pipelines/components/dag/parsing_utils_spec.js b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
index d9a1296e572..e93fa8e6760 100644
--- a/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
+++ b/spec/frontend/pipelines/components/dag/parsing_utils_spec.js
@@ -1,5 +1,5 @@
import {
- createNodesStructure,
+ createNodeDict,
makeLinksFromNodes,
filterByAncestors,
parseData,
@@ -8,56 +8,17 @@ import {
} from '~/pipelines/components/dag/parsing_utils';
import { createSankey } from '~/pipelines/components/dag/drawing_utils';
-import { mockBaseData } from './mock_data';
+import { mockParsedGraphQLNodes } from './mock_data';
describe('DAG visualization parsing utilities', () => {
- const { nodes, nodeDict } = createNodesStructure(mockBaseData.stages);
- const unfilteredLinks = makeLinksFromNodes(nodes, nodeDict);
- const parsed = parseData(mockBaseData.stages);
-
- const layoutSettings = {
- width: 200,
- height: 200,
- nodeWidth: 10,
- nodePadding: 20,
- paddingForLabels: 100,
- };
-
- const sankeyLayout = createSankey(layoutSettings)(parsed);
-
- describe('createNodesStructure', () => {
- const parallelGroupName = 'jest';
- const parallelJobName = 'jest 1/2';
- const singleJobName = 'frontend fixtures';
-
- const { name, jobs, size } = mockBaseData.stages[0].groups[0];
-
- it('returns the expected node structure', () => {
- expect(nodes[0]).toHaveProperty('category', mockBaseData.stages[0].name);
- expect(nodes[0]).toHaveProperty('name', name);
- expect(nodes[0]).toHaveProperty('jobs', jobs);
- expect(nodes[0]).toHaveProperty('size', size);
- });
-
- it('adds needs to top level of nodeDict entries', () => {
- expect(nodeDict[parallelGroupName]).toHaveProperty('needs');
- expect(nodeDict[parallelJobName]).toHaveProperty('needs');
- expect(nodeDict[singleJobName]).toHaveProperty('needs');
- });
-
- it('makes entries in nodeDict for jobs and parallel jobs', () => {
- const nodeNames = Object.keys(nodeDict);
-
- expect(nodeNames.includes(parallelGroupName)).toBe(true);
- expect(nodeNames.includes(parallelJobName)).toBe(true);
- expect(nodeNames.includes(singleJobName)).toBe(true);
- });
- });
+ const nodeDict = createNodeDict(mockParsedGraphQLNodes);
+ const unfilteredLinks = makeLinksFromNodes(mockParsedGraphQLNodes, nodeDict);
+ const parsed = parseData(mockParsedGraphQLNodes);
describe('makeLinksFromNodes', () => {
it('returns the expected link structure', () => {
- expect(unfilteredLinks[0]).toHaveProperty('source', 'frontend fixtures');
- expect(unfilteredLinks[0]).toHaveProperty('target', 'jest');
+ expect(unfilteredLinks[0]).toHaveProperty('source', 'build_a');
+ expect(unfilteredLinks[0]).toHaveProperty('target', 'test_a');
expect(unfilteredLinks[0]).toHaveProperty('value', 10);
});
});
@@ -107,8 +68,22 @@ describe('DAG visualization parsing utilities', () => {
describe('removeOrphanNodes', () => {
it('removes sankey nodes that have no needs and are not needed', () => {
+ const layoutSettings = {
+ width: 200,
+ height: 200,
+ nodeWidth: 10,
+ nodePadding: 20,
+ paddingForLabels: 100,
+ };
+
+ const sankeyLayout = createSankey(layoutSettings)(parsed);
const cleanedNodes = removeOrphanNodes(sankeyLayout.nodes);
- expect(cleanedNodes).toHaveLength(sankeyLayout.nodes.length - 1);
+ /*
+ These lengths are determined by the mock data.
+ If the data changes, the numbers may also change.
+ */
+ expect(parsed.nodes).toHaveLength(21);
+ expect(cleanedNodes).toHaveLength(12);
});
});
diff --git a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
index add7b56845e..c5b7318d3af 100644
--- a/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
+++ b/spec/frontend/pipelines/components/pipelines_filtered_search_spec.js
@@ -1,10 +1,10 @@
-import Api from '~/api';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { GlFilteredSearch } from '@gitlab/ui';
+import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import PipelinesFilteredSearch from '~/pipelines/components/pipelines_list/pipelines_filtered_search.vue';
import { users, mockSearch, branches, tags } from '../mock_data';
-import { GlFilteredSearch } from '@gitlab/ui';
describe('Pipelines filtered search', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
index 3c5938cfa1f..ab477292bc1 100644
--- a/spec/frontend/pipelines/graph/action_component_spec.js
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
@@ -7,7 +8,7 @@ import ActionComponent from '~/pipelines/components/graph/action_component.vue';
describe('pipeline graph action component', () => {
let wrapper;
let mock;
- const findButton = () => wrapper.find('button');
+ const findButton = () => wrapper.find(GlButton);
beforeEach(() => {
mock = new MockAdapter(axios);
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index 9731ce3f8a6..1389649abea 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import { mount } from '@vue/test-utils';
+import { setHTMLFixture } from 'helpers/fixtures';
import PipelineStore from '~/pipelines/stores/pipeline_store';
import graphComponent from '~/pipelines/components/graph/graph_component.vue';
import stageColumnComponent from '~/pipelines/components/graph/stage_column_component.vue';
@@ -7,7 +8,6 @@ import linkedPipelinesColumn from '~/pipelines/components/graph/linked_pipelines
import graphJSON from './mock_data';
import linkedPipelineJSON from './linked_pipelines_mock_data';
import PipelinesMediator from '~/pipelines/pipeline_details_mediator';
-import { setHTMLFixture } from 'helpers/fixtures';
describe('graph component', () => {
const store = new PipelineStore();
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index 133d5695afb..59121c54ff3 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -1,4 +1,5 @@
import { mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
@@ -12,7 +13,7 @@ const invalidTriggeredPipelineId = mockPipeline.project.id + 5;
describe('Linked pipeline', () => {
let wrapper;
- const findButton = () => wrapper.find('button');
+ const findButton = () => wrapper.find(GlButton);
const findPipelineLabel = () => wrapper.find('[data-testid="downstream-pipeline-label"]');
const findLinkedPipeline = () => wrapper.find({ ref: 'linkedPipeline' });
@@ -42,9 +43,7 @@ describe('Linked pipeline', () => {
});
it('should render a button', () => {
- const linkElement = wrapper.find('.js-linked-pipeline-content');
-
- expect(linkElement.exists()).toBe(true);
+ expect(findButton().exists()).toBe(true);
});
it('should render the project name', () => {
@@ -62,7 +61,7 @@ describe('Linked pipeline', () => {
});
it('should have a ci-status child component', () => {
- expect(wrapper.find('.js-linked-pipeline-status').exists()).toBe(true);
+ expect(wrapper.find(CiStatus).exists()).toBe(true);
});
it('should render the pipeline id', () => {
@@ -77,15 +76,14 @@ describe('Linked pipeline', () => {
});
it('should render the tooltip text as the title attribute', () => {
- const tooltipRef = wrapper.find('.js-linked-pipeline-content');
- const titleAttr = tooltipRef.attributes('title');
+ const titleAttr = findButton().attributes('title');
expect(titleAttr).toContain(mockPipeline.project.name);
expect(titleAttr).toContain(mockPipeline.details.status.label);
});
- it('does not render the loading icon when isLoading is false', () => {
- expect(wrapper.find('.js-linked-pipeline-loading').exists()).toBe(false);
+ it('sets the loading prop to false', () => {
+ expect(findButton().props('loading')).toBe(false);
});
it('should display multi-project label when pipeline project id is not the same as triggered pipeline project id', () => {
@@ -132,8 +130,8 @@ describe('Linked pipeline', () => {
createWrapper(props);
});
- it('renders a loading icon', () => {
- expect(wrapper.find('.js-linked-pipeline-loading').exists()).toBe(true);
+ it('sets the loading prop to true', () => {
+ expect(findButton().props('loading')).toBe(true);
});
});
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index 1c3a6c545a0..5388d624d3c 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
import HeaderComponent from '~/pipelines/components/header_component.vue';
import CiHeader from '~/vue_shared/components/header_ci_component.vue';
import eventHub from '~/pipelines/event_hub';
-import { GlModal } from '@gitlab/ui';
describe('Pipeline details header', () => {
let wrapper;
@@ -85,13 +85,13 @@ describe('Pipeline details header', () => {
});
it('should call postAction when retry button action is clicked', () => {
- wrapper.find('.js-retry-button').vm.$emit('click');
+ wrapper.find('[data-testid="retryButton"]').vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'retry');
});
it('should call postAction when cancel button action is clicked', () => {
- wrapper.find('.js-btn-cancel-pipeline').vm.$emit('click');
+ wrapper.find('[data-testid="cancelPipeline"]').vm.$emit('click');
expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel');
});
diff --git a/spec/frontend/pipelines/pipeline_details_mediator_spec.js b/spec/frontend/pipelines/pipeline_details_mediator_spec.js
index 083e97666ed..d6699a43b54 100644
--- a/spec/frontend/pipelines/pipeline_details_mediator_spec.js
+++ b/spec/frontend/pipelines/pipeline_details_mediator_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import PipelineMediator from '~/pipelines/pipeline_details_mediator';
-import waitForPromises from 'helpers/wait_for_promises';
describe('PipelineMdediator', () => {
let mediator;
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
index aef54d94974..cce4c2dfa7b 100644
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'spec/test_constants';
+import { GlDeprecatedButton } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import PipelinesActions from '~/pipelines/components/pipelines_list/pipelines_actions.vue';
-import { GlDeprecatedButton } from '@gitlab/ui';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
-import waitForPromises from 'helpers/wait_for_promises';
describe('Pipelines Actions dropdown', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/pipelines_artifacts_spec.js b/spec/frontend/pipelines/pipelines_artifacts_spec.js
index 512205c3fc3..83f6cb68eba 100644
--- a/spec/frontend/pipelines/pipelines_artifacts_spec.js
+++ b/spec/frontend/pipelines/pipelines_artifacts_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
import { GlLink } from '@gitlab/ui';
+import PipelineArtifacts from '~/pipelines/components/pipelines_list/pipelines_artifacts.vue';
describe('Pipelines Artifacts dropdown', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 66446b9aa1d..b0ad6bbd228 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -1,16 +1,16 @@
-import Api from '~/api';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import axios from '~/lib/utils/axios_utils';
import waitForPromises from 'helpers/wait_for_promises';
+import { GlFilteredSearch } from '@gitlab/ui';
+import Api from '~/api';
+import axios from '~/lib/utils/axios_utils';
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
import Store from '~/pipelines/stores/pipelines_store';
import { pipelineWithStages, stageReply, users, mockSearch, branches } from './mock_data';
import { RAW_TEXT_WARNING } from '~/pipelines/constants';
-import { GlFilteredSearch } from '@gitlab/ui';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
-jest.mock('~/flash', () => jest.fn());
+jest.mock('~/flash');
describe('Pipelines', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js
index 547f8994ca5..e134b81856b 100644
--- a/spec/frontend/pipelines/stage_spec.js
+++ b/spec/frontend/pipelines/stage_spec.js
@@ -1,10 +1,10 @@
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import StageComponent from '~/pipelines/components/pipelines_list/stage.vue';
import eventHub from '~/pipelines/event_hub';
import { stageReply } from './mock_data';
-import waitForPromises from 'helpers/wait_for_promises';
describe('Pipelines stage component', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index d4647c55a53..1809f15a6e6 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -5,7 +5,7 @@ import * as actions from '~/pipelines/stores/test_reports/actions';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
import { TEST_HOST } from '../../../helpers/test_constants';
import testAction from '../../../helpers/vuex_action_helper';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash.js');
@@ -16,14 +16,13 @@ describe('Actions TestReports Store', () => {
const testReports = getJSONFixture('pipelines/test_report.json');
const summary = { total_count: 1 };
- const fullReportEndpoint = `${TEST_HOST}/test_reports.json`;
+ const suiteEndpoint = `${TEST_HOST}/tests/:suite_name.json`;
const summaryEndpoint = `${TEST_HOST}/test_reports/summary.json`;
const defaultState = {
- fullReportEndpoint,
+ suiteEndpoint,
summaryEndpoint,
testReports: {},
selectedSuite: null,
- useBuildSummaryReport: false,
};
beforeEach(() => {
@@ -40,89 +39,63 @@ describe('Actions TestReports Store', () => {
mock.onGet(summaryEndpoint).replyOnce(200, summary, {});
});
- describe('when useBuildSummaryReport in state is true', () => {
- it('sets testReports and shows tests', done => {
- testAction(
- actions.fetchSummary,
- null,
- { ...state, useBuildSummaryReport: true },
- [{ type: types.SET_SUMMARY, payload: summary }],
- [{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
- done,
- );
- });
-
- it('should create flash on API error', done => {
- testAction(
- actions.fetchSummary,
- null,
- {
- summaryEndpoint: null,
- useBuildSummaryReport: true,
- },
- [],
- [{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
- () => {
- expect(createFlash).toHaveBeenCalled();
- done();
- },
- );
- });
+ it('sets testReports and shows tests', done => {
+ testAction(
+ actions.fetchSummary,
+ null,
+ state,
+ [{ type: types.SET_SUMMARY, payload: summary }],
+ [{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
+ done,
+ );
});
- describe('when useBuildSummaryReport in state is false', () => {
- it('sets testReports and shows tests', done => {
- testAction(
- actions.fetchSummary,
- null,
- state,
- [{ type: types.SET_SUMMARY, payload: summary }],
- [],
- done,
- );
- });
-
- it('should create flash on API error', done => {
- testAction(
- actions.fetchSummary,
- null,
- {
- summaryEndpoint: null,
- },
- [],
- [],
- () => {
- expect(createFlash).toHaveBeenCalled();
- done();
- },
- );
- });
+ it('should create flash on API error', done => {
+ testAction(
+ actions.fetchSummary,
+ null,
+ { summaryEndpoint: null },
+ [],
+ [{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
+ () => {
+ expect(createFlash).toHaveBeenCalled();
+ done();
+ },
+ );
});
});
- describe('fetch full report', () => {
+ describe('fetch test suite', () => {
beforeEach(() => {
- mock.onGet(fullReportEndpoint).replyOnce(200, testReports, {});
+ const buildIds = [1];
+ testReports.test_suites[0].build_ids = buildIds;
+ const endpoint = suiteEndpoint.replace(':suite_name', testReports.test_suites[0].name);
+ mock
+ .onGet(endpoint, { params: { build_ids: buildIds } })
+ .replyOnce(200, testReports.test_suites[0], {});
});
- it('sets testReports and shows tests', done => {
+ it('sets test suite and shows tests', done => {
+ const suite = testReports.test_suites[0];
+ const index = 0;
+
testAction(
- actions.fetchFullReport,
- null,
- state,
- [{ type: types.SET_REPORTS, payload: testReports }],
+ actions.fetchTestSuite,
+ index,
+ { ...state, testReports },
+ [{ type: types.SET_SUITE, payload: { suite, index } }],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
done,
);
});
it('should create flash on API error', done => {
+ const index = 0;
+
testAction(
- actions.fetchFullReport,
- null,
- {
- fullReportEndpoint: null,
- },
+ actions.fetchTestSuite,
+ index,
+ { ...state, testReports, suiteEndpoint: null },
[],
[{ type: 'toggleLoading' }, { type: 'toggleLoading' }],
() => {
@@ -131,6 +104,15 @@ describe('Actions TestReports Store', () => {
},
);
});
+
+ describe('when we already have the suite data', () => {
+ it('should not fetch suite', done => {
+ const index = 0;
+ testReports.test_suites[0].hasFullSuite = true;
+
+ testAction(actions.fetchTestSuite, index, { ...state, testReports }, [], [], done);
+ });
+ });
});
describe('set selected suite index', () => {
diff --git a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
index f4cc5c4bc5d..b935029bc6a 100644
--- a/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/mutations_spec.js
@@ -12,20 +12,24 @@ describe('Mutations TestReports Store', () => {
testReports: {},
selectedSuite: null,
isLoading: false,
- hasFullReport: false,
};
beforeEach(() => {
mockState = { ...defaultState };
});
- describe('set reports', () => {
- it('should set testReports', () => {
- const expectedState = { ...mockState, testReports };
- mutations[types.SET_REPORTS](mockState, testReports);
+ describe('set suite', () => {
+ it('should set the suite at the given index', () => {
+ mockState.testReports = testReports;
+ const suite = { name: 'test_suite' };
+ const index = 0;
+ const expectedState = { ...mockState };
+ expectedState.testReports.test_suites[index] = { suite, hasFullSuite: true };
+ mutations[types.SET_SUITE](mockState, { suite, index });
- expect(mockState.testReports).toEqual(expectedState.testReports);
- expect(mockState.hasFullReport).toBe(true);
+ expect(mockState.testReports.test_suites[index]).toEqual(
+ expectedState.testReports.test_suites[index],
+ );
});
});
@@ -40,10 +44,21 @@ describe('Mutations TestReports Store', () => {
describe('set summary', () => {
it('should set summary', () => {
- const summary = { total_count: 1 };
+ const summary = {
+ total: { time: 0, count: 10, success: 1, failed: 2, skipped: 3, error: 4 },
+ };
+ const expectedSummary = {
+ ...summary,
+ total_time: 0,
+ total_count: 10,
+ success_count: 1,
+ failed_count: 2,
+ skipped_count: 3,
+ error_count: 4,
+ };
mutations[types.SET_SUMMARY](mockState, summary);
- expect(mockState.testReports).toEqual(summary);
+ expect(mockState.testReports).toEqual(expectedSummary);
});
});
diff --git a/spec/frontend/pipelines/test_reports/test_reports_spec.js b/spec/frontend/pipelines/test_reports/test_reports_spec.js
index ef0bcffabe3..a709edf5184 100644
--- a/spec/frontend/pipelines/test_reports/test_reports_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_reports_spec.js
@@ -22,7 +22,7 @@ describe('Test reports app', () => {
const testSummaryTable = () => wrapper.find(TestSummaryTable);
const actionSpies = {
- fetchFullReport: jest.fn(),
+ fetchTestSuite: jest.fn(),
fetchSummary: jest.fn(),
setSelectedSuiteIndex: jest.fn(),
removeSelectedSuiteIndex: jest.fn(),
@@ -91,28 +91,14 @@ describe('Test reports app', () => {
});
describe('when a suite is clicked', () => {
- describe('when the full test report has already been received', () => {
- beforeEach(() => {
- createComponent({ hasFullReport: true });
- testSummaryTable().vm.$emit('row-click', 0);
- });
-
- it('should only call setSelectedSuiteIndex', () => {
- expect(actionSpies.setSelectedSuiteIndex).toHaveBeenCalled();
- expect(actionSpies.fetchFullReport).not.toHaveBeenCalled();
- });
+ beforeEach(() => {
+ createComponent({ hasFullReport: true });
+ testSummaryTable().vm.$emit('row-click', 0);
});
- describe('when the full test report has not been received', () => {
- beforeEach(() => {
- createComponent({ hasFullReport: false });
- testSummaryTable().vm.$emit('row-click', 0);
- });
-
- it('should call setSelectedSuiteIndex and fetchFullReport', () => {
- expect(actionSpies.setSelectedSuiteIndex).toHaveBeenCalled();
- expect(actionSpies.fetchFullReport).toHaveBeenCalled();
- });
+ it('should call setSelectedSuiteIndex and fetchTestSuite', () => {
+ expect(actionSpies.setSelectedSuiteIndex).toHaveBeenCalled();
+ expect(actionSpies.fetchTestSuite).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
index 65bffe7039a..3a4aa94571e 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -23,6 +23,8 @@ describe('Test reports suite table', () => {
const noCasesMessage = () => wrapper.find('.js-no-test-cases');
const allCaseRows = () => wrapper.findAll('.js-case-row');
const findCaseRowAtIndex = index => wrapper.findAll('.js-case-row').at(index);
+ const allCaseNames = () =>
+ wrapper.findAll('[data-testid="caseName"]').wrappers.map(el => el.attributes('text'));
const findIconForRow = (row, status) => row.find(`.ci-status-icon-${status}`);
const createComponent = (suite = testSuite) => {
@@ -61,18 +63,14 @@ describe('Test reports suite table', () => {
expect(allCaseRows().length).toBe(testCases.length);
});
- it('renders the failed tests first', () => {
- const failedCaseNames = testCases
- .filter(x => x.status === TestStatus.FAILED)
- .map(x => x.name);
+ it('renders the failed tests first, skipped tests next, then successful tests', () => {
+ const expectedCaseOrder = [
+ ...testCases.filter(x => x.status === TestStatus.FAILED),
+ ...testCases.filter(x => x.status === TestStatus.SKIPPED),
+ ...testCases.filter(x => x.status === TestStatus.SUCCESS),
+ ].map(x => x.name);
- const skippedCaseNames = testCases
- .filter(x => x.status === TestStatus.SKIPPED)
- .map(x => x.name);
-
- expect(findCaseRowAtIndex(0).text()).toContain(failedCaseNames[0]);
- expect(findCaseRowAtIndex(1).text()).toContain(failedCaseNames[1]);
- expect(findCaseRowAtIndex(2).text()).toContain(skippedCaseNames[0]);
+ expect(allCaseNames()).toEqual(expectedCaseOrder);
});
it('renders the correct icon for each status', () => {
diff --git a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
index 650dd8a1def..2e32d62b4bd 100644
--- a/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_branch_name_token_spec.js
@@ -1,6 +1,6 @@
-import Api from '~/api';
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import Api from '~/api';
import PipelineBranchNameToken from '~/pipelines/components/pipelines_list/tokens/pipeline_branch_name_token.vue';
import { branches, mockBranchesAfterMap } from '../mock_data';
diff --git a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
index 15b283dc2ff..42c9dfc9ff0 100644
--- a/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_tag_name_token_spec.js
@@ -1,6 +1,6 @@
-import Api from '~/api';
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import Api from '~/api';
import PipelineTagNameToken from '~/pipelines/components/pipelines_list/tokens/pipeline_tag_name_token.vue';
import { tags, mockTagsAfterMap } from '../mock_data';
diff --git a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
index 0b5cf2e202b..c95d2ea1b7b 100644
--- a/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
+++ b/spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js
@@ -1,6 +1,6 @@
-import Api from '~/api';
import { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import Api from '~/api';
import PipelineTriggerAuthorToken from '~/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue';
import { users } from '../mock_data';
diff --git a/spec/frontend/project_find_file_spec.js b/spec/frontend/project_find_file_spec.js
index b4c6d202e14..757a02a04a3 100644
--- a/spec/frontend/project_find_file_spec.js
+++ b/spec/frontend/project_find_file_spec.js
@@ -1,11 +1,13 @@
import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { TEST_HOST } from 'helpers/test_constants';
-import sanitize from 'sanitize-html';
+import { sanitize } from 'dompurify';
import ProjectFindFile from '~/project_find_file';
import axios from '~/lib/utils/axios_utils';
-jest.mock('sanitize-html', () => jest.fn(val => val));
+jest.mock('dompurify', () => ({
+ sanitize: jest.fn(val => val),
+}));
const BLOB_URL_TEMPLATE = `${TEST_HOST}/namespace/project/blob/master`;
const FILE_FIND_URL = `${TEST_HOST}/namespace/project/files/master?format=json`;
diff --git a/spec/frontend/projects/commits/components/author_select_spec.js b/spec/frontend/projects/commits/components/author_select_spec.js
index dab91d8b37c..d6fac6f5f79 100644
--- a/spec/frontend/projects/commits/components/author_select_spec.js
+++ b/spec/frontend/projects/commits/components/author_select_spec.js
@@ -1,14 +1,14 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import * as urlUtility from '~/lib/utils/url_utility';
-import AuthorSelect from '~/projects/commits/components/author_select.vue';
-import { createStore } from '~/projects/commits/store';
import {
GlNewDropdown,
GlNewDropdownHeader,
GlSearchBoxByType,
GlNewDropdownItem,
} from '@gitlab/ui';
+import * as urlUtility from '~/lib/utils/url_utility';
+import AuthorSelect from '~/projects/commits/components/author_select.vue';
+import { createStore } from '~/projects/commits/store';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/projects/commits/store/actions_spec.js b/spec/frontend/projects/commits/store/actions_spec.js
index 886224252ad..a842aaa2a76 100644
--- a/spec/frontend/projects/commits/store/actions_spec.js
+++ b/spec/frontend/projects/commits/store/actions_spec.js
@@ -1,10 +1,10 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import * as types from '~/projects/commits/store/mutation_types';
import testAction from 'helpers/vuex_action_helper';
+import * as types from '~/projects/commits/store/mutation_types';
import actions from '~/projects/commits/store/actions';
import createState from '~/projects/commits/store/state';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
new file mode 100644
index 00000000000..44220bdef64
--- /dev/null
+++ b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
@@ -0,0 +1,83 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Project remove modal initialized matches the snapshot 1`] = `
+<form
+ action="some/path"
+ method="post"
+>
+ <input
+ name="_method"
+ type="hidden"
+ value="delete"
+ />
+
+ <input
+ name="authenticity_token"
+ type="hidden"
+ />
+
+ <gl-button-stub
+ category="primary"
+ icon=""
+ role="button"
+ size="medium"
+ tabindex="0"
+ variant="danger"
+ >
+ Delete project
+ </gl-button-stub>
+
+ <gl-modal-stub
+ actioncancel="[object Object]"
+ actionprimary="[object Object]"
+ footer-class="gl-bg-gray-10 gl-p-5"
+ modalclass=""
+ modalid="fakeUniqueId"
+ ok-variant="danger"
+ size="sm"
+ title-class="gl-text-red-500"
+ titletag="h4"
+ >
+
+ <div>
+ <gl-alert-stub
+ class="gl-mb-5"
+ dismisslabel="Dismiss"
+ primarybuttonlink=""
+ primarybuttontext=""
+ secondarybuttonlink=""
+ secondarybuttontext=""
+ title="You are about to permanently delete this project"
+ variant="danger"
+ >
+ <gl-sprintf-stub
+ message="Once a project is permanently deleted it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its respositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc."
+ />
+ </gl-alert-stub>
+
+ <p>
+ This action cannot be undone. You will lose the project's respository and all conent: issues, merge requests, etc.
+ </p>
+
+ <p
+ class="gl-mb-1"
+ >
+ Please type the following to confirm:
+ </p>
+
+ <p>
+ <code>
+ foo
+ </code>
+ </p>
+
+ <gl-form-input-stub
+ id="confirm_name_input"
+ name="confirm_name_input"
+ type="text"
+ />
+
+ </div>
+ </gl-modal-stub>
+</form>
+`;
diff --git a/spec/frontend/projects/components/__snapshots__/remove_modal_spec.js.snap b/spec/frontend/projects/components/__snapshots__/remove_modal_spec.js.snap
deleted file mode 100644
index 4d5b6c56a34..00000000000
--- a/spec/frontend/projects/components/__snapshots__/remove_modal_spec.js.snap
+++ /dev/null
@@ -1,126 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Project remove modal initialized matches the snapshot 1`] = `
-<form
- action="some/path"
- method="post"
->
- <input
- name="_method"
- type="hidden"
- value="delete"
- />
-
- <input
- name="authenticity_token"
- type="hidden"
- />
-
- <b-button-stub
- class="[object Object]"
- event="click"
- role="button"
- routertag="a"
- size="md"
- tabindex="0"
- tag="button"
- type="button"
- variant="danger"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
- Remove project
- </span>
- </b-button-stub>
-
- <b-modal-stub
- canceltitle="Cancel"
- cancelvariant="secondary"
- footerclass="bg-gray-light gl-p-5"
- headerclosecontent="&times;"
- headercloselabel="Close"
- id="remove-project-modal"
- ignoreenforcefocusselector=""
- lazy="true"
- modalclass="gl-modal,"
- oktitle="OK"
- okvariant="danger"
- size="sm"
- title=""
- titletag="h4"
- >
-
- <div>
- <p
- class="gl-text-red-500 gl-font-weight-bold"
- >
- This can lead to data loss.
- </p>
-
- <p
- class="gl-mb-0"
- >
- This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention.
- </p>
-
- <p>
- <gl-sprintf-stub
- message="Please type %{phrase_code} to proceed or close this modal to cancel."
- />
- </p>
-
- <gl-form-input-stub
- id="confirm_name_input"
- name="confirm_name_input"
- type="text"
- />
- </div>
-
- <template />
-
- <template>
- Confirmation required
- </template>
-
- <template />
-
- <template />
-
- <template />
-
- <template>
- <div
- class="gl-w-full gl-display-flex gl-just-content-start gl-m-0"
- >
- <b-button-stub
- class="[object Object]"
- disabled="true"
- event="click"
- routertag="a"
- size="md"
- tag="button"
- type="button"
- variant="danger"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
-
- Confirm
-
- </span>
- </b-button-stub>
- </div>
- </template>
- </b-modal-stub>
-</form>
-`;
diff --git a/spec/frontend/projects/components/project_delete_button_spec.js b/spec/frontend/projects/components/project_delete_button_spec.js
new file mode 100644
index 00000000000..444e465ebaa
--- /dev/null
+++ b/spec/frontend/projects/components/project_delete_button_spec.js
@@ -0,0 +1,47 @@
+import { shallowMount } from '@vue/test-utils';
+import ProjectDeleteButton from '~/projects/components/project_delete_button.vue';
+import SharedDeleteButton from '~/projects/components/shared/delete_button.vue';
+
+jest.mock('lodash/uniqueId', () => () => 'fakeUniqueId');
+
+describe('Project remove modal', () => {
+ let wrapper;
+
+ const findSharedDeleteButton = () => wrapper.find(SharedDeleteButton);
+
+ const defaultProps = {
+ confirmPhrase: 'foo',
+ formPath: 'some/path',
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(ProjectDeleteButton, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ SharedDeleteButton,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('initialized', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('matches the snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('passes confirmPhrase and formPath props to the shared delete button', () => {
+ expect(findSharedDeleteButton().props()).toEqual(defaultProps);
+ });
+ });
+});
diff --git a/spec/frontend/projects/components/remove_modal_spec.js b/spec/frontend/projects/components/remove_modal_spec.js
deleted file mode 100644
index 339aee65b99..00000000000
--- a/spec/frontend/projects/components/remove_modal_spec.js
+++ /dev/null
@@ -1,62 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlButton, GlModal } from '@gitlab/ui';
-import ProjectRemoveModal from '~/projects/components/remove_modal.vue';
-
-describe('Project remove modal', () => {
- let wrapper;
-
- const findFormElement = () => wrapper.find('form').element;
- const findConfirmButton = () => wrapper.find(GlModal).find(GlButton);
-
- const defaultProps = {
- formPath: 'some/path',
- confirmPhrase: 'foo',
- warningMessage: 'This can lead to data loss.',
- };
-
- const createComponent = (data = {}) => {
- wrapper = shallowMount(ProjectRemoveModal, {
- propsData: defaultProps,
- data: () => data,
- stubs: {
- GlButton,
- GlModal,
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- describe('initialized', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('matches the snapshot', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
- });
-
- describe('user input matches the confirmPhrase', () => {
- beforeEach(() => {
- createComponent({ userInput: defaultProps.confirmPhrase });
- });
-
- it('the confirm button is not dislabled', () => {
- expect(findConfirmButton().attributes('disabled')).toBe(undefined);
- });
-
- describe('and when the confirmation button is clicked', () => {
- beforeEach(() => {
- findConfirmButton().vm.$emit('click');
- });
-
- it('submits the form element', () => {
- expect(findFormElement().submit).toHaveBeenCalled();
- });
- });
- });
-});
diff --git a/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
new file mode 100644
index 00000000000..a43acc8c002
--- /dev/null
+++ b/spec/frontend/projects/components/shared/__snapshots__/delete_button_spec.js.snap
@@ -0,0 +1,113 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Project remove modal intialized matches the snapshot 1`] = `
+<form
+ action="some/path"
+ method="post"
+>
+ <input
+ name="_method"
+ type="hidden"
+ value="delete"
+ />
+
+ <input
+ name="authenticity_token"
+ type="hidden"
+ value="test-csrf-token"
+ />
+
+ <gl-button-stub
+ category="primary"
+ icon=""
+ role="button"
+ size="medium"
+ tabindex="0"
+ variant="danger"
+ >
+ Delete project
+ </gl-button-stub>
+
+ <b-modal-stub
+ canceltitle="Cancel"
+ cancelvariant="secondary"
+ footerclass="gl-bg-gray-10 gl-p-5"
+ headerclosecontent="&times;"
+ headercloselabel="Close"
+ id="delete-project-modal-2"
+ ignoreenforcefocusselector=""
+ lazy="true"
+ modalclass="gl-modal,"
+ oktitle="OK"
+ okvariant="danger"
+ size="sm"
+ title=""
+ titleclass="gl-text-red-500"
+ titletag="h4"
+ >
+
+ <div>
+
+ <p
+ class="gl-mb-1"
+ >
+ Please type the following to confirm:
+ </p>
+
+ <p>
+ <code>
+ foo
+ </code>
+ </p>
+
+ <gl-form-input-stub
+ id="confirm_name_input"
+ name="confirm_name_input"
+ type="text"
+ />
+
+ </div>
+
+ <template />
+
+ <template>
+ Delete project. Are you ABSOLUTELY SURE?
+ </template>
+
+ <template />
+
+ <template />
+
+ <template />
+
+ <template>
+ <gl-button-stub
+ category="primary"
+ class="js-modal-action-cancel"
+ icon=""
+ size="medium"
+ variant="default"
+ >
+
+ Cancel, keep project
+
+ </gl-button-stub>
+
+ <!---->
+
+ <gl-button-stub
+ category="primary"
+ class="js-modal-action-primary"
+ disabled="true"
+ icon=""
+ size="medium"
+ variant="danger"
+ >
+
+ Yes, delete project
+
+ </gl-button-stub>
+ </template>
+ </b-modal-stub>
+</form>
+`;
diff --git a/spec/frontend/projects/components/shared/delete_button_spec.js b/spec/frontend/projects/components/shared/delete_button_spec.js
new file mode 100644
index 00000000000..a6394a50011
--- /dev/null
+++ b/spec/frontend/projects/components/shared/delete_button_spec.js
@@ -0,0 +1,83 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
+import SharedDeleteButton from '~/projects/components/shared/delete_button.vue';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'test-csrf-token' }));
+
+describe('Project remove modal', () => {
+ let wrapper;
+
+ const findFormElement = () => wrapper.find('form');
+ const findConfirmButton = () => wrapper.find('.js-modal-action-primary');
+ const findAuthenticityTokenInput = () => findFormElement().find('input[name=authenticity_token]');
+ const findModal = () => wrapper.find(GlModal);
+
+ const defaultProps = {
+ confirmPhrase: 'foo',
+ formPath: 'some/path',
+ };
+
+ const createComponent = (data = {}) => {
+ wrapper = shallowMount(SharedDeleteButton, {
+ propsData: defaultProps,
+ data: () => data,
+ stubs: {
+ GlModal,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('intialized', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('matches the snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('sets a csrf token on the authenticity form input', () => {
+ expect(findAuthenticityTokenInput().element.value).toEqual('test-csrf-token');
+ });
+
+ it('sets the form action to the provided path', () => {
+ expect(findFormElement().attributes('action')).toEqual(defaultProps.formPath);
+ });
+ });
+
+ describe('when the user input does not match the confirmPhrase', () => {
+ beforeEach(() => {
+ createComponent({ userInput: 'bar' });
+ });
+
+ it('the confirm button is disabled', () => {
+ expect(findConfirmButton().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('when the user input matches the confirmPhrase', () => {
+ beforeEach(() => {
+ createComponent({ userInput: defaultProps.confirmPhrase });
+ });
+
+ it('the confirm button is not disabled', () => {
+ expect(findConfirmButton().attributes('disabled')).toBe(undefined);
+ });
+ });
+
+ describe('when the modal is confirmed', () => {
+ beforeEach(() => {
+ createComponent();
+ findModal().vm.$emit('ok');
+ });
+
+ it('submits the form element', () => {
+ expect(findFormElement().element.submit).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/projects/experiment_new_project_creation/components/legacy_container_spec.js b/spec/frontend/projects/experiment_new_project_creation/components/legacy_container_spec.js
index cd8b39f0426..42a7aa6bc88 100644
--- a/spec/frontend/projects/experiment_new_project_creation/components/legacy_container_spec.js
+++ b/spec/frontend/projects/experiment_new_project_creation/components/legacy_container_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import LegacyContainer from '~/projects/experiment_new_project_creation/components/legacy_container.vue';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import LegacyContainer from '~/projects/experiment_new_project_creation/components/legacy_container.vue';
describe('Legacy container component', () => {
let wrapper;
diff --git a/spec/frontend/projects/experiment_new_project_creation/components/welcome_spec.js b/spec/frontend/projects/experiment_new_project_creation/components/welcome_spec.js
index acd142fa5ba..cf23ba281f9 100644
--- a/spec/frontend/projects/experiment_new_project_creation/components/welcome_spec.js
+++ b/spec/frontend/projects/experiment_new_project_creation/components/welcome_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue';
import { mockTracking } from 'helpers/tracking_helper';
+import WelcomePage from '~/projects/experiment_new_project_creation/components/welcome.vue';
describe('Welcome page', () => {
let wrapper;
diff --git a/spec/frontend/projects/project_new_spec.js b/spec/frontend/projects/project_new_spec.js
index 7aafbd33fc8..c32979dcd74 100644
--- a/spec/frontend/projects/project_new_spec.js
+++ b/spec/frontend/projects/project_new_spec.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import projectNew from '~/projects/project_new';
import { TEST_HOST } from 'jest/helpers/test_constants';
+import projectNew from '~/projects/project_new';
describe('New Project', () => {
let $projectImportUrl;
diff --git a/spec/frontend/projects/settings/access_dropdown_spec.js b/spec/frontend/projects/settings/access_dropdown_spec.js
new file mode 100644
index 00000000000..6d323b0408b
--- /dev/null
+++ b/spec/frontend/projects/settings/access_dropdown_spec.js
@@ -0,0 +1,140 @@
+import $ from 'jquery';
+import '~/gl_dropdown';
+import AccessDropdown from '~/projects/settings/access_dropdown';
+import { LEVEL_TYPES } from '~/projects/settings/constants';
+
+describe('AccessDropdown', () => {
+ const defaultLabel = 'dummy default label';
+ let dropdown;
+
+ beforeEach(() => {
+ setFixtures(`
+ <div id="dummy-dropdown">
+ <span class="dropdown-toggle-text"></span>
+ </div>
+ `);
+ const $dropdown = $('#dummy-dropdown');
+ $dropdown.data('defaultLabel', defaultLabel);
+ const options = {
+ $dropdown,
+ accessLevelsData: {
+ roles: [
+ {
+ id: 42,
+ text: 'Dummy Role',
+ },
+ ],
+ },
+ };
+ dropdown = new AccessDropdown(options);
+ });
+
+ describe('toggleLabel', () => {
+ let $dropdownToggleText;
+ const dummyItems = [
+ { type: LEVEL_TYPES.ROLE, access_level: 42 },
+ { type: LEVEL_TYPES.USER },
+ { type: LEVEL_TYPES.USER },
+ { type: LEVEL_TYPES.GROUP },
+ { type: LEVEL_TYPES.GROUP },
+ { type: LEVEL_TYPES.GROUP },
+ ];
+
+ beforeEach(() => {
+ $dropdownToggleText = $('.dropdown-toggle-text');
+ });
+
+ it('displays number of items', () => {
+ dropdown.setSelectedItems(dummyItems);
+ $dropdownToggleText.addClass('is-default');
+
+ const label = dropdown.toggleLabel();
+
+ expect(label).toBe('1 role, 2 users, 3 groups');
+ expect($dropdownToggleText).not.toHaveClass('is-default');
+ });
+
+ describe('without selected items', () => {
+ beforeEach(() => {
+ dropdown.setSelectedItems([]);
+ });
+
+ it('falls back to default label', () => {
+ const label = dropdown.toggleLabel();
+
+ expect(label).toBe(defaultLabel);
+ expect($dropdownToggleText).toHaveClass('is-default');
+ });
+ });
+
+ describe('with only role', () => {
+ beforeEach(() => {
+ dropdown.setSelectedItems(dummyItems.filter(item => item.type === LEVEL_TYPES.ROLE));
+ $dropdownToggleText.addClass('is-default');
+ });
+
+ it('displays the role name', () => {
+ const label = dropdown.toggleLabel();
+
+ expect(label).toBe('Dummy Role');
+ expect($dropdownToggleText).not.toHaveClass('is-default');
+ });
+ });
+
+ describe('with only users', () => {
+ beforeEach(() => {
+ dropdown.setSelectedItems(dummyItems.filter(item => item.type === LEVEL_TYPES.USER));
+ $dropdownToggleText.addClass('is-default');
+ });
+
+ it('displays number of users', () => {
+ const label = dropdown.toggleLabel();
+
+ expect(label).toBe('2 users');
+ expect($dropdownToggleText).not.toHaveClass('is-default');
+ });
+ });
+
+ describe('with only groups', () => {
+ beforeEach(() => {
+ dropdown.setSelectedItems(dummyItems.filter(item => item.type === LEVEL_TYPES.GROUP));
+ $dropdownToggleText.addClass('is-default');
+ });
+
+ it('displays number of groups', () => {
+ const label = dropdown.toggleLabel();
+
+ expect(label).toBe('3 groups');
+ expect($dropdownToggleText).not.toHaveClass('is-default');
+ });
+ });
+
+ describe('with users and groups', () => {
+ beforeEach(() => {
+ const selectedTypes = [LEVEL_TYPES.GROUP, LEVEL_TYPES.USER];
+ dropdown.setSelectedItems(dummyItems.filter(item => selectedTypes.includes(item.type)));
+ $dropdownToggleText.addClass('is-default');
+ });
+
+ it('displays number of groups', () => {
+ const label = dropdown.toggleLabel();
+
+ expect(label).toBe('2 users, 3 groups');
+ expect($dropdownToggleText).not.toHaveClass('is-default');
+ });
+ });
+ });
+
+ describe('userRowHtml', () => {
+ it('escapes users name', () => {
+ const user = {
+ avatar_url: '',
+ name: '<img src=x onerror=alert(document.domain)>',
+ username: 'test',
+ };
+ const template = dropdown.userRowHtml(user);
+
+ expect(template).not.toContain(user.name);
+ });
+ });
+});
diff --git a/spec/frontend/prometheus_alerts/components/reset_key_spec.js b/spec/frontend/prometheus_alerts/components/reset_key_spec.js
index df52baafa29..489586a60fe 100644
--- a/spec/frontend/prometheus_alerts/components/reset_key_spec.js
+++ b/spec/frontend/prometheus_alerts/components/reset_key_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import ResetKey from '~/prometheus_alerts/components/reset_key.vue';
import { GlModal } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
+import ResetKey from '~/prometheus_alerts/components/reset_key.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import axios from '~/lib/utils/axios_utils';
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index 2688e4b3428..1556f5b19dc 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -26,12 +26,14 @@ describe('Ref selector component', () => {
let tagsApiCallSpy;
let commitApiCallSpy;
- const createComponent = () => {
+ const createComponent = (props = {}, attrs = {}) => {
wrapper = mount(RefSelector, {
propsData: {
projectId,
value: '',
+ ...props,
},
+ attrs,
listeners: {
// simulate a parent component v-model binding
input: selectedRef => {
@@ -163,6 +165,52 @@ describe('Ref selector component', () => {
});
describe('post-initialization behavior', () => {
+ describe('when the parent component provides an `id` binding', () => {
+ const id = 'git-ref';
+
+ beforeEach(() => {
+ createComponent({}, { id });
+
+ return waitForRequests();
+ });
+
+ it('adds the provided ID to the GlNewDropdown instance', () => {
+ expect(wrapper.attributes().id).toBe(id);
+ });
+ });
+
+ describe('when a ref is pre-selected', () => {
+ const preselectedRef = fixtures.branches[0].name;
+
+ beforeEach(() => {
+ createComponent({ value: preselectedRef });
+
+ return waitForRequests();
+ });
+
+ it('renders the pre-selected ref name', () => {
+ expect(findButtonContent().text()).toBe(preselectedRef);
+ });
+ });
+
+ describe('when the selected ref is updated by the parent component', () => {
+ const updatedRef = fixtures.branches[0].name;
+
+ beforeEach(() => {
+ createComponent();
+
+ return waitForRequests();
+ });
+
+ it('renders the updated ref name', () => {
+ wrapper.setProps({ value: updatedRef });
+
+ return localVue.nextTick().then(() => {
+ expect(findButtonContent().text()).toBe(updatedRef);
+ });
+ });
+ });
+
describe('when the search query is updated', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/registry/explorer/components/details_page/details_row_spec.js b/spec/frontend/registry/explorer/components/details_page/details_row_spec.js
deleted file mode 100644
index 95b8e18d677..00000000000
--- a/spec/frontend/registry/explorer/components/details_page/details_row_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlIcon } from '@gitlab/ui';
-import component from '~/registry/explorer/components/details_page/details_row.vue';
-
-describe('DetailsRow', () => {
- let wrapper;
-
- const findIcon = () => wrapper.find(GlIcon);
- const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
-
- const mountComponent = () => {
- wrapper = shallowMount(component, {
- propsData: {
- icon: 'clock',
- },
- slots: {
- default: '<div data-testid="default-slot"></div>',
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('contains an icon', () => {
- mountComponent();
- expect(findIcon().exists()).toBe(true);
- });
-
- it('icon has the correct props', () => {
- mountComponent();
- expect(findIcon().props()).toMatchObject({
- name: 'clock',
- });
- });
-
- it('has a default slot', () => {
- mountComponent();
- expect(findDefaultSlot().exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
index 9e876d6d8a3..a21facefc97 100644
--- a/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/registry/explorer/components/details_page/tags_list_row_spec.js
@@ -1,11 +1,12 @@
import { shallowMount } from '@vue/test-utils';
import { GlFormCheckbox, GlSprintf, GlIcon } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import component from '~/registry/explorer/components/details_page/tags_list_row.vue';
import DeleteButton from '~/registry/explorer/components/delete_button.vue';
-import DetailsRow from '~/registry/explorer/components/details_page/details_row.vue';
+import DetailsRow from '~/registry/shared/components/details_row.vue';
import {
REMOVE_TAG_BUTTON_TITLE,
REMOVE_TAG_BUTTON_DISABLE_TOOLTIP,
@@ -13,7 +14,6 @@ import {
NOT_AVAILABLE_TEXT,
NOT_AVAILABLE_SIZE,
} from '~/registry/explorer/constants/index';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { tagsListResponse } from '../../mock_data';
import { ListItem } from '../../stubs';
diff --git a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js b/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
index a556be12089..b0291de5f3c 100644
--- a/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/cli_commands_spec.js
@@ -1,6 +1,6 @@
import Vuex from 'vuex';
import { mount, createLocalVue } from '@vue/test-utils';
-import { GlDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlFormGroup, GlFormInputGroup } from '@gitlab/ui';
import Tracking from '~/tracking';
import * as getters from '~/registry/explorer/stores/getters';
import QuickstartDropdown from '~/registry/explorer/components/list_page/cli_commands.vue';
@@ -23,7 +23,7 @@ describe('cli_commands', () => {
let wrapper;
let store;
- const findDropdownButton = () => wrapper.find(GlDropdown);
+ const findDropdownButton = () => wrapper.find(GlDeprecatedDropdown);
const findFormGroups = () => wrapper.findAll(GlFormGroup);
const mountComponent = () => {
diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js
index 9bc0bae5c23..66e8a4aea0d 100644
--- a/spec/frontend/registry/explorer/pages/details_spec.js
+++ b/spec/frontend/registry/explorer/pages/details_spec.js
@@ -13,7 +13,7 @@ import {
SET_TAGS_LIST_SUCCESS,
SET_TAGS_PAGINATION,
SET_INITIAL_STATE,
-} from '~/registry/explorer/stores/mutation_types/';
+} from '~/registry/explorer/stores/mutation_types';
import { tagsListResponse } from '../mock_data';
import { DeleteModal } from '../stubs';
diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js
index 2ece7593b41..b4e46fda2c4 100644
--- a/spec/frontend/registry/explorer/pages/list_spec.js
+++ b/spec/frontend/registry/explorer/pages/list_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlSkeletonLoader, GlSprintf, GlAlert, GlSearchBoxByClick } from '@gitlab/ui';
-import Tracking from '~/tracking';
import waitForPromises from 'helpers/wait_for_promises';
+import Tracking from '~/tracking';
import component from '~/registry/explorer/pages/list.vue';
import CliCommands from '~/registry/explorer/components/list_page/cli_commands.vue';
import GroupEmptyState from '~/registry/explorer/components/list_page/group_empty_state.vue';
@@ -14,7 +14,7 @@ import {
SET_IMAGES_LIST_SUCCESS,
SET_PAGINATION,
SET_INITIAL_STATE,
-} from '~/registry/explorer/stores/mutation_types/';
+} from '~/registry/explorer/stores/mutation_types';
import {
DELETE_IMAGE_SUCCESS_MESSAGE,
DELETE_IMAGE_ERROR_MESSAGE,
diff --git a/spec/frontend/registry/explorer/stores/actions_spec.js b/spec/frontend/registry/explorer/stores/actions_spec.js
index 15f9db90910..fb93ab06ca8 100644
--- a/spec/frontend/registry/explorer/stores/actions_spec.js
+++ b/spec/frontend/registry/explorer/stores/actions_spec.js
@@ -1,10 +1,10 @@
-import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
-import * as actions from '~/registry/explorer/stores/actions';
-import * as types from '~/registry/explorer/stores/mutation_types';
import testAction from 'helpers/vuex_action_helper';
-import createFlash from '~/flash';
import { TEST_HOST } from 'helpers/test_constants';
+import axios from '~/lib/utils/axios_utils';
+import * as actions from '~/registry/explorer/stores/actions';
+import * as types from '~/registry/explorer/stores/mutation_types';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { reposServerResponse, registryServerResponse } from '../mock_data';
jest.mock('~/flash.js');
diff --git a/spec/frontend/registry/settings/components/settings_form_spec.js b/spec/frontend/registry/settings/components/settings_form_spec.js
index 9b9ca92270c..6f9518808db 100644
--- a/spec/frontend/registry/settings/components/settings_form_spec.js
+++ b/spec/frontend/registry/settings/components/settings_form_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
import Tracking from '~/tracking';
import component from '~/registry/settings/components/settings_form.vue';
import expirationPolicyFields from '~/registry/shared/components/expiration_policy_fields.vue';
@@ -7,7 +8,6 @@ import {
UPDATE_SETTINGS_ERROR_MESSAGE,
UPDATE_SETTINGS_SUCCESS_MESSAGE,
} from '~/registry/shared/constants';
-import waitForPromises from 'helpers/wait_for_promises';
import { stringifiedFormOptions } from '../../shared/mock_data';
describe('Settings Form', () => {
diff --git a/spec/frontend/registry/settings/store/actions_spec.js b/spec/frontend/registry/settings/store/actions_spec.js
index f92d10d087f..51b89f96ef2 100644
--- a/spec/frontend/registry/settings/store/actions_spec.js
+++ b/spec/frontend/registry/settings/store/actions_spec.js
@@ -1,5 +1,5 @@
-import Api from '~/api';
import testAction from 'helpers/vuex_action_helper';
+import Api from '~/api';
import * as actions from '~/registry/settings/store/actions';
import * as types from '~/registry/settings/store/mutation_types';
diff --git a/spec/frontend/registry/shared/components/details_row_spec.js b/spec/frontend/registry/shared/components/details_row_spec.js
new file mode 100644
index 00000000000..5ae4e0ab37f
--- /dev/null
+++ b/spec/frontend/registry/shared/components/details_row_spec.js
@@ -0,0 +1,71 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import component from '~/registry/shared/components/details_row.vue';
+
+describe('DetailsRow', () => {
+ let wrapper;
+
+ const findIcon = () => wrapper.find(GlIcon);
+ const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
+
+ const mountComponent = props => {
+ wrapper = shallowMount(component, {
+ propsData: {
+ icon: 'clock',
+ ...props,
+ },
+ slots: {
+ default: '<div data-testid="default-slot"></div>',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('has a default slot', () => {
+ mountComponent();
+ expect(findDefaultSlot().exists()).toBe(true);
+ });
+
+ describe('icon prop', () => {
+ it('contains an icon', () => {
+ mountComponent();
+ expect(findIcon().exists()).toBe(true);
+ });
+
+ it('icon has the correct props', () => {
+ mountComponent();
+ expect(findIcon().props()).toMatchObject({
+ name: 'clock',
+ });
+ });
+ });
+
+ describe('padding prop', () => {
+ it('padding has a default', () => {
+ mountComponent();
+ expect(wrapper.classes('gl-py-2')).toBe(true);
+ });
+
+ it('is reflected in the template', () => {
+ mountComponent({ padding: 'gl-py-4' });
+ expect(wrapper.classes('gl-py-4')).toBe(true);
+ });
+ });
+
+ describe('dashed prop', () => {
+ const borderClasses = ['gl-border-b-solid', 'gl-border-gray-100', 'gl-border-b-1'];
+ it('by default component has no border', () => {
+ mountComponent();
+ expect(wrapper.classes).not.toEqual(expect.arrayContaining(borderClasses));
+ });
+
+ it('has a border when dashed is true', () => {
+ mountComponent({ dashed: true });
+ expect(wrapper.classes()).toEqual(expect.arrayContaining(borderClasses));
+ });
+ });
+});
diff --git a/spec/frontend/related_merge_requests/store/actions_spec.js b/spec/frontend/related_merge_requests/store/actions_spec.js
index 26c5977cb5f..fa031a91c83 100644
--- a/spec/frontend/related_merge_requests/store/actions_spec.js
+++ b/spec/frontend/related_merge_requests/store/actions_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
-import createFlash from '~/flash';
import testAction from 'helpers/vuex_action_helper';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as types from '~/related_merge_requests/store/mutation_types';
import * as actions from '~/related_merge_requests/store/actions';
diff --git a/spec/frontend/releases/components/app_edit_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 4450b047acd..e9727801c1a 100644
--- a/spec/frontend/releases/components/app_edit_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -1,15 +1,15 @@
import Vuex from 'vuex';
import { mount } from '@vue/test-utils';
-import ReleaseEditApp from '~/releases/components/app_edit.vue';
+import { merge } from 'lodash';
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
import { release as originalRelease, milestones as originalMilestones } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
import { BACK_URL_PARAM } from '~/releases/constants';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
-import { merge } from 'lodash';
-import axios from 'axios';
-import MockAdapter from 'axios-mock-adapter';
-describe('Release edit component', () => {
+describe('Release edit/new component', () => {
let wrapper;
let release;
let actions;
@@ -27,13 +27,14 @@ describe('Release edit component', () => {
};
actions = {
- fetchRelease: jest.fn(),
- updateRelease: jest.fn(),
+ initializeRelease: jest.fn(),
+ saveRelease: jest.fn(),
addEmptyAssetLink: jest.fn(),
};
getters = {
isValid: () => true,
+ isExistingRelease: () => true,
validationErrors: () => ({
assets: {
links: [],
@@ -57,12 +58,14 @@ describe('Release edit component', () => {
),
);
- wrapper = mount(ReleaseEditApp, {
+ wrapper = mount(ReleaseEditNewApp, {
store,
provide: {
glFeatures: featureFlags,
},
});
+
+ wrapper.element.querySelectorAll('input').forEach(input => jest.spyOn(input, 'focus'));
};
beforeEach(() => {
@@ -80,14 +83,23 @@ describe('Release edit component', () => {
});
const findSubmitButton = () => wrapper.find('button[type=submit]');
+ const findForm = () => wrapper.find('form');
describe(`basic functionality tests: all tests unrelated to the "${BACK_URL_PARAM}" parameter`, () => {
- beforeEach(() => {
- factory();
+ beforeEach(factory);
+
+ it('calls initializeRelease when the component is created', () => {
+ expect(actions.initializeRelease).toHaveBeenCalledTimes(1);
});
- it('calls fetchRelease when the component is created', () => {
- expect(actions.fetchRelease).toHaveBeenCalledTimes(1);
+ it('focuses the first non-disabled input element once the page is shown', () => {
+ const firstEnabledInput = wrapper.element.querySelector('input:enabled');
+ const allInputs = wrapper.element.querySelectorAll('input');
+
+ allInputs.forEach(input => {
+ const expectedFocusCalls = input === firstEnabledInput ? 1 : 0;
+ expect(input.focus).toHaveBeenCalledTimes(expectedFocusCalls);
+ });
});
it('renders the description text at the top of the page', () => {
@@ -96,28 +108,6 @@ describe('Release edit component', () => {
);
});
- it('renders the correct tag name in the "Tag name" field', () => {
- expect(wrapper.find('#git-ref').element.value).toBe(release.tagName);
- });
-
- it('renders the correct help text under the "Tag name" field', () => {
- const helperText = wrapper.find('#tag-name-help');
- const helperTextLink = helperText.find('a');
- const helperTextLinkAttrs = helperTextLink.attributes();
-
- expect(helperText.text()).toBe(
- 'Changing a Release tag is only supported via Releases API. More information',
- );
- expect(helperTextLink.text()).toBe('More information');
- expect(helperTextLinkAttrs).toEqual(
- expect.objectContaining({
- href: state.updateReleaseApiDocsPath,
- rel: 'noopener noreferrer',
- target: '_blank',
- }),
- );
- });
-
it('renders the correct release title in the "Release title" field', () => {
expect(wrapper.find('#release-title').element.value).toBe(release.name);
});
@@ -130,16 +120,15 @@ describe('Release edit component', () => {
expect(findSubmitButton().attributes('type')).toBe('submit');
});
- it('calls updateRelease when the form is submitted', () => {
- wrapper.find('form').trigger('submit');
- expect(actions.updateRelease).toHaveBeenCalledTimes(1);
+ it('calls saveRelease when the form is submitted', () => {
+ findForm().trigger('submit');
+
+ expect(actions.saveRelease).toHaveBeenCalledTimes(1);
});
});
describe(`when the URL does not contain a "${BACK_URL_PARAM}" parameter`, () => {
- beforeEach(() => {
- factory();
- });
+ beforeEach(factory);
it(`renders a "Cancel" button with an href pointing to "${BACK_URL_PARAM}"`, () => {
const cancelButton = wrapper.find('.js-cancel-button');
@@ -164,6 +153,34 @@ describe('Release edit component', () => {
});
});
+ describe('when creating a new release', () => {
+ beforeEach(() => {
+ factory({
+ store: {
+ modules: {
+ detail: {
+ getters: {
+ isExistingRelease: () => false,
+ },
+ },
+ },
+ },
+ });
+ });
+
+ it('renders the submit button with the text "Create release"', () => {
+ expect(findSubmitButton().text()).toBe('Create release');
+ });
+ });
+
+ describe('when editing an existing release', () => {
+ beforeEach(factory);
+
+ it('renders the submit button with the text "Save changes"', () => {
+ expect(findSubmitButton().text()).toBe('Save changes');
+ });
+ });
+
describe('asset links form', () => {
const findAssetLinksForm = () => wrapper.find(AssetLinksForm);
@@ -227,6 +244,12 @@ describe('Release edit component', () => {
it('renders the submit button as disabled', () => {
expect(findSubmitButton().attributes('disabled')).toBe('disabled');
});
+
+ it('does not allow the form to be submitted', () => {
+ findForm().trigger('submit');
+
+ expect(actions.saveRelease).not.toHaveBeenCalled();
+ });
});
});
});
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index 91beb5b1418..8eafe07cb2f 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -1,6 +1,7 @@
import { range as rge } from 'lodash';
import Vue from 'vue';
import { mountComponentWithStore } from 'helpers/vue_mount_component_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import app from '~/releases/components/app_index.vue';
import createStore from '~/releases/stores';
import listModule from '~/releases/stores/modules/list';
@@ -13,7 +14,6 @@ import {
releases,
} from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import waitForPromises from 'helpers/wait_for_promises';
describe('Releases App ', () => {
const Component = Vue.extend(app);
diff --git a/spec/frontend/releases/components/app_new_spec.js b/spec/frontend/releases/components/app_new_spec.js
deleted file mode 100644
index 0d5664766e5..00000000000
--- a/spec/frontend/releases/components/app_new_spec.js
+++ /dev/null
@@ -1,26 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import { mount } from '@vue/test-utils';
-import ReleaseNewApp from '~/releases/components/app_new.vue';
-
-Vue.use(Vuex);
-
-describe('Release new component', () => {
- let wrapper;
-
- const factory = () => {
- const store = new Vuex.Store();
- wrapper = mount(ReleaseNewApp, { store });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('renders the app', () => {
- factory();
-
- expect(wrapper.exists()).toBe(true);
- });
-});
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index 3dc9964c25c..e757fe98661 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -1,8 +1,8 @@
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
+import { GlSkeletonLoading } from '@gitlab/ui';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import { release as originalRelease } from '../mock_data';
-import { GlSkeletonLoading } from '@gitlab/ui';
import ReleaseBlock from '~/releases/components/release_block.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
diff --git a/spec/frontend/releases/components/asset_links_form_spec.js b/spec/frontend/releases/components/asset_links_form_spec.js
index e1f8592270e..727d593d851 100644
--- a/spec/frontend/releases/components/asset_links_form_spec.js
+++ b/spec/frontend/releases/components/asset_links_form_spec.js
@@ -3,6 +3,7 @@ import { mount, createLocalVue } from '@vue/test-utils';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import { release as originalRelease } from '../mock_data';
import * as commonUtils from '~/lib/utils/common_utils';
+import { ENTER_KEY } from '~/lib/utils/keys';
import { ASSET_LINK_TYPE, DEFAULT_ASSET_LINK_TYPE } from '~/releases/constants';
const localVue = createLocalVue();
@@ -91,42 +92,128 @@ describe('Release edit component', () => {
expect(actions.removeAssetLink).toHaveBeenCalledTimes(1);
});
- it('calls the "updateAssetLinkUrl" store method when text is entered into the "URL" input field', () => {
- const linkIdToUpdate = release.assets.links[0].id;
- const newUrl = 'updated url';
+ describe('URL input field', () => {
+ let input;
+ let linkIdToUpdate;
+ let newUrl;
- expect(actions.updateAssetLinkUrl).not.toHaveBeenCalled();
+ beforeEach(() => {
+ input = wrapper.find({ ref: 'urlInput' }).element;
+ linkIdToUpdate = release.assets.links[0].id;
+ newUrl = 'updated url';
+ });
- wrapper.find({ ref: 'urlInput' }).vm.$emit('change', newUrl);
+ const expectStoreMethodNotToBeCalled = () => {
+ expect(actions.updateAssetLinkUrl).not.toHaveBeenCalled();
+ };
- expect(actions.updateAssetLinkUrl).toHaveBeenCalledTimes(1);
- expect(actions.updateAssetLinkUrl).toHaveBeenCalledWith(
- expect.anything(),
- {
- linkIdToUpdate,
- newUrl,
- },
- undefined,
- );
+ const dispatchKeydowEvent = eventParams => {
+ const event = new KeyboardEvent('keydown', eventParams);
+
+ input.dispatchEvent(event);
+ };
+
+ const expectStoreMethodToBeCalled = () => {
+ expect(actions.updateAssetLinkUrl).toHaveBeenCalledTimes(1);
+ expect(actions.updateAssetLinkUrl).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ linkIdToUpdate,
+ newUrl,
+ },
+ undefined,
+ );
+ };
+
+ it('calls the "updateAssetLinkUrl" store method when text is entered into the "URL" input field', () => {
+ expectStoreMethodNotToBeCalled();
+
+ wrapper.find({ ref: 'urlInput' }).vm.$emit('change', newUrl);
+
+ expectStoreMethodToBeCalled();
+ });
+
+ it('calls the "updateAssetLinkUrl" store method when Ctrl+Enter is pressed inside the "URL" input field', () => {
+ expectStoreMethodNotToBeCalled();
+
+ input.value = newUrl;
+
+ dispatchKeydowEvent({ key: ENTER_KEY, ctrlKey: true });
+
+ expectStoreMethodToBeCalled();
+ });
+
+ it('calls the "updateAssetLinkUrl" store method when Cmd+Enter is pressed inside the "URL" input field', () => {
+ expectStoreMethodNotToBeCalled();
+
+ input.value = newUrl;
+
+ dispatchKeydowEvent({ key: ENTER_KEY, metaKey: true });
+
+ expectStoreMethodToBeCalled();
+ });
});
- it('calls the "updateAssetLinkName" store method when text is entered into the "Link title" input field', () => {
- const linkIdToUpdate = release.assets.links[0].id;
- const newName = 'updated name';
+ describe('Link title field', () => {
+ let input;
+ let linkIdToUpdate;
+ let newName;
- expect(actions.updateAssetLinkName).not.toHaveBeenCalled();
+ beforeEach(() => {
+ input = wrapper.find({ ref: 'nameInput' }).element;
+ linkIdToUpdate = release.assets.links[0].id;
+ newName = 'updated name';
+ });
- wrapper.find({ ref: 'nameInput' }).vm.$emit('change', newName);
+ const expectStoreMethodNotToBeCalled = () => {
+ expect(actions.updateAssetLinkUrl).not.toHaveBeenCalled();
+ };
- expect(actions.updateAssetLinkName).toHaveBeenCalledTimes(1);
- expect(actions.updateAssetLinkName).toHaveBeenCalledWith(
- expect.anything(),
- {
- linkIdToUpdate,
- newName,
- },
- undefined,
- );
+ const dispatchKeydowEvent = eventParams => {
+ const event = new KeyboardEvent('keydown', eventParams);
+
+ input.dispatchEvent(event);
+ };
+
+ const expectStoreMethodToBeCalled = () => {
+ expect(actions.updateAssetLinkName).toHaveBeenCalledTimes(1);
+ expect(actions.updateAssetLinkName).toHaveBeenCalledWith(
+ expect.anything(),
+ {
+ linkIdToUpdate,
+ newName,
+ },
+ undefined,
+ );
+ };
+
+ it('calls the "updateAssetLinkName" store method when text is entered into the "Link title" input field', () => {
+ expectStoreMethodNotToBeCalled();
+
+ wrapper.find({ ref: 'nameInput' }).vm.$emit('change', newName);
+
+ expectStoreMethodToBeCalled();
+ });
+
+ it('calls the "updateAssetLinkName" store method when Ctrl+Enter is pressed inside the "Link title" input field', () => {
+ expectStoreMethodNotToBeCalled();
+
+ input.value = newName;
+
+ dispatchKeydowEvent({ key: ENTER_KEY, ctrlKey: true });
+
+ expectStoreMethodToBeCalled();
+ });
+
+ it('calls the "updateAssetLinkName" store method when Cmd+Enter is pressed inside the "Link title" input field', () => {
+ expectStoreMethodNotToBeCalled();
+
+ input.value = newName;
+
+ dispatchKeydowEvent({ key: ENTER_KEY, metaKey: true });
+
+ expectStoreMethodToBeCalled();
+ });
});
it('calls the "updateAssetLinkType" store method when an option is selected from the "Type" dropdown', () => {
diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js
index a85532a8118..5e84290716c 100644
--- a/spec/frontend/releases/components/release_block_assets_spec.js
+++ b/spec/frontend/releases/components/release_block_assets_spec.js
@@ -1,10 +1,10 @@
import { mount } from '@vue/test-utils';
import { GlCollapse } from '@gitlab/ui';
+import { trimText } from 'helpers/text_helper';
+import { cloneDeep } from 'lodash';
import ReleaseBlockAssets from '~/releases/components/release_block_assets.vue';
import { ASSET_LINK_TYPE } from '~/releases/constants';
-import { trimText } from 'helpers/text_helper';
import { assets } from '../mock_data';
-import { cloneDeep } from 'lodash';
describe('Release block assets', () => {
let wrapper;
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index b91cfb82b65..c066bfbf020 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -1,11 +1,11 @@
import { mount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import { trimText } from 'helpers/text_helper';
+import { cloneDeep } from 'lodash';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import Icon from '~/vue_shared/components/icon.vue';
import { release as originalRelease } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { cloneDeep } from 'lodash';
const mockFutureDate = new Date(9999, 0, 0).toISOString();
let mockIsFutureRelease = false;
diff --git a/spec/frontend/releases/components/release_block_metadata_spec.js b/spec/frontend/releases/components/release_block_metadata_spec.js
index cbe478bfa1f..6f184e45600 100644
--- a/spec/frontend/releases/components/release_block_metadata_spec.js
+++ b/spec/frontend/releases/components/release_block_metadata_spec.js
@@ -1,9 +1,9 @@
import { mount } from '@vue/test-utils';
import { trimText } from 'helpers/text_helper';
+import { cloneDeep } from 'lodash';
import ReleaseBlockMetadata from '~/releases/components/release_block_metadata.vue';
import { release as originalRelease } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { cloneDeep } from 'lodash';
const mockFutureDate = new Date(9999, 0, 0).toISOString();
let mockIsFutureRelease = false;
diff --git a/spec/frontend/releases/components/tag_field_exsting_spec.js b/spec/frontend/releases/components/tag_field_exsting_spec.js
new file mode 100644
index 00000000000..0a04f68bd67
--- /dev/null
+++ b/spec/frontend/releases/components/tag_field_exsting_spec.js
@@ -0,0 +1,78 @@
+import { GlFormInput } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
+import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
+import createStore from '~/releases/stores';
+import createDetailModule from '~/releases/stores/modules/detail';
+
+const TEST_TAG_NAME = 'test-tag-name';
+const TEST_DOCS_PATH = '/help/test/docs/path';
+
+describe('releases/components/tag_field_existing', () => {
+ let store;
+ let wrapper;
+
+ const createComponent = (mountFn = shallowMount) => {
+ wrapper = mountFn(TagFieldExisting, {
+ store,
+ });
+ };
+
+ const findInput = () => wrapper.find(GlFormInput);
+ const findHelp = () => wrapper.find('[data-testid="tag-name-help"]');
+ const findHelpLink = () => {
+ const link = findHelp().find('a');
+
+ return {
+ text: link.text(),
+ href: link.attributes('href'),
+ target: link.attributes('target'),
+ };
+ };
+
+ beforeEach(() => {
+ store = createStore({
+ modules: {
+ detail: createDetailModule({
+ updateReleaseApiDocsPath: TEST_DOCS_PATH,
+ tagName: TEST_TAG_NAME,
+ }),
+ },
+ });
+
+ store.state.detail.release = {
+ tagName: TEST_TAG_NAME,
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('default', () => {
+ it('shows the tag name', () => {
+ createComponent();
+
+ expect(findInput().attributes()).toMatchObject({
+ disabled: '',
+ value: TEST_TAG_NAME,
+ });
+ });
+
+ it('shows help', () => {
+ createComponent(mount);
+
+ expect(findHelp().text()).toMatchInterpolatedText(
+ 'Changing a Release tag is only supported via Releases API. More information',
+ );
+
+ const helpLink = findHelpLink();
+
+ expect(helpLink).toEqual({
+ text: 'More information',
+ href: TEST_DOCS_PATH,
+ target: '_blank',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
new file mode 100644
index 00000000000..b6ebc496f33
--- /dev/null
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -0,0 +1,144 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { GlFormInput } from '@gitlab/ui';
+import TagFieldNew from '~/releases/components/tag_field_new.vue';
+import createStore from '~/releases/stores';
+import createDetailModule from '~/releases/stores/modules/detail';
+import RefSelector from '~/ref/components/ref_selector.vue';
+
+const TEST_TAG_NAME = 'test-tag-name';
+const TEST_PROJECT_ID = '1234';
+const TEST_CREATE_FROM = 'test-create-from';
+
+describe('releases/components/tag_field_new', () => {
+ let store;
+ let wrapper;
+
+ const createComponent = (mountFn = shallowMount) => {
+ wrapper = mountFn(TagFieldNew, {
+ store,
+ stubs: {
+ RefSelector: true,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ store = createStore({
+ modules: {
+ detail: createDetailModule({
+ projectId: TEST_PROJECT_ID,
+ }),
+ },
+ });
+
+ store.state.detail.createFrom = TEST_CREATE_FROM;
+
+ store.state.detail.release = {
+ tagName: TEST_TAG_NAME,
+ assets: {
+ links: [],
+ },
+ };
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findTagNameFormGroup = () => wrapper.find('[data-testid="tag-name-field"]');
+ const findTagNameGlInput = () => findTagNameFormGroup().find(GlFormInput);
+ const findTagNameInput = () => findTagNameFormGroup().find('input');
+
+ const findCreateFromFormGroup = () => wrapper.find('[data-testid="create-from-field"]');
+ const findCreateFromDropdown = () => findCreateFromFormGroup().find(RefSelector);
+
+ describe('"Tag name" field', () => {
+ describe('rendering and behavior', () => {
+ beforeEach(() => createComponent());
+
+ it('renders a label', () => {
+ expect(findTagNameFormGroup().attributes().label).toBe('Tag name');
+ });
+
+ describe('when the user updates the field', () => {
+ it("updates the store's release.tagName property", () => {
+ const updatedTagName = 'updated-tag-name';
+ findTagNameGlInput().vm.$emit('input', updatedTagName);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.state.detail.release.tagName).toBe(updatedTagName);
+ });
+ });
+ });
+ });
+
+ describe('validation', () => {
+ beforeEach(() => {
+ createComponent(mount);
+ });
+
+ /**
+ * Utility function to test the visibility of the validation message
+ * @param {'shown' | 'hidden'} state The expected state of the validation message.
+ * Should be passed either 'shown' or 'hidden'
+ */
+ const expectValidationMessageToBe = state => {
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findTagNameFormGroup().element).toHaveClass(
+ state === 'shown' ? 'is-invalid' : 'is-valid',
+ );
+ expect(findTagNameFormGroup().element).not.toHaveClass(
+ state === 'shown' ? 'is-valid' : 'is-invalid',
+ );
+ });
+ };
+
+ describe('when the user has not yet interacted with the component', () => {
+ it('does not display a validation error', () => {
+ findTagNameInput().setValue('');
+
+ return expectValidationMessageToBe('hidden');
+ });
+ });
+
+ describe('when the user has interacted with the component and the value is not empty', () => {
+ it('does not display validation error', () => {
+ findTagNameInput().trigger('blur');
+
+ return expectValidationMessageToBe('hidden');
+ });
+ });
+
+ describe('when the user has interacted with the component and the value is empty', () => {
+ it('displays a validation error', () => {
+ const tagNameInput = findTagNameInput();
+
+ tagNameInput.setValue('');
+ tagNameInput.trigger('blur');
+
+ return expectValidationMessageToBe('shown');
+ });
+ });
+ });
+ });
+
+ describe('"Create from" field', () => {
+ beforeEach(() => createComponent());
+
+ it('renders a label', () => {
+ expect(findCreateFromFormGroup().attributes().label).toBe('Create from');
+ });
+
+ describe('when the user selects a git ref', () => {
+ it("updates the store's createFrom property", () => {
+ const updatedCreateFrom = 'update-create-from';
+ findCreateFromDropdown().vm.$emit('input', updatedCreateFrom);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(store.state.detail.createFrom).toBe(updatedCreateFrom);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/components/tag_field_spec.js b/spec/frontend/releases/components/tag_field_spec.js
new file mode 100644
index 00000000000..c7909a2369b
--- /dev/null
+++ b/spec/frontend/releases/components/tag_field_spec.js
@@ -0,0 +1,59 @@
+import { shallowMount } from '@vue/test-utils';
+import TagField from '~/releases/components/tag_field.vue';
+import TagFieldNew from '~/releases/components/tag_field_new.vue';
+import TagFieldExisting from '~/releases/components/tag_field_existing.vue';
+import createStore from '~/releases/stores';
+import createDetailModule from '~/releases/stores/modules/detail';
+
+describe('releases/components/tag_field', () => {
+ let store;
+ let wrapper;
+
+ const createComponent = ({ tagName }) => {
+ store = createStore({
+ modules: {
+ detail: createDetailModule({}),
+ },
+ });
+
+ store.state.detail.tagName = tagName;
+
+ wrapper = shallowMount(TagField, { store });
+ };
+
+ const findTagFieldNew = () => wrapper.find(TagFieldNew);
+ const findTagFieldExisting = () => wrapper.find(TagFieldExisting);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when an existing release is being edited', () => {
+ beforeEach(() => {
+ createComponent({ tagName: 'v1.0' });
+ });
+
+ it('renders the TagFieldExisting component', () => {
+ expect(findTagFieldExisting().exists()).toBe(true);
+ });
+
+ it('does not render the TagFieldNew component', () => {
+ expect(findTagFieldNew().exists()).toBe(false);
+ });
+ });
+
+ describe('when a new release is being created', () => {
+ beforeEach(() => {
+ createComponent({ tagName: null });
+ });
+
+ it('renders the TagFieldNew component', () => {
+ expect(findTagFieldNew().exists()).toBe(true);
+ });
+
+ it('does not render the TagFieldExisting component', () => {
+ expect(findTagFieldExisting().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index 345be2acc71..1b2a705e8f4 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -1,18 +1,20 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { cloneDeep, merge } from 'lodash';
+import { cloneDeep } from 'lodash';
import * as actions from '~/releases/stores/modules/detail/actions';
import * as types from '~/releases/stores/modules/detail/mutation_types';
import { release as originalRelease } from '../../../mock_data';
import createState from '~/releases/stores/modules/detail/state';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { redirectTo } from '~/lib/utils/url_utility';
import api from '~/api';
+import httpStatus from '~/lib/utils/http_status';
import { ASSET_LINK_TYPE } from '~/releases/constants';
+import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
-jest.mock('~/flash', () => jest.fn());
+jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
@@ -25,15 +27,26 @@ describe('Release detail actions', () => {
let mock;
let error;
+ const setupState = (updates = {}) => {
+ const getters = {
+ isExistingRelease: true,
+ };
+
+ state = {
+ ...createState({
+ projectId: '18',
+ tagName: release.tag_name,
+ releasesPagePath: 'path/to/releases/page',
+ markdownDocsPath: 'path/to/markdown/docs',
+ markdownPreviewPath: 'path/to/markdown/preview',
+ updateReleaseApiDocsPath: 'path/to/api/docs',
+ }),
+ ...getters,
+ ...updates,
+ };
+ };
+
beforeEach(() => {
- state = createState({
- projectId: '18',
- tagName: 'v1.3',
- releasesPagePath: 'path/to/releases/page',
- markdownDocsPath: 'path/to/markdown/docs',
- markdownPreviewPath: 'path/to/markdown/preview',
- updateReleaseApiDocsPath: 'path/to/api/docs',
- });
release = cloneDeep(originalRelease);
mock = new MockAdapter(axios);
gon.api_version = 'v4';
@@ -45,284 +58,424 @@ describe('Release detail actions', () => {
mock.restore();
});
- describe('requestRelease', () => {
- it(`commits ${types.REQUEST_RELEASE}`, () =>
- testAction(actions.requestRelease, undefined, state, [{ type: types.REQUEST_RELEASE }]));
- });
+ describe('when creating a new release', () => {
+ beforeEach(() => {
+ setupState({ isExistingRelease: false });
+ });
- describe('receiveReleaseSuccess', () => {
- it(`commits ${types.RECEIVE_RELEASE_SUCCESS}`, () =>
- testAction(actions.receiveReleaseSuccess, release, state, [
- { type: types.RECEIVE_RELEASE_SUCCESS, payload: release },
- ]));
+ describe('initializeRelease', () => {
+ it(`commits ${types.INITIALIZE_EMPTY_RELEASE}`, () => {
+ testAction(actions.initializeRelease, undefined, state, [
+ { type: types.INITIALIZE_EMPTY_RELEASE },
+ ]);
+ });
+ });
+
+ describe('saveRelease', () => {
+ it(`commits ${types.REQUEST_SAVE_RELEASE} and then dispatched "createRelease"`, () => {
+ testAction(
+ actions.saveRelease,
+ undefined,
+ state,
+ [{ type: types.REQUEST_SAVE_RELEASE }],
+ [{ type: 'createRelease' }],
+ );
+ });
+ });
});
- describe('receiveReleaseError', () => {
- it(`commits ${types.RECEIVE_RELEASE_ERROR}`, () =>
- testAction(actions.receiveReleaseError, error, state, [
- { type: types.RECEIVE_RELEASE_ERROR, payload: error },
- ]));
+ describe('when editing an existing release', () => {
+ beforeEach(setupState);
- it('shows a flash with an error message', () => {
- actions.receiveReleaseError({ commit: jest.fn() }, error);
+ describe('initializeRelease', () => {
+ it('dispatches "fetchRelease"', () => {
+ testAction(actions.initializeRelease, undefined, state, [], [{ type: 'fetchRelease' }]);
+ });
+ });
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(
- 'Something went wrong while getting the release details',
- );
+ describe('saveRelease', () => {
+ it(`commits ${types.REQUEST_SAVE_RELEASE} and then dispatched "updateRelease"`, () => {
+ testAction(
+ actions.saveRelease,
+ undefined,
+ state,
+ [{ type: types.REQUEST_SAVE_RELEASE }],
+ [{ type: 'updateRelease' }],
+ );
+ });
});
});
- describe('fetchRelease', () => {
- let getReleaseUrl;
+ describe('actions that behave the same whether creating a new release or editing an existing release', () => {
+ beforeEach(setupState);
- beforeEach(() => {
- state.projectId = '18';
- state.tagName = 'v1.3';
- getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
- });
+ describe('fetchRelease', () => {
+ let getReleaseUrl;
+
+ beforeEach(() => {
+ getReleaseUrl = `/api/v4/projects/${state.projectId}/releases/${state.tagName}`;
+ });
+
+ describe('when the network request to the Release API is successful', () => {
+ beforeEach(() => {
+ mock.onGet(getReleaseUrl).replyOnce(httpStatus.OK, release);
+ });
+
+ it(`commits ${types.REQUEST_RELEASE} and then commits ${types.RECEIVE_RELEASE_SUCCESS} with the converted release object`, () => {
+ return testAction(actions.fetchRelease, undefined, state, [
+ {
+ type: types.REQUEST_RELEASE,
+ },
+ {
+ type: types.RECEIVE_RELEASE_SUCCESS,
+ payload: apiJsonToRelease(release, { deep: true }),
+ },
+ ]);
+ });
+ });
- it(`dispatches requestRelease and receiveReleaseSuccess with the camel-case'd release object`, () => {
- mock.onGet(getReleaseUrl).replyOnce(200, release);
-
- return testAction(
- actions.fetchRelease,
- undefined,
- state,
- [],
- [
- { type: 'requestRelease' },
- {
- type: 'receiveReleaseSuccess',
- payload: convertObjectPropsToCamelCase(release, { deep: true }),
- },
- ],
- );
+ describe('when the network request to the Release API fails', () => {
+ beforeEach(() => {
+ mock.onGet(getReleaseUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ });
+
+ it(`commits ${types.REQUEST_RELEASE} and then commits ${types.RECEIVE_RELEASE_ERROR} with an error object`, () => {
+ return testAction(actions.fetchRelease, undefined, state, [
+ {
+ type: types.REQUEST_RELEASE,
+ },
+ {
+ type: types.RECEIVE_RELEASE_ERROR,
+ payload: expect.any(Error),
+ },
+ ]);
+ });
+
+ it(`shows a flash message`, () => {
+ return actions.fetchRelease({ commit: jest.fn(), state }).then(() => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(
+ 'Something went wrong while getting the release details',
+ );
+ });
+ });
+ });
});
- it(`dispatches requestRelease and receiveReleaseError with an error object`, () => {
- mock.onGet(getReleaseUrl).replyOnce(500);
+ describe('updateReleaseTagName', () => {
+ it(`commits ${types.UPDATE_RELEASE_TAG_NAME} with the updated tag name`, () => {
+ const newTag = 'updated-tag-name';
+ return testAction(actions.updateReleaseTagName, newTag, state, [
+ { type: types.UPDATE_RELEASE_TAG_NAME, payload: newTag },
+ ]);
+ });
+ });
- return testAction(
- actions.fetchRelease,
- undefined,
- state,
- [],
- [{ type: 'requestRelease' }, { type: 'receiveReleaseError', payload: expect.anything() }],
- );
+ describe('updateCreateFrom', () => {
+ it(`commits ${types.UPDATE_CREATE_FROM} with the updated ref`, () => {
+ const newRef = 'my-feature-branch';
+ return testAction(actions.updateCreateFrom, newRef, state, [
+ { type: types.UPDATE_CREATE_FROM, payload: newRef },
+ ]);
+ });
});
- });
- describe('updateReleaseTitle', () => {
- it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
- const newTitle = 'The new release title';
- return testAction(actions.updateReleaseTitle, newTitle, state, [
- { type: types.UPDATE_RELEASE_TITLE, payload: newTitle },
- ]);
+ describe('updateReleaseTitle', () => {
+ it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
+ const newTitle = 'The new release title';
+ return testAction(actions.updateReleaseTitle, newTitle, state, [
+ { type: types.UPDATE_RELEASE_TITLE, payload: newTitle },
+ ]);
+ });
});
- });
- describe('updateReleaseNotes', () => {
- it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => {
- const newReleaseNotes = 'The new release notes';
- return testAction(actions.updateReleaseNotes, newReleaseNotes, state, [
- { type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes },
- ]);
+ describe('updateReleaseNotes', () => {
+ it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => {
+ const newReleaseNotes = 'The new release notes';
+ return testAction(actions.updateReleaseNotes, newReleaseNotes, state, [
+ { type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes },
+ ]);
+ });
});
- });
- describe('updateAssetLinkUrl', () => {
- it(`commits ${types.UPDATE_ASSET_LINK_URL} with the updated link URL`, () => {
- const params = {
- linkIdToUpdate: 2,
- newUrl: 'https://example.com/updated',
- };
+ describe('updateReleaseMilestones', () => {
+ it(`commits ${types.UPDATE_RELEASE_MILESTONES} with the updated release milestones`, () => {
+ const newReleaseMilestones = ['v0.0', 'v0.1'];
+ return testAction(actions.updateReleaseMilestones, newReleaseMilestones, state, [
+ { type: types.UPDATE_RELEASE_MILESTONES, payload: newReleaseMilestones },
+ ]);
+ });
+ });
- return testAction(actions.updateAssetLinkUrl, params, state, [
- { type: types.UPDATE_ASSET_LINK_URL, payload: params },
- ]);
+ describe('addEmptyAssetLink', () => {
+ it(`commits ${types.ADD_EMPTY_ASSET_LINK}`, () => {
+ return testAction(actions.addEmptyAssetLink, undefined, state, [
+ { type: types.ADD_EMPTY_ASSET_LINK },
+ ]);
+ });
});
- });
- describe('updateAssetLinkName', () => {
- it(`commits ${types.UPDATE_ASSET_LINK_NAME} with the updated link name`, () => {
- const params = {
- linkIdToUpdate: 2,
- newName: 'Updated link name',
- };
+ describe('updateAssetLinkUrl', () => {
+ it(`commits ${types.UPDATE_ASSET_LINK_URL} with the updated link URL`, () => {
+ const params = {
+ linkIdToUpdate: 2,
+ newUrl: 'https://example.com/updated',
+ };
- return testAction(actions.updateAssetLinkName, params, state, [
- { type: types.UPDATE_ASSET_LINK_NAME, payload: params },
- ]);
+ return testAction(actions.updateAssetLinkUrl, params, state, [
+ { type: types.UPDATE_ASSET_LINK_URL, payload: params },
+ ]);
+ });
});
- });
- describe('updateAssetLinkType', () => {
- it(`commits ${types.UPDATE_ASSET_LINK_TYPE} with the updated link type`, () => {
- const params = {
- linkIdToUpdate: 2,
- newType: ASSET_LINK_TYPE.RUNBOOK,
- };
+ describe('updateAssetLinkName', () => {
+ it(`commits ${types.UPDATE_ASSET_LINK_NAME} with the updated link name`, () => {
+ const params = {
+ linkIdToUpdate: 2,
+ newName: 'Updated link name',
+ };
- return testAction(actions.updateAssetLinkType, params, state, [
- { type: types.UPDATE_ASSET_LINK_TYPE, payload: params },
- ]);
+ return testAction(actions.updateAssetLinkName, params, state, [
+ { type: types.UPDATE_ASSET_LINK_NAME, payload: params },
+ ]);
+ });
});
- });
- describe('removeAssetLink', () => {
- it(`commits ${types.REMOVE_ASSET_LINK} with the ID of the asset link to remove`, () => {
- const idToRemove = 2;
- return testAction(actions.removeAssetLink, idToRemove, state, [
- { type: types.REMOVE_ASSET_LINK, payload: idToRemove },
- ]);
+ describe('updateAssetLinkType', () => {
+ it(`commits ${types.UPDATE_ASSET_LINK_TYPE} with the updated link type`, () => {
+ const params = {
+ linkIdToUpdate: 2,
+ newType: ASSET_LINK_TYPE.RUNBOOK,
+ };
+
+ return testAction(actions.updateAssetLinkType, params, state, [
+ { type: types.UPDATE_ASSET_LINK_TYPE, payload: params },
+ ]);
+ });
});
- });
- describe('updateReleaseMilestones', () => {
- it(`commits ${types.UPDATE_RELEASE_MILESTONES} with the updated release milestones`, () => {
- const newReleaseMilestones = ['v0.0', 'v0.1'];
- return testAction(actions.updateReleaseMilestones, newReleaseMilestones, state, [
- { type: types.UPDATE_RELEASE_MILESTONES, payload: newReleaseMilestones },
- ]);
+ describe('removeAssetLink', () => {
+ it(`commits ${types.REMOVE_ASSET_LINK} with the ID of the asset link to remove`, () => {
+ const idToRemove = 2;
+ return testAction(actions.removeAssetLink, idToRemove, state, [
+ { type: types.REMOVE_ASSET_LINK, payload: idToRemove },
+ ]);
+ });
});
- });
- describe('requestUpdateRelease', () => {
- it(`commits ${types.REQUEST_UPDATE_RELEASE}`, () =>
- testAction(actions.requestUpdateRelease, undefined, state, [
- { type: types.REQUEST_UPDATE_RELEASE },
- ]));
- });
+ describe('receiveSaveReleaseSuccess', () => {
+ it(`commits ${types.RECEIVE_SAVE_RELEASE_SUCCESS}`, () =>
+ testAction(actions.receiveSaveReleaseSuccess, undefined, { ...state, featureFlags: {} }, [
+ { type: types.RECEIVE_SAVE_RELEASE_SUCCESS },
+ ]));
- describe('receiveUpdateReleaseSuccess', () => {
- it(`commits ${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () =>
- testAction(actions.receiveUpdateReleaseSuccess, undefined, { ...state, featureFlags: {} }, [
- { type: types.RECEIVE_UPDATE_RELEASE_SUCCESS },
- ]));
+ describe('when the releaseShowPage feature flag is enabled', () => {
+ beforeEach(() => {
+ const rootState = { featureFlags: { releaseShowPage: true } };
+ actions.receiveSaveReleaseSuccess({ commit: jest.fn(), state, rootState }, release);
+ });
- it('redirects to the releases page if releaseShowPage feature flag is enabled', () => {
- const rootState = { featureFlags: { releaseShowPage: true } };
- const updatedState = merge({}, state, {
- releasesPagePath: 'path/to/releases/page',
- release: {
- _links: {
- self: 'path/to/self',
- },
- },
+ it("redirects to the release's dedicated page", () => {
+ expect(redirectTo).toHaveBeenCalledTimes(1);
+ expect(redirectTo).toHaveBeenCalledWith(release._links.self);
+ });
});
- actions.receiveUpdateReleaseSuccess({ commit: jest.fn(), state: updatedState, rootState });
+ describe('when the releaseShowPage feature flag is disabled', () => {
+ beforeEach(() => {
+ const rootState = { featureFlags: { releaseShowPage: false } };
+ actions.receiveSaveReleaseSuccess({ commit: jest.fn(), state, rootState }, release);
+ });
- expect(redirectTo).toHaveBeenCalledTimes(1);
- expect(redirectTo).toHaveBeenCalledWith(updatedState.release._links.self);
+ it("redirects to the project's main Releases page", () => {
+ expect(redirectTo).toHaveBeenCalledTimes(1);
+ expect(redirectTo).toHaveBeenCalledWith(state.releasesPagePath);
+ });
+ });
});
- describe('when the releaseShowPage feature flag is disabled', () => {});
- });
-
- describe('receiveUpdateReleaseError', () => {
- it(`commits ${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () =>
- testAction(actions.receiveUpdateReleaseError, error, state, [
- { type: types.RECEIVE_UPDATE_RELEASE_ERROR, payload: error },
- ]));
+ describe('createRelease', () => {
+ let createReleaseUrl;
+ let releaseLinksToCreate;
- it('shows a flash with an error message', () => {
- actions.receiveUpdateReleaseError({ commit: jest.fn() }, error);
+ beforeEach(() => {
+ const camelCasedRelease = convertObjectPropsToCamelCase(release);
- expect(createFlash).toHaveBeenCalledTimes(1);
- expect(createFlash).toHaveBeenCalledWith(
- 'Something went wrong while saving the release details',
- );
- });
- });
+ releaseLinksToCreate = camelCasedRelease.assets.links.slice(0, 1);
- describe('updateRelease', () => {
- let getters;
- let dispatch;
- let callOrder;
+ setupState({
+ release: camelCasedRelease,
+ releaseLinksToCreate,
+ });
- beforeEach(() => {
- state.release = convertObjectPropsToCamelCase(release);
- state.projectId = '18';
- state.tagName = state.release.tagName;
+ createReleaseUrl = `/api/v4/projects/${state.projectId}/releases`;
+ });
- getters = {
- releaseLinksToDelete: [{ id: '1' }, { id: '2' }],
- releaseLinksToCreate: [{ id: 'new-link-1' }, { id: 'new-link-2' }],
- };
+ describe('when the network request to the Release API is successful', () => {
+ beforeEach(() => {
+ const expectedRelease = releaseToApiJson({
+ ...state.release,
+ assets: {
+ links: releaseLinksToCreate,
+ },
+ });
- dispatch = jest.fn();
+ mock.onPost(createReleaseUrl, expectedRelease).replyOnce(httpStatus.CREATED, release);
+ });
- callOrder = [];
- jest.spyOn(api, 'updateRelease').mockImplementation(() => {
- callOrder.push('updateRelease');
- return Promise.resolve();
- });
- jest.spyOn(api, 'deleteReleaseLink').mockImplementation(() => {
- callOrder.push('deleteReleaseLink');
- return Promise.resolve();
- });
- jest.spyOn(api, 'createReleaseLink').mockImplementation(() => {
- callOrder.push('createReleaseLink');
- return Promise.resolve();
+ it(`dispatches "receiveSaveReleaseSuccess" with the converted release object`, () => {
+ return testAction(
+ actions.createRelease,
+ undefined,
+ state,
+ [],
+ [
+ {
+ type: 'receiveSaveReleaseSuccess',
+ payload: apiJsonToRelease(release, { deep: true }),
+ },
+ ],
+ );
+ });
});
- });
- it('dispatches requestUpdateRelease and receiveUpdateReleaseSuccess', () => {
- return actions.updateRelease({ dispatch, state, getters }).then(() => {
- expect(dispatch.mock.calls).toEqual([
- ['requestUpdateRelease'],
- ['receiveUpdateReleaseSuccess'],
- ]);
+ describe('when the network request to the Release API fails', () => {
+ beforeEach(() => {
+ mock.onPost(createReleaseUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ });
+
+ it(`commits ${types.RECEIVE_SAVE_RELEASE_ERROR} with an error object`, () => {
+ return testAction(actions.createRelease, undefined, state, [
+ {
+ type: types.RECEIVE_SAVE_RELEASE_ERROR,
+ payload: expect.any(Error),
+ },
+ ]);
+ });
+
+ it(`shows a flash message`, () => {
+ return actions
+ .createRelease({ commit: jest.fn(), dispatch: jest.fn(), state, getters: {} })
+ .then(() => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(
+ 'Something went wrong while creating a new release',
+ );
+ });
+ });
});
});
- it('dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object', () => {
- jest.spyOn(api, 'updateRelease').mockRejectedValue(error);
+ describe('updateRelease', () => {
+ let getters;
+ let dispatch;
+ let commit;
+ let callOrder;
+
+ beforeEach(() => {
+ getters = {
+ releaseLinksToDelete: [{ id: '1' }, { id: '2' }],
+ releaseLinksToCreate: [{ id: 'new-link-1' }, { id: 'new-link-2' }],
+ };
+
+ setupState({
+ release: convertObjectPropsToCamelCase(release),
+ ...getters,
+ });
- return actions.updateRelease({ dispatch, state, getters }).then(() => {
- expect(dispatch.mock.calls).toEqual([
- ['requestUpdateRelease'],
- ['receiveUpdateReleaseError', error],
- ]);
+ dispatch = jest.fn();
+ commit = jest.fn();
+
+ callOrder = [];
+ jest.spyOn(api, 'updateRelease').mockImplementation(() => {
+ callOrder.push('updateRelease');
+ return Promise.resolve({ data: release });
+ });
+ jest.spyOn(api, 'deleteReleaseLink').mockImplementation(() => {
+ callOrder.push('deleteReleaseLink');
+ return Promise.resolve();
+ });
+ jest.spyOn(api, 'createReleaseLink').mockImplementation(() => {
+ callOrder.push('createReleaseLink');
+ return Promise.resolve();
+ });
});
- });
- it('updates the Release, then deletes all existing links, and then recreates new links', () => {
- return actions.updateRelease({ dispatch, state, getters }).then(() => {
- expect(callOrder).toEqual([
- 'updateRelease',
- 'deleteReleaseLink',
- 'deleteReleaseLink',
- 'createReleaseLink',
- 'createReleaseLink',
- ]);
+ describe('when the network request to the Release API is successful', () => {
+ it('dispatches receiveSaveReleaseSuccess', () => {
+ return actions.updateRelease({ commit, dispatch, state, getters }).then(() => {
+ expect(dispatch.mock.calls).toEqual([
+ ['receiveSaveReleaseSuccess', apiJsonToRelease(release)],
+ ]);
+ });
+ });
- expect(api.updateRelease.mock.calls).toEqual([
- [
- state.projectId,
- state.tagName,
- {
- name: state.release.name,
- description: state.release.description,
- milestones: state.release.milestones.map(milestone => milestone.title),
- },
- ],
- ]);
+ it('updates the Release, then deletes all existing links, and then recreates new links', () => {
+ return actions.updateRelease({ dispatch, state, getters }).then(() => {
+ expect(callOrder).toEqual([
+ 'updateRelease',
+ 'deleteReleaseLink',
+ 'deleteReleaseLink',
+ 'createReleaseLink',
+ 'createReleaseLink',
+ ]);
+
+ expect(api.updateRelease.mock.calls).toEqual([
+ [
+ state.projectId,
+ state.tagName,
+ releaseToApiJson({
+ ...state.release,
+ assets: {
+ links: getters.releaseLinksToCreate,
+ },
+ }),
+ ],
+ ]);
+
+ expect(api.deleteReleaseLink).toHaveBeenCalledTimes(
+ getters.releaseLinksToDelete.length,
+ );
+ getters.releaseLinksToDelete.forEach(link => {
+ expect(api.deleteReleaseLink).toHaveBeenCalledWith(
+ state.projectId,
+ state.tagName,
+ link.id,
+ );
+ });
+
+ expect(api.createReleaseLink).toHaveBeenCalledTimes(
+ getters.releaseLinksToCreate.length,
+ );
+ getters.releaseLinksToCreate.forEach(link => {
+ expect(api.createReleaseLink).toHaveBeenCalledWith(
+ state.projectId,
+ state.tagName,
+ link,
+ );
+ });
+ });
+ });
+ });
- expect(api.deleteReleaseLink).toHaveBeenCalledTimes(getters.releaseLinksToDelete.length);
- getters.releaseLinksToDelete.forEach(link => {
- expect(api.deleteReleaseLink).toHaveBeenCalledWith(
- state.projectId,
- state.tagName,
- link.id,
- );
+ describe('when the network request to the Release API fails', () => {
+ beforeEach(() => {
+ jest.spyOn(api, 'updateRelease').mockRejectedValue(error);
+ });
+
+ it('dispatches requestUpdateRelease and receiveUpdateReleaseError with an error object', () => {
+ return actions.updateRelease({ commit, dispatch, state, getters }).then(() => {
+ expect(commit.mock.calls).toEqual([[types.RECEIVE_SAVE_RELEASE_ERROR, error]]);
+ });
});
- expect(api.createReleaseLink).toHaveBeenCalledTimes(getters.releaseLinksToCreate.length);
- getters.releaseLinksToCreate.forEach(link => {
- expect(api.createReleaseLink).toHaveBeenCalledWith(state.projectId, state.tagName, link);
+ it('shows a flash message', () => {
+ return actions.updateRelease({ commit, dispatch, state, getters }).then(() => {
+ expect(createFlash).toHaveBeenCalledTimes(1);
+ expect(createFlash).toHaveBeenCalledWith(
+ 'Something went wrong while saving the release details',
+ );
+ });
});
});
});
diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js
index 8945ad97c93..2d9f35428f2 100644
--- a/spec/frontend/releases/stores/modules/detail/getters_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js
@@ -1,6 +1,20 @@
import * as getters from '~/releases/stores/modules/detail/getters';
describe('Release detail getters', () => {
+ describe('isExistingRelease', () => {
+ it('returns true if the release is an existing release that already exists in the database', () => {
+ const state = { tagName: 'test-tag-name' };
+
+ expect(getters.isExistingRelease(state)).toBe(true);
+ });
+
+ it('returns false if the release is a new release that has not yet been saved to the database', () => {
+ const state = { tagName: null };
+
+ expect(getters.isExistingRelease(state)).toBe(false);
+ });
+ });
+
describe('releaseLinksToCreate', () => {
it("returns an empty array if state.release doesn't exist", () => {
const state = {};
@@ -62,6 +76,7 @@ describe('Release detail getters', () => {
it('returns no validation errors', () => {
const state = {
release: {
+ tagName: 'test-tag-name',
assets: {
links: [
{ id: 1, url: 'https://example.com/valid', name: 'Link 1' },
@@ -96,6 +111,9 @@ describe('Release detail getters', () => {
beforeEach(() => {
const state = {
release: {
+ // empty tag name
+ tagName: '',
+
assets: {
links: [
// Duplicate URLs
@@ -124,7 +142,15 @@ describe('Release detail getters', () => {
actualErrors = getters.validationErrors(state);
});
- it('returns a validation errors if links share a URL', () => {
+ it('returns a validation error if the tag name is empty', () => {
+ const expectedErrors = {
+ isTagNameEmpty: true,
+ };
+
+ expect(actualErrors).toMatchObject(expectedErrors);
+ });
+
+ it('returns a validation error if links share a URL', () => {
const expectedErrors = {
assets: {
links: {
@@ -182,32 +208,53 @@ describe('Release detail getters', () => {
// the value of state is not actually used by this getter
const state = {};
- it('returns true when the form is valid', () => {
- const mockGetters = {
- validationErrors: {
- assets: {
- links: {
- 1: {},
+ describe('when the form is valid', () => {
+ it('returns true', () => {
+ const mockGetters = {
+ validationErrors: {
+ assets: {
+ links: {
+ 1: {},
+ },
},
},
- },
- };
+ };
- expect(getters.isValid(state, mockGetters)).toBe(true);
+ expect(getters.isValid(state, mockGetters)).toBe(true);
+ });
});
- it('returns false when the form is invalid', () => {
- const mockGetters = {
- validationErrors: {
- assets: {
- links: {
- 1: { isNameEmpty: true },
+ describe('when an asset link contains a validation error', () => {
+ it('returns false', () => {
+ const mockGetters = {
+ validationErrors: {
+ assets: {
+ links: {
+ 1: { isNameEmpty: true },
+ },
},
},
- },
- };
+ };
- expect(getters.isValid(state, mockGetters)).toBe(false);
+ expect(getters.isValid(state, mockGetters)).toBe(false);
+ });
+ });
+
+ describe('when the tag name is empty', () => {
+ it('returns false', () => {
+ const mockGetters = {
+ validationErrors: {
+ isTagNameEmpty: true,
+ assets: {
+ links: {
+ 1: {},
+ },
+ },
+ },
+ };
+
+ expect(getters.isValid(state, mockGetters)).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
index a34c1be64d9..cd7c6b7d275 100644
--- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
@@ -21,6 +21,22 @@ describe('Release detail mutations', () => {
release = convertObjectPropsToCamelCase(originalRelease);
});
+ describe(`${types.INITIALIZE_EMPTY_RELEASE}`, () => {
+ it('set state.release to an empty release object', () => {
+ mutations[types.INITIALIZE_EMPTY_RELEASE](state);
+
+ expect(state.release).toEqual({
+ tagName: null,
+ name: '',
+ description: '',
+ milestones: [],
+ assets: {
+ links: [],
+ },
+ });
+ });
+ });
+
describe(`${types.REQUEST_RELEASE}`, () => {
it('set state.isFetchingRelease to true', () => {
mutations[types.REQUEST_RELEASE](state);
@@ -56,6 +72,26 @@ describe('Release detail mutations', () => {
});
});
+ describe(`${types.UPDATE_RELEASE_TAG_NAME}`, () => {
+ it("updates the release's tag name", () => {
+ state.release = release;
+ const newTag = 'updated-tag-name';
+ mutations[types.UPDATE_RELEASE_TAG_NAME](state, newTag);
+
+ expect(state.release.tagName).toBe(newTag);
+ });
+ });
+
+ describe(`${types.UPDATE_CREATE_FROM}`, () => {
+ it('updates the ref that the ref will be created from', () => {
+ state.createFrom = 'main';
+ const newRef = 'my-feature-branch';
+ mutations[types.UPDATE_CREATE_FROM](state, newRef);
+
+ expect(state.createFrom).toBe(newRef);
+ });
+ });
+
describe(`${types.UPDATE_RELEASE_TITLE}`, () => {
it("updates the release's title", () => {
state.release = release;
@@ -76,17 +112,17 @@ describe('Release detail mutations', () => {
});
});
- describe(`${types.REQUEST_UPDATE_RELEASE}`, () => {
+ describe(`${types.REQUEST_SAVE_RELEASE}`, () => {
it('set state.isUpdatingRelease to true', () => {
- mutations[types.REQUEST_UPDATE_RELEASE](state);
+ mutations[types.REQUEST_SAVE_RELEASE](state);
expect(state.isUpdatingRelease).toBe(true);
});
});
- describe(`${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () => {
+ describe(`${types.RECEIVE_SAVE_RELEASE_SUCCESS}`, () => {
it('handles a successful response from the server', () => {
- mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, release);
+ mutations[types.RECEIVE_SAVE_RELEASE_SUCCESS](state, release);
expect(state.updateError).toBeUndefined();
@@ -94,10 +130,10 @@ describe('Release detail mutations', () => {
});
});
- describe(`${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () => {
+ describe(`${types.RECEIVE_SAVE_RELEASE_ERROR}`, () => {
it('handles an unsuccessful response from the server', () => {
const error = { message: 'An error occurred!' };
- mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error);
+ mutations[types.RECEIVE_SAVE_RELEASE_ERROR](state, error);
expect(state.isUpdatingRelease).toBe(false);
diff --git a/spec/frontend/releases/util_spec.js b/spec/frontend/releases/util_spec.js
new file mode 100644
index 00000000000..90aa9c4c7d8
--- /dev/null
+++ b/spec/frontend/releases/util_spec.js
@@ -0,0 +1,103 @@
+import { releaseToApiJson, apiJsonToRelease } from '~/releases/util';
+
+describe('releases/util.js', () => {
+ describe('releaseToApiJson', () => {
+ it('converts a release JavaScript object into JSON that the Release API can accept', () => {
+ const release = {
+ tagName: 'tag-name',
+ name: 'Release name',
+ description: 'Release description',
+ milestones: [{ id: 1, title: '13.2' }, { id: 2, title: '13.3' }],
+ assets: {
+ links: [{ url: 'https://gitlab.example.com/link', linkType: 'other' }],
+ },
+ };
+
+ const expectedJson = {
+ tag_name: 'tag-name',
+ ref: null,
+ name: 'Release name',
+ description: 'Release description',
+ milestones: ['13.2', '13.3'],
+ assets: {
+ links: [{ url: 'https://gitlab.example.com/link', link_type: 'other' }],
+ },
+ };
+
+ expect(releaseToApiJson(release)).toEqual(expectedJson);
+ });
+
+ describe('when createFrom is provided', () => {
+ it('adds the provided createFrom ref to the JSON as a "ref" property', () => {
+ const createFrom = 'main';
+
+ const release = {};
+
+ const expectedJson = {
+ ref: createFrom,
+ };
+
+ expect(releaseToApiJson(release, createFrom)).toMatchObject(expectedJson);
+ });
+ });
+
+ describe('release.name', () => {
+ it.each`
+ input | output
+ ${null} | ${null}
+ ${''} | ${null}
+ ${' \t\n\r\n'} | ${null}
+ ${' Release name '} | ${'Release name'}
+ `('converts a name like `$input` to `$output`', ({ input, output }) => {
+ const release = { name: input };
+
+ const expectedJson = {
+ name: output,
+ };
+
+ expect(releaseToApiJson(release)).toMatchObject(expectedJson);
+ });
+ });
+
+ describe('when release.milestones is falsy', () => {
+ it('includes a "milestone" property in the returned result as an empty array', () => {
+ const release = {};
+
+ const expectedJson = {
+ milestones: [],
+ };
+
+ expect(releaseToApiJson(release)).toMatchObject(expectedJson);
+ });
+ });
+ });
+
+ describe('apiJsonToRelease', () => {
+ it('converts JSON received from the Release API into an object usable by the Vue application', () => {
+ const json = {
+ tag_name: 'tag-name',
+ assets: {
+ links: [
+ {
+ link_type: 'other',
+ },
+ ],
+ },
+ };
+
+ const expectedRelease = {
+ tagName: 'tag-name',
+ assets: {
+ links: [
+ {
+ linkType: 'other',
+ },
+ ],
+ },
+ milestones: [],
+ };
+
+ expect(apiJsonToRelease(json)).toEqual(expectedRelease);
+ });
+ });
+});
diff --git a/spec/frontend/reports/accessibility_report/mock_data.js b/spec/frontend/reports/accessibility_report/mock_data.js
index f8e832c1ce5..20ad01bd802 100644
--- a/spec/frontend/reports/accessibility_report/mock_data.js
+++ b/spec/frontend/reports/accessibility_report/mock_data.js
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/prefer-default-export
export const mockReport = {
status: 'failed',
summary: {
@@ -51,5 +52,3 @@ export const mockReport = {
existing_notes: [],
existing_warnings: [],
};
-
-export default () => {};
diff --git a/spec/frontend/reports/accessibility_report/store/actions_spec.js b/spec/frontend/reports/accessibility_report/store/actions_spec.js
index 129a5bade86..9f210659cfd 100644
--- a/spec/frontend/reports/accessibility_report/store/actions_spec.js
+++ b/spec/frontend/reports/accessibility_report/store/actions_spec.js
@@ -1,10 +1,10 @@
-import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'spec/test_constants';
+import testAction from 'helpers/vuex_action_helper';
+import axios from '~/lib/utils/axios_utils';
import * as actions from '~/reports/accessibility_report/store/actions';
import * as types from '~/reports/accessibility_report/store/mutation_types';
import createStore from '~/reports/accessibility_report/store';
-import { TEST_HOST } from 'spec/test_constants';
-import testAction from 'helpers/vuex_action_helper';
import { mockReport } from '../mock_data';
describe('Accessibility Reports actions', () => {
diff --git a/spec/frontend/reports/codequality_report/store/actions_spec.js b/spec/frontend/reports/codequality_report/store/actions_spec.js
index 6c30fdb7871..7d9e4bbbe9f 100644
--- a/spec/frontend/reports/codequality_report/store/actions_spec.js
+++ b/spec/frontend/reports/codequality_report/store/actions_spec.js
@@ -1,10 +1,10 @@
-import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'spec/test_constants';
+import testAction from 'helpers/vuex_action_helper';
+import axios from '~/lib/utils/axios_utils';
import * as actions from '~/reports/codequality_report/store/actions';
import * as types from '~/reports/codequality_report/store/mutation_types';
import createStore from '~/reports/codequality_report/store';
-import { TEST_HOST } from 'spec/test_constants';
-import testAction from 'helpers/vuex_action_helper';
import { headIssues, baseIssues, mockParsedHeadIssues, mockParsedBaseIssues } from '../mock_data';
// mock codequality comparison worker
diff --git a/spec/frontend/reports/components/grouped_test_reports_app_spec.js b/spec/frontend/reports/components/grouped_test_reports_app_spec.js
index 017e0335569..c26e2fbc19a 100644
--- a/spec/frontend/reports/components/grouped_test_reports_app_spec.js
+++ b/spec/frontend/reports/components/grouped_test_reports_app_spec.js
@@ -20,10 +20,7 @@ describe('Grouped test reports app', () => {
let wrapper;
let mockStore;
- const mountComponent = ({
- glFeatures = { junitPipelineView: false },
- props = { pipelinePath },
- } = {}) => {
+ const mountComponent = ({ props = { pipelinePath } } = {}) => {
wrapper = mount(Component, {
store: mockStore,
localVue,
@@ -35,9 +32,6 @@ describe('Grouped test reports app', () => {
methods: {
fetchReports: () => {},
},
- provide: {
- glFeatures,
- },
});
};
@@ -78,28 +72,17 @@ describe('Grouped test reports app', () => {
});
describe('`View full report` button', () => {
- it('should not render the full test report link', () => {
- expect(findFullTestReportLink().exists()).toBe(false);
- });
+ it('should render the full test report link', () => {
+ const fullTestReportLink = findFullTestReportLink();
- describe('With junitPipelineView feature flag enabled', () => {
- beforeEach(() => {
- mountComponent({ glFeatures: { junitPipelineView: true } });
- });
-
- it('should render the full test report link', () => {
- const fullTestReportLink = findFullTestReportLink();
-
- expect(fullTestReportLink.exists()).toBe(true);
- expect(pipelinePath).not.toBe('');
- expect(fullTestReportLink.attributes('href')).toBe(`${pipelinePath}/test_report`);
- });
+ expect(fullTestReportLink.exists()).toBe(true);
+ expect(pipelinePath).not.toBe('');
+ expect(fullTestReportLink.attributes('href')).toBe(`${pipelinePath}/test_report`);
});
describe('Without a pipelinePath', () => {
beforeEach(() => {
mountComponent({
- glFeatures: { junitPipelineView: true },
props: { pipelinePath: '' },
});
});
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 1dca65dd862..cf2e6b00800 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -10,7 +10,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
imgcssclasses=""
imgsize="40"
imgsrc="https://test.com"
- linkhref="https://test.com/test"
+ linkhref="/test"
tooltipplacement="top"
tooltiptext=""
username=""
@@ -24,7 +24,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
>
<gl-link-stub
class="commit-row-message item-title"
- href="https://test.com/commit/123"
+ href="/commit/123"
>
Commit title
</gl-link-stub>
@@ -36,7 +36,7 @@ exports[`Repository last commit component renders commit widget 1`] = `
>
<gl-link-stub
class="commit-author-link js-user-link"
- href="https://test.com/test"
+ href="/test"
>
Test
@@ -110,7 +110,7 @@ exports[`Repository last commit component renders the signature HTML as returned
imgcssclasses=""
imgsize="40"
imgsrc="https://test.com"
- linkhref="https://test.com/test"
+ linkhref="/test"
tooltipplacement="top"
tooltiptext=""
username=""
@@ -124,7 +124,7 @@ exports[`Repository last commit component renders the signature HTML as returned
>
<gl-link-stub
class="commit-row-message item-title"
- href="https://test.com/commit/123"
+ href="/commit/123"
>
Commit title
</gl-link-stub>
@@ -136,7 +136,7 @@ exports[`Repository last commit component renders the signature HTML as returned
>
<gl-link-stub
class="commit-author-link js-user-link"
- href="https://test.com/test"
+ href="/test"
>
Test
diff --git a/spec/frontend/repository/components/breadcrumbs_spec.js b/spec/frontend/repository/components/breadcrumbs_spec.js
index 38e5c9aaca5..ca4120576f5 100644
--- a/spec/frontend/repository/components/breadcrumbs_spec.js
+++ b/spec/frontend/repository/components/breadcrumbs_spec.js
@@ -1,5 +1,5 @@
import { shallowMount, RouterLinkStub } from '@vue/test-utils';
-import { GlDropdown } from '@gitlab/ui';
+import { GlDeprecatedDropdown } from '@gitlab/ui';
import Breadcrumbs from '~/repository/components/breadcrumbs.vue';
let vm;
@@ -61,7 +61,7 @@ describe('Repository breadcrumbs component', () => {
vm.setData({ userPermissions: { forkProject: false, createMergeRequestIn: false } });
return vm.vm.$nextTick(() => {
- expect(vm.find(GlDropdown).exists()).toBe(false);
+ expect(vm.find(GlDeprecatedDropdown).exists()).toBe(false);
});
});
@@ -71,7 +71,7 @@ describe('Repository breadcrumbs component', () => {
vm.setData({ userPermissions: { forkProject: true, createMergeRequestIn: true } });
return vm.vm.$nextTick(() => {
- expect(vm.find(GlDropdown).exists()).toBe(true);
+ expect(vm.find(GlDeprecatedDropdown).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index a5bfeb08fe4..c14a7f0e061 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -11,12 +11,12 @@ function createCommitData(data = {}) {
title: 'Commit title',
titleHtml: 'Commit title',
message: 'Commit message',
- webUrl: 'https://test.com/commit/123',
+ webPath: '/commit/123',
authoredDate: '2019-01-01',
author: {
name: 'Test',
avatarUrl: 'https://test.com',
- webUrl: 'https://test.com/test',
+ webPath: '/test',
},
pipeline: {
detailedStatus: {
@@ -108,7 +108,7 @@ describe('Repository last commit component', () => {
});
it('does not render description expander when description is null', () => {
- factory(createCommitData({ description: null }));
+ factory(createCommitData({ descriptionHtml: null }));
return vm.vm.$nextTick(() => {
expect(vm.find('.text-expander').exists()).toBe(false);
@@ -117,7 +117,7 @@ describe('Repository last commit component', () => {
});
it('expands commit description when clicking expander', () => {
- factory(createCommitData({ description: 'Test description' }));
+ factory(createCommitData({ descriptionHtml: 'Test description' }));
return vm.vm
.$nextTick()
diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js
index 6ae323f5c3f..ebd985e640c 100644
--- a/spec/frontend/repository/components/preview/index_spec.js
+++ b/spec/frontend/repository/components/preview/index_spec.js
@@ -30,7 +30,7 @@ describe('Repository file preview component', () => {
it('renders file HTML', () => {
factory({
- webUrl: 'http://test.com',
+ webPath: 'http://test.com',
name: 'README.md',
});
@@ -43,7 +43,7 @@ describe('Repository file preview component', () => {
it('handles hash after render', () => {
factory({
- webUrl: 'http://test.com',
+ webPath: 'http://test.com',
name: 'README.md',
});
@@ -59,7 +59,7 @@ describe('Repository file preview component', () => {
it('renders loading icon', () => {
factory({
- webUrl: 'http://test.com',
+ webPath: 'http://test.com',
name: 'README.md',
});
diff --git a/spec/frontend/repository/components/table/index_spec.js b/spec/frontend/repository/components/table/index_spec.js
index ed50f292b8c..10669330b61 100644
--- a/spec/frontend/repository/components/table/index_spec.js
+++ b/spec/frontend/repository/components/table/index_spec.js
@@ -13,7 +13,7 @@ const MOCK_BLOBS = [
flatPath: 'blob',
name: 'blob.md',
type: 'blob',
- webUrl: 'http://test.com',
+ webPath: '/blob',
},
{
id: '124abc',
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index da892ce51d8..ea85cd34743 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -1,5 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import TreeContent from '~/repository/components/tree_content.vue';
+import { GlButton } from '@gitlab/ui';
+import TreeContent, { INITIAL_FETCH_COUNT } from '~/repository/components/tree_content.vue';
import FilePreview from '~/repository/components/preview/index.vue';
let vm;
@@ -25,14 +26,24 @@ describe('Repository table component', () => {
vm.destroy();
});
- it('renders file preview', () => {
+ it('renders file preview', async () => {
factory('/');
vm.setData({ entries: { blobs: [{ name: 'README.md' }] } });
- return vm.vm.$nextTick().then(() => {
- expect(vm.find(FilePreview).exists()).toBe(true);
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find(FilePreview).exists()).toBe(true);
+ });
+
+ it('trigger fetchFiles when mounted', async () => {
+ factory('/');
+
+ jest.spyOn(vm.vm, 'fetchFiles').mockImplementation(() => {});
+
+ await vm.vm.$nextTick();
+
+ expect(vm.vm.fetchFiles).toHaveBeenCalled();
});
describe('normalizeData', () => {
@@ -70,4 +81,59 @@ describe('Repository table component', () => {
expect(output).toEqual({ hasNextPage: true, nextCursor: 'test' });
});
});
+
+ describe('Show more button', () => {
+ const showMoreButton = () => vm.find(GlButton);
+
+ describe('when is present', () => {
+ beforeEach(async () => {
+ factory('/');
+
+ vm.setData({ fetchCounter: 10, clickedShowMore: false });
+
+ await vm.vm.$nextTick();
+ });
+
+ it('is not rendered once it is clicked', async () => {
+ showMoreButton().vm.$emit('click');
+ await vm.vm.$nextTick();
+
+ expect(showMoreButton().exists()).toBe(false);
+ });
+
+ it('is rendered', async () => {
+ expect(showMoreButton().exists()).toBe(true);
+ });
+
+ it('changes clickedShowMore when show more button is clicked', async () => {
+ showMoreButton().vm.$emit('click');
+
+ expect(vm.vm.clickedShowMore).toBe(true);
+ });
+
+ it('triggers fetchFiles when show more button is clicked', async () => {
+ jest.spyOn(vm.vm, 'fetchFiles');
+
+ showMoreButton().vm.$emit('click');
+
+ expect(vm.vm.fetchFiles).toBeCalled();
+ });
+ });
+
+ it('is not rendered if less than 1000 files', async () => {
+ factory('/');
+
+ vm.setData({ fetchCounter: 5, clickedShowMore: false });
+
+ await vm.vm.$nextTick();
+
+ expect(showMoreButton().exists()).toBe(false);
+ });
+
+ it('has limit of 1000 files on initial load', () => {
+ factory('/');
+
+ expect(INITIAL_FETCH_COUNT * vm.vm.pageSize).toBe(1000);
+ });
+ });
});
diff --git a/spec/frontend/repository/components/web_ide_link_spec.js b/spec/frontend/repository/components/web_ide_link_spec.js
index 59e1a4fd719..877756db364 100644
--- a/spec/frontend/repository/components/web_ide_link_spec.js
+++ b/spec/frontend/repository/components/web_ide_link_spec.js
@@ -1,5 +1,5 @@
-import WebIdeLink from '~/repository/components/web_ide_link.vue';
import { mount } from '@vue/test-utils';
+import WebIdeLink from '~/repository/components/web_ide_link.vue';
describe('Web IDE link component', () => {
let wrapper;
diff --git a/spec/frontend/repository/utils/dom_spec.js b/spec/frontend/repository/utils/dom_spec.js
index e8b0565868e..26ed57f0392 100644
--- a/spec/frontend/repository/utils/dom_spec.js
+++ b/spec/frontend/repository/utils/dom_spec.js
@@ -1,6 +1,6 @@
+import { TEST_HOST } from 'helpers/test_constants';
import { setHTMLFixture } from '../../helpers/fixtures';
import { updateElementsVisibility, updateFormAction } from '~/repository/utils/dom';
-import { TEST_HOST } from 'helpers/test_constants';
describe('updateElementsVisibility', () => {
it('adds hidden class', () => {
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index 05b36474548..ee46dc015af 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -2,10 +2,11 @@
import $ from 'jquery';
import '~/gl_dropdown';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import initSearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
-import AxiosMockAdapter from 'axios-mock-adapter';
describe('Search autocomplete dropdown', () => {
let widget = null;
@@ -274,11 +275,32 @@ describe('Search autocomplete dropdown', () => {
});
describe('enableAutocomplete', () => {
+ let toggleSpy;
+ let trackingSpy;
+
+ beforeEach(() => {
+ toggleSpy = jest.spyOn(widget.dropdownToggle, 'dropdown');
+ trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
+ document.body.dataset.page = 'some:page'; // default tracking for category
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
it('should open the Dropdown', () => {
- const toggleSpy = jest.spyOn(widget.dropdownToggle, 'dropdown');
widget.enableAutocomplete();
expect(toggleSpy).toHaveBeenCalledWith('toggle');
});
+
+ it('should track the opening', () => {
+ widget.enableAutocomplete();
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_search_bar', {
+ label: 'main_navigation',
+ property: 'navigation',
+ });
+ });
});
});
diff --git a/spec/frontend/self_monitor/components/self_monitor_form_spec.js b/spec/frontend/self_monitor/components/self_monitor_form_spec.js
index aa6f71b6412..ec5f7b0a394 100644
--- a/spec/frontend/self_monitor/components/self_monitor_form_spec.js
+++ b/spec/frontend/self_monitor/components/self_monitor_form_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { GlDeprecatedButton } from '@gitlab/ui';
+import { TEST_HOST } from 'helpers/test_constants';
import SelfMonitor from '~/self_monitor/components/self_monitor_form.vue';
import { createStore } from '~/self_monitor/store';
-import { TEST_HOST } from 'helpers/test_constants';
describe('self monitor component', () => {
let wrapper;
diff --git a/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
new file mode 100644
index 00000000000..22689080063
--- /dev/null
+++ b/spec/frontend/serverless/components/__snapshots__/empty_state_spec.js.snap
@@ -0,0 +1,20 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`EmptyStateComponent should render content 1`] = `
+"<section class=\\"row empty-state text-center\\">
+ <div class=\\"col-12\\">
+ <div class=\\"svg-250 svg-content\\"><img src=\\"/image.svg\\" alt=\\"Getting started with serverless\\" class=\\"gl-max-w-full\\"></div>
+ </div>
+ <div class=\\"col-12\\">
+ <div class=\\"text-content gl-mx-auto gl-my-0 gl-p-5\\">
+ <h1 class=\\"h4\\">Getting started with serverless</h1>
+ <p>In order to start using functions as a service, you must first install Knative on your Kubernetes cluster. <gl-link-stub href=\\"/help\\">More information</gl-link-stub>
+ </p>
+ <div>
+ <gl-button-stub category=\\"primary\\" variant=\\"success\\" size=\\"medium\\" icon=\\"\\" href=\\"/clusters\\">Install Knative</gl-button-stub>
+ <!---->
+ </div>
+ </div>
+ </div>
+</section>"
+`;
diff --git a/spec/frontend/serverless/components/empty_state_spec.js b/spec/frontend/serverless/components/empty_state_spec.js
new file mode 100644
index 00000000000..daa1576a4ec
--- /dev/null
+++ b/spec/frontend/serverless/components/empty_state_spec.js
@@ -0,0 +1,25 @@
+import { GlEmptyState, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { createStore } from '~/serverless/store';
+import EmptyStateComponent from '~/serverless/components/empty_state.vue';
+
+describe('EmptyStateComponent', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ const store = createStore({
+ clustersPath: '/clusters',
+ helpPath: '/help',
+ emptyImagePath: '/image.svg',
+ });
+ wrapper = shallowMount(EmptyStateComponent, { store, stubs: { GlEmptyState, GlSprintf } });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('should render content', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/serverless/components/function_details_spec.js b/spec/frontend/serverless/components/function_details_spec.js
index 40d2bbb0291..248dd418941 100644
--- a/spec/frontend/serverless/components/function_details_spec.js
+++ b/spec/frontend/serverless/components/function_details_spec.js
@@ -13,7 +13,7 @@ describe('functionDetailsComponent', () => {
localVue = createLocalVue();
localVue.use(Vuex);
- store = createStore();
+ store = createStore({ clustersPath: '/clusters', helpPath: '/help' });
});
afterEach(() => {
@@ -38,8 +38,6 @@ describe('functionDetailsComponent', () => {
propsData: {
func: serviceStub,
hasPrometheus: false,
- clustersPath: '/clusters',
- helpPath: '/help',
},
});
@@ -65,8 +63,6 @@ describe('functionDetailsComponent', () => {
propsData: {
func: serviceStub,
hasPrometheus: false,
- clustersPath: '/clusters',
- helpPath: '/help',
},
});
@@ -82,8 +78,6 @@ describe('functionDetailsComponent', () => {
propsData: {
func: serviceStub,
hasPrometheus: false,
- clustersPath: '/clusters',
- helpPath: '/help',
},
});
@@ -99,8 +93,6 @@ describe('functionDetailsComponent', () => {
propsData: {
func: serviceStub,
hasPrometheus: false,
- clustersPath: '/clusters',
- helpPath: '/help',
},
});
diff --git a/spec/frontend/serverless/components/functions_spec.js b/spec/frontend/serverless/components/functions_spec.js
index 8db04409357..0fca027fe56 100644
--- a/spec/frontend/serverless/components/functions_spec.js
+++ b/spec/frontend/serverless/components/functions_spec.js
@@ -25,55 +25,31 @@ describe('functionsComponent', () => {
localVue = createLocalVue();
localVue.use(Vuex);
- store = createStore();
+ store = createStore({});
});
afterEach(() => {
- component.vm.$destroy();
+ component.destroy();
axiosMock.restore();
});
it('should render empty state when Knative is not installed', () => {
store.dispatch('receiveFunctionsSuccess', { knative_installed: false });
- component = shallowMount(functionsComponent, {
- localVue,
- store,
- propsData: {
- clustersPath: '',
- helpPath: '',
- statusPath: '',
- },
- });
+ component = shallowMount(functionsComponent, { localVue, store });
expect(component.find(EmptyState).exists()).toBe(true);
});
it('should render a loading component', () => {
store.dispatch('requestFunctionsLoading');
- component = shallowMount(functionsComponent, {
- localVue,
- store,
- propsData: {
- clustersPath: '',
- helpPath: '',
- statusPath: '',
- },
- });
+ component = shallowMount(functionsComponent, { localVue, store });
expect(component.find(GlLoadingIcon).exists()).toBe(true);
});
it('should render empty state when there is no function data', () => {
store.dispatch('receiveFunctionsNoDataSuccess', { knative_installed: true });
- component = shallowMount(functionsComponent, {
- localVue,
- store,
- propsData: {
- clustersPath: '',
- helpPath: '',
- statusPath: '',
- },
- });
+ component = shallowMount(functionsComponent, { localVue, store });
expect(
component.vm.$el
@@ -91,30 +67,17 @@ describe('functionsComponent', () => {
...mockServerlessFunctions,
knative_installed: 'checking',
});
- component = shallowMount(functionsComponent, {
- localVue,
- store,
- propsData: {
- clustersPath: '',
- helpPath: '',
- statusPath: '',
- },
- });
+
+ component = shallowMount(functionsComponent, { localVue, store });
expect(component.find('.js-functions-wrapper').exists()).toBe(true);
expect(component.find('.js-functions-loader').exists()).toBe(true);
});
it('should render the functions list', () => {
- component = shallowMount(functionsComponent, {
- localVue,
- store,
- propsData: {
- clustersPath: 'clustersPath',
- helpPath: 'helpPath',
- statusPath,
- },
- });
+ store = createStore({ clustersPath: 'clustersPath', helpPath: 'helpPath', statusPath });
+
+ component = shallowMount(functionsComponent, { localVue, store });
component.vm.$store.dispatch('receiveFunctionsSuccess', mockServerlessFunctions);
diff --git a/spec/frontend/serverless/components/missing_prometheus_spec.js b/spec/frontend/serverless/components/missing_prometheus_spec.js
index 90730765f7c..9ca4a45dd5f 100644
--- a/spec/frontend/serverless/components/missing_prometheus_spec.js
+++ b/spec/frontend/serverless/components/missing_prometheus_spec.js
@@ -1,25 +1,23 @@
import { GlDeprecatedButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { createStore } from '~/serverless/store';
import missingPrometheusComponent from '~/serverless/components/missing_prometheus.vue';
-const createComponent = missingData =>
- shallowMount(missingPrometheusComponent, {
- propsData: {
- clustersPath: '/clusters',
- helpPath: '/help',
- missingData,
- },
- });
-
describe('missingPrometheusComponent', () => {
let wrapper;
+ const createComponent = missingData => {
+ const store = createStore({ clustersPath: '/clusters', helpPath: '/help' });
+
+ wrapper = shallowMount(missingPrometheusComponent, { store, propsData: { missingData } });
+ };
+
afterEach(() => {
wrapper.destroy();
});
it('should render missing prometheus message', () => {
- wrapper = createComponent(false);
+ createComponent(false);
const { vm } = wrapper;
expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
@@ -30,7 +28,7 @@ describe('missingPrometheusComponent', () => {
});
it('should render no prometheus data message', () => {
- wrapper = createComponent(true);
+ createComponent(true);
const { vm } = wrapper;
expect(vm.$el.querySelector('.state-description').innerHTML.trim()).toContain(
diff --git a/spec/frontend/serverless/survey_banner_spec.js b/spec/frontend/serverless/survey_banner_spec.js
index 15e9c6ec350..29b36fb9b5f 100644
--- a/spec/frontend/serverless/survey_banner_spec.js
+++ b/spec/frontend/serverless/survey_banner_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import Cookies from 'js-cookie';
-import SurveyBanner from '~/serverless/survey_banner.vue';
import { GlBanner } from '@gitlab/ui';
+import SurveyBanner from '~/serverless/survey_banner.vue';
describe('Knative survey banner', () => {
let wrapper;
diff --git a/spec/frontend/serverless/utils.js b/spec/frontend/serverless/utils.js
index 5ce2e37d493..ba451b7d573 100644
--- a/spec/frontend/serverless/utils.js
+++ b/spec/frontend/serverless/utils.js
@@ -1,3 +1,4 @@
+// eslint-disable-next-line import/prefer-default-export
export const adjustMetricQuery = data => {
const updatedMetric = data.metrics;
@@ -15,6 +16,3 @@ export const adjustMetricQuery = data => {
updatedMetric.queries = queries;
return updatedMetric;
};
-
-// prevent babel-plugin-rewire from generating an invalid default during karma tests
-export default () => {};
diff --git a/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap b/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
index da571af3a0d..4c1ab4a499c 100644
--- a/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
+++ b/spec/frontend/sidebar/__snapshots__/confidential_issue_sidebar_spec.js.snap
@@ -49,8 +49,6 @@ exports[`Confidential Issue Sidebar Block renders for confidential = false and i
</div>
</div>
-
- <!---->
</div>
`;
@@ -111,8 +109,6 @@ exports[`Confidential Issue Sidebar Block renders for confidential = false and i
</div>
</div>
-
- <!---->
</div>
`;
@@ -164,8 +160,6 @@ exports[`Confidential Issue Sidebar Block renders for confidential = true and is
</div>
</div>
-
- <!---->
</div>
`;
@@ -225,7 +219,5 @@ exports[`Confidential Issue Sidebar Block renders for confidential = true and is
</div>
</div>
-
- <!---->
</div>
`;
diff --git a/spec/frontend/sidebar/confidential/__snapshots__/edit_form_spec.js.snap b/spec/frontend/sidebar/confidential/__snapshots__/edit_form_spec.js.snap
new file mode 100644
index 00000000000..d33f6c7f389
--- /dev/null
+++ b/spec/frontend/sidebar/confidential/__snapshots__/edit_form_spec.js.snap
@@ -0,0 +1,50 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Edit Form Dropdown when confidential renders on or off text based on confidentiality 1`] = `
+<div
+ class="dropdown show"
+ toggleform="function () {}"
+ updateconfidentialattribute="function () {}"
+>
+ <div
+ class="dropdown-menu sidebar-item-warning-message"
+ >
+ <div>
+ <p>
+ <gl-sprintf-stub
+ message="You are going to turn off the confidentiality. This means %{strongStart}everyone%{strongEnd} will be able to see and leave a comment on this %{issuableType}."
+ />
+ </p>
+
+ <edit-form-buttons-stub
+ confidential="true"
+ fullpath=""
+ />
+ </div>
+ </div>
+</div>
+`;
+
+exports[`Edit Form Dropdown when not confidential renders "You are going to turn on the confidentiality." in the 1`] = `
+<div
+ class="dropdown show"
+ toggleform="function () {}"
+ updateconfidentialattribute="function () {}"
+>
+ <div
+ class="dropdown-menu sidebar-item-warning-message"
+ >
+ <div>
+ <p>
+ <gl-sprintf-stub
+ message="You are going to turn on the confidentiality. This means that only team members with %{strongStart}at least Reporter access%{strongEnd} are able to see and leave comments on the %{issuableType}."
+ />
+ </p>
+
+ <edit-form-buttons-stub
+ fullpath=""
+ />
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js b/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
index 15493d3087f..2f11c6a07c2 100644
--- a/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
+++ b/spec/frontend/sidebar/confidential/edit_form_buttons_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
import EditFormButtons from '~/sidebar/components/confidential/edit_form_buttons.vue';
import eventHub from '~/sidebar/event_hub';
import createStore from '~/notes/stores';
-import waitForPromises from 'helpers/wait_for_promises';
-import flash from '~/flash';
+import { deprecatedCreateFlash as flash } from '~/flash';
jest.mock('~/sidebar/event_hub', () => ({ $emit: jest.fn() }));
jest.mock('~/flash');
@@ -14,12 +14,7 @@ describe('Edit Form Buttons', () => {
let store;
const findConfidentialToggle = () => wrapper.find('[data-testid="confidential-toggle"]');
- const createComponent = ({
- props = {},
- data = {},
- confidentialApolloSidebar = false,
- resolved = true,
- }) => {
+ const createComponent = ({ props = {}, data = {}, resolved = true }) => {
store = createStore();
if (resolved) {
jest.spyOn(store, 'dispatch').mockResolvedValue();
@@ -38,11 +33,6 @@ describe('Edit Form Buttons', () => {
...data,
};
},
- provide: {
- glFeatures: {
- confidentialApolloSidebar,
- },
- },
store,
});
};
@@ -54,9 +44,11 @@ describe('Edit Form Buttons', () => {
describe('when isLoading', () => {
beforeEach(() => {
- createComponent({});
-
- wrapper.vm.$store.state.noteableData.confidential = false;
+ createComponent({
+ props: {
+ confidential: false,
+ },
+ });
});
it('renders "Applying" in the toggle button', () => {
@@ -78,6 +70,9 @@ describe('Edit Form Buttons', () => {
data: {
isLoading: false,
},
+ props: {
+ confidential: false,
+ },
});
expect(findConfidentialToggle().text()).toBe('Turn On');
@@ -90,70 +85,63 @@ describe('Edit Form Buttons', () => {
data: {
isLoading: false,
},
+ props: {
+ confidential: true,
+ },
});
-
- wrapper.vm.$store.state.noteableData.confidential = true;
});
it('renders on or off text based on confidentiality', () => {
expect(findConfidentialToggle().text()).toBe('Turn Off');
});
-
- describe('when clicking on the confidential toggle', () => {
- it('emits updateConfidentialAttribute', () => {
- findConfidentialToggle().trigger('click');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('updateConfidentialAttribute');
- });
- });
});
- describe('when confidentialApolloSidebar is turned on', () => {
- const isConfidential = true;
+ describe('when succeeds', () => {
+ beforeEach(() => {
+ createComponent({ data: { isLoading: false }, props: { confidential: true } });
+ findConfidentialToggle().trigger('click');
+ });
- describe('when succeeds', () => {
- beforeEach(() => {
- createComponent({ data: { isLoading: false }, confidentialApolloSidebar: true });
- wrapper.vm.$store.state.noteableData.confidential = isConfidential;
- findConfidentialToggle().trigger('click');
+ it('dispatches the correct action', () => {
+ expect(store.dispatch).toHaveBeenCalledWith('updateConfidentialityOnIssuable', {
+ confidential: false,
+ fullPath: '',
});
+ });
- it('dispatches the correct action', () => {
- expect(store.dispatch).toHaveBeenCalledWith('updateConfidentialityOnIssue', {
- confidential: !isConfidential,
- fullPath: '',
- });
+ it('resets loading', () => {
+ return waitForPromises().then(() => {
+ expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
+ });
- it('resets loading', () => {
- return waitForPromises().then(() => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
- });
+ it('emits close form', () => {
+ return waitForPromises().then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('closeConfidentialityForm');
});
+ });
- it('emits close form', () => {
- return waitForPromises().then(() => {
- expect(eventHub.$emit).toHaveBeenCalledWith('closeConfidentialityForm');
- });
+ it('emits updateOnConfidentiality event', () => {
+ return waitForPromises().then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('updateIssuableConfidentiality', false);
});
});
+ });
- describe('when fails', () => {
- beforeEach(() => {
- createComponent({
- data: { isLoading: false },
- confidentialApolloSidebar: true,
- resolved: false,
- });
- wrapper.vm.$store.state.noteableData.confidential = isConfidential;
- findConfidentialToggle().trigger('click');
+ describe('when fails', () => {
+ beforeEach(() => {
+ createComponent({
+ data: { isLoading: false },
+ props: { confidential: true },
+ resolved: false,
});
+ findConfidentialToggle().trigger('click');
+ });
- it('calls flash with the correct message', () => {
- expect(flash).toHaveBeenCalledWith(
- 'Something went wrong trying to change the confidentiality of this issue',
- );
- });
+ it('calls flash with the correct message', () => {
+ expect(flash).toHaveBeenCalledWith(
+ 'Something went wrong trying to change the confidentiality of this issue',
+ );
});
});
});
diff --git a/spec/frontend/sidebar/confidential/edit_form_spec.js b/spec/frontend/sidebar/confidential/edit_form_spec.js
index a22bbe5ae0d..56f163eecd1 100644
--- a/spec/frontend/sidebar/confidential/edit_form_spec.js
+++ b/spec/frontend/sidebar/confidential/edit_form_spec.js
@@ -12,6 +12,7 @@ describe('Edit Form Dropdown', () => {
...props,
isLoading: false,
fullPath: '',
+ issuableType: 'issue',
},
});
};
@@ -22,26 +23,26 @@ describe('Edit Form Dropdown', () => {
});
describe('when not confidential', () => {
- it('renders "You are going to turn off the confidentiality." in the ', () => {
+ it('renders "You are going to turn on the confidentiality." in the ', () => {
createComponent({
- isConfidential: false,
+ confidential: false,
toggleForm,
updateConfidentialAttribute,
});
- expect(wrapper.find('p').text()).toContain('You are going to turn on the confidentiality.');
+ expect(wrapper.element).toMatchSnapshot();
});
});
describe('when confidential', () => {
it('renders on or off text based on confidentiality', () => {
createComponent({
- isConfidential: true,
+ confidential: true,
toggleForm,
updateConfidentialAttribute,
});
- expect(wrapper.find('p').text()).toContain('You are going to turn off the confidentiality.');
+ expect(wrapper.element).toMatchSnapshot();
});
});
});
diff --git a/spec/frontend/sidebar/confidential_issue_sidebar_spec.js b/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
index 06cf1e6166c..bc2df9305d0 100644
--- a/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
+++ b/spec/frontend/sidebar/confidential_issue_sidebar_spec.js
@@ -1,13 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import ConfidentialIssueSidebar from '~/sidebar/components/confidential/confidential_issue_sidebar.vue';
import EditForm from '~/sidebar/components/confidential/edit_form.vue';
-import SidebarService from '~/sidebar/services/sidebar_service';
-import createFlash from '~/flash';
-import RecaptchaModal from '~/vue_shared/components/recaptcha_modal.vue';
import createStore from '~/notes/stores';
-import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import eventHub from '~/sidebar/event_hub';
+import * as types from '~/notes/stores/mutation_types';
jest.mock('~/flash');
jest.mock('~/sidebar/services/sidebar_service');
@@ -20,32 +17,14 @@ describe('Confidential Issue Sidebar Block', () => {
.fn()
.mockResolvedValue({ data: { issueSetConfidential: { issue: { confidential: true } } } });
- const findRecaptchaModal = () => wrapper.find(RecaptchaModal);
-
- const triggerUpdateConfidentialAttribute = () => {
- wrapper.setData({ edit: true });
- return (
- // wait for edit form to become visible
- wrapper.vm
- .$nextTick()
- .then(() => {
- eventHub.$emit('updateConfidentialAttribute');
- })
- // wait for reCAPTCHA modal to render
- .then(() => wrapper.vm.$nextTick())
- );
- };
-
const createComponent = ({ propsData, data = {} }) => {
const store = createStore();
- const service = new SidebarService();
wrapper = shallowMount(ConfidentialIssueSidebar, {
store,
data() {
return data;
},
propsData: {
- service,
iid: '',
fullPath: '',
...propsData,
@@ -133,61 +112,48 @@ describe('Confidential Issue Sidebar Block', () => {
property: 'confidentiality',
});
});
-
- describe('for successful update', () => {
- beforeEach(() => {
- SidebarService.prototype.update.mockResolvedValue({ data: 'irrelevant' });
+ });
+ describe('computed confidential', () => {
+ beforeEach(() => {
+ createComponent({
+ propsData: {
+ isEditable: true,
+ },
});
+ });
- it('reloads the page', () =>
- triggerUpdateConfidentialAttribute().then(() => {
- expect(window.location.reload).toHaveBeenCalled();
- }));
+ it('returns false when noteableData is not present', () => {
+ wrapper.vm.$store.commit(types.SET_NOTEABLE_DATA, null);
- it('does not show an error message', () =>
- triggerUpdateConfidentialAttribute().then(() => {
- expect(createFlash).not.toHaveBeenCalled();
- }));
+ expect(wrapper.vm.confidential).toBe(false);
});
- describe('for update error', () => {
- beforeEach(() => {
- SidebarService.prototype.update.mockRejectedValue(new Error('updating failed!'));
- });
-
- it('does not reload the page', () =>
- triggerUpdateConfidentialAttribute().then(() => {
- expect(window.location.reload).not.toHaveBeenCalled();
- }));
+ it('returns true when noteableData has confidential attr as true', () => {
+ wrapper.vm.$store.commit(types.SET_NOTEABLE_DATA, {});
+ wrapper.vm.$store.commit(types.SET_ISSUE_CONFIDENTIAL, true);
- it('shows an error message', () =>
- triggerUpdateConfidentialAttribute().then(() => {
- expect(createFlash).toHaveBeenCalled();
- }));
+ expect(wrapper.vm.confidential).toBe(true);
});
- describe('for spam error', () => {
- beforeEach(() => {
- SidebarService.prototype.update.mockRejectedValue({ name: 'SpamError' });
- });
+ it('returns false when noteableData has confidential attr as false', () => {
+ wrapper.vm.$store.commit(types.SET_NOTEABLE_DATA, {});
+ wrapper.vm.$store.commit(types.SET_ISSUE_CONFIDENTIAL, false);
+
+ expect(wrapper.vm.confidential).toBe(false);
+ });
- it('does not reload the page', () =>
- triggerUpdateConfidentialAttribute().then(() => {
- expect(window.location.reload).not.toHaveBeenCalled();
- }));
+ it('returns true when confidential attr is true', () => {
+ wrapper.vm.$store.commit(types.SET_NOTEABLE_DATA, {});
+ wrapper.vm.$store.commit(types.SET_ISSUE_CONFIDENTIAL, true);
- it('does not show an error message', () =>
- triggerUpdateConfidentialAttribute().then(() => {
- expect(createFlash).not.toHaveBeenCalled();
- }));
+ expect(wrapper.vm.confidential).toBe(true);
+ });
- it('shows a reCAPTCHA modal', () => {
- expect(findRecaptchaModal().exists()).toBe(false);
+ it('returns false when confidential attr is false', () => {
+ wrapper.vm.$store.commit(types.SET_NOTEABLE_DATA, {});
+ wrapper.vm.$store.commit(types.SET_ISSUE_CONFIDENTIAL, false);
- return triggerUpdateConfidentialAttribute().then(() => {
- expect(findRecaptchaModal().exists()).toBe(true);
- });
- });
+ expect(wrapper.vm.confidential).toBe(false);
});
});
});
diff --git a/spec/frontend/sidebar/lock/__snapshots__/edit_form_spec.js.snap b/spec/frontend/sidebar/lock/__snapshots__/edit_form_spec.js.snap
new file mode 100644
index 00000000000..18d4df297df
--- /dev/null
+++ b/spec/frontend/sidebar/lock/__snapshots__/edit_form_spec.js.snap
@@ -0,0 +1,79 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Edit Form Dropdown In issue page when locked the appropriate warning text is rendered 1`] = `
+<div
+ class="dropdown-menu sidebar-item-warning-message"
+ data-testid="warning-text"
+>
+ <p
+ class="text"
+ >
+ <gl-sprintf-stub
+ message="Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment."
+ />
+ </p>
+
+ <edit-form-buttons-stub
+ islocked="true"
+ issuabledisplayname="issue"
+ />
+</div>
+`;
+
+exports[`Edit Form Dropdown In issue page when unlocked the appropriate warning text is rendered 1`] = `
+<div
+ class="dropdown-menu sidebar-item-warning-message"
+ data-testid="warning-text"
+>
+ <p
+ class="text"
+ >
+ <gl-sprintf-stub
+ message="Lock this %{issuableDisplayName}? Only %{strongStart}project members%{strongEnd} will be able to comment."
+ />
+ </p>
+
+ <edit-form-buttons-stub
+ issuabledisplayname="issue"
+ />
+</div>
+`;
+
+exports[`Edit Form Dropdown In merge request page when locked the appropriate warning text is rendered 1`] = `
+<div
+ class="dropdown-menu sidebar-item-warning-message"
+ data-testid="warning-text"
+>
+ <p
+ class="text"
+ >
+ <gl-sprintf-stub
+ message="Unlock this %{issuableDisplayName}? %{strongStart}Everyone%{strongEnd} will be able to comment."
+ />
+ </p>
+
+ <edit-form-buttons-stub
+ islocked="true"
+ issuabledisplayname="merge request"
+ />
+</div>
+`;
+
+exports[`Edit Form Dropdown In merge request page when unlocked the appropriate warning text is rendered 1`] = `
+<div
+ class="dropdown-menu sidebar-item-warning-message"
+ data-testid="warning-text"
+>
+ <p
+ class="text"
+ >
+ <gl-sprintf-stub
+ message="Lock this %{issuableDisplayName}? Only %{strongStart}project members%{strongEnd} will be able to comment."
+ />
+ </p>
+
+ <edit-form-buttons-stub
+ issuabledisplayname="merge request"
+ />
+</div>
+`;
diff --git a/spec/frontend/sidebar/lock/constants.js b/spec/frontend/sidebar/lock/constants.js
new file mode 100644
index 00000000000..b9f08e9286d
--- /dev/null
+++ b/spec/frontend/sidebar/lock/constants.js
@@ -0,0 +1,2 @@
+export const ISSUABLE_TYPE_ISSUE = 'issue';
+export const ISSUABLE_TYPE_MR = 'merge request';
diff --git a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
index 66f9237ce97..de1da3456f8 100644
--- a/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
+++ b/spec/frontend/sidebar/lock/edit_form_buttons_spec.js
@@ -1,31 +1,178 @@
import { shallowMount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
import EditFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
+import eventHub from '~/sidebar/event_hub';
+import { deprecatedCreateFlash as flash } from '~/flash';
+import createStore from '~/notes/stores';
+import { createStore as createMrStore } from '~/mr_notes/stores';
+import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants';
+
+jest.mock('~/sidebar/event_hub', () => ({ $emit: jest.fn() }));
+jest.mock('~/flash');
describe('EditFormButtons', () => {
let wrapper;
+ let store;
+ let issuableType;
+ let issuableDisplayName;
+
+ const setIssuableType = pageType => {
+ issuableType = pageType;
+ issuableDisplayName = issuableType.replace(/_/g, ' ');
+ };
+
+ const findLockToggle = () => wrapper.find('[data-testid="lock-toggle"]');
+ const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const mountComponent = propsData => shallowMount(EditFormButtons, { propsData });
+ const createComponent = ({ props = {}, data = {}, resolved = true }) => {
+ store = issuableType === ISSUABLE_TYPE_ISSUE ? createStore() : createMrStore();
+
+ if (resolved) {
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
+ } else {
+ jest.spyOn(store, 'dispatch').mockRejectedValue();
+ }
+
+ wrapper = shallowMount(EditFormButtons, {
+ store,
+ provide: {
+ fullPath: '',
+ },
+ propsData: {
+ isLocked: false,
+ issuableDisplayName,
+ ...props,
+ },
+ data() {
+ return {
+ isLoading: false,
+ ...data,
+ };
+ },
+ });
+ };
afterEach(() => {
wrapper.destroy();
wrapper = null;
});
- it('displays "Unlock" when locked', () => {
- wrapper = mountComponent({
- isLocked: true,
- updateLockedAttribute: () => {},
+ describe.each`
+ pageType
+ ${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
+ `('In $pageType page', ({ pageType }) => {
+ beforeEach(() => {
+ setIssuableType(pageType);
});
- expect(wrapper.text()).toContain('Unlock');
- });
+ describe('when isLoading', () => {
+ beforeEach(() => {
+ createComponent({ data: { isLoading: true } });
+ });
+
+ it('renders "Applying" in the toggle button', () => {
+ expect(findLockToggle().text()).toBe('Applying');
+ });
+
+ it('disables the toggle button', () => {
+ expect(findLockToggle().attributes('disabled')).toBe('disabled');
+ });
- it('displays "Lock" when unlocked', () => {
- wrapper = mountComponent({
- isLocked: false,
- updateLockedAttribute: () => {},
+ it('displays the GlLoadingIcon', () => {
+ expect(findGlLoadingIcon().exists()).toBe(true);
+ });
});
- expect(wrapper.text()).toContain('Lock');
+ describe.each`
+ isLocked | toggleText | statusText
+ ${false} | ${'Lock'} | ${'unlocked'}
+ ${true} | ${'Unlock'} | ${'locked'}
+ `('when $statusText', ({ isLocked, toggleText }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ isLocked,
+ },
+ });
+ });
+
+ it(`toggle button displays "${toggleText}"`, () => {
+ expect(findLockToggle().text()).toContain(toggleText);
+ });
+
+ describe('when toggled', () => {
+ describe(`when resolved`, () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ isLocked,
+ },
+ resolved: true,
+ });
+ findLockToggle().trigger('click');
+ });
+
+ it('dispatches the correct action', () => {
+ expect(store.dispatch).toHaveBeenCalledWith('updateLockedAttribute', {
+ locked: !isLocked,
+ fullPath: '',
+ });
+ });
+
+ it('resets loading', async () => {
+ await wrapper.vm.$nextTick().then(() => {
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ it('emits close form', () => {
+ return wrapper.vm.$nextTick().then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
+ });
+ });
+
+ it('does not flash an error message', () => {
+ expect(flash).not.toHaveBeenCalled();
+ });
+ });
+
+ describe(`when not resolved`, () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ isLocked,
+ },
+ resolved: false,
+ });
+ findLockToggle().trigger('click');
+ });
+
+ it('dispatches the correct action', () => {
+ expect(store.dispatch).toHaveBeenCalledWith('updateLockedAttribute', {
+ locked: !isLocked,
+ fullPath: '',
+ });
+ });
+
+ it('resets loading', async () => {
+ await wrapper.vm.$nextTick().then(() => {
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ it('emits close form', () => {
+ return wrapper.vm.$nextTick().then(() => {
+ expect(eventHub.$emit).toHaveBeenCalledWith('closeLockForm');
+ });
+ });
+
+ it('calls flash with the correct message', () => {
+ expect(flash).toHaveBeenCalledWith(
+ `Something went wrong trying to change the locked state of this ${issuableDisplayName}`,
+ );
+ });
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/sidebar/lock/edit_form_spec.js b/spec/frontend/sidebar/lock/edit_form_spec.js
index ec10a999a40..b1c3bfe3ef5 100644
--- a/spec/frontend/sidebar/lock/edit_form_spec.js
+++ b/spec/frontend/sidebar/lock/edit_form_spec.js
@@ -1,37 +1,54 @@
-import Vue from 'vue';
-import editForm from '~/sidebar/components/lock/edit_form.vue';
+import { shallowMount } from '@vue/test-utils';
+import EditForm from '~/sidebar/components/lock/edit_form.vue';
+import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants';
-describe('EditForm', () => {
- let vm1;
- let vm2;
+describe('Edit Form Dropdown', () => {
+ let wrapper;
+ let issuableType; // Either ISSUABLE_TYPE_ISSUE or ISSUABLE_TYPE_MR
+ let issuableDisplayName;
- beforeEach(() => {
- const Component = Vue.extend(editForm);
- const toggleForm = () => {};
- const updateLockedAttribute = () => {};
+ const setIssuableType = pageType => {
+ issuableType = pageType;
+ issuableDisplayName = issuableType.replace(/_/g, ' ');
+ };
- vm1 = new Component({
- propsData: {
- isLocked: true,
- toggleForm,
- updateLockedAttribute,
- issuableType: 'issue',
- },
- }).$mount();
+ const findWarningText = () => wrapper.find('[data-testid="warning-text"]');
- vm2 = new Component({
+ const createComponent = ({ props }) => {
+ wrapper = shallowMount(EditForm, {
propsData: {
isLocked: false,
- toggleForm,
- updateLockedAttribute,
- issuableType: 'merge_request',
+ issuableDisplayName,
+ ...props,
},
- }).$mount();
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
});
- it('renders on the appropriate warning text', () => {
- expect(vm1.$el.innerHTML.includes('Unlock this issue?')).toBe(true);
+ describe.each`
+ pageType
+ ${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
+ `('In $pageType page', ({ pageType }) => {
+ beforeEach(() => {
+ setIssuableType(pageType);
+ });
+
+ describe.each`
+ isLocked | lockStatusText
+ ${false} | ${'unlocked'}
+ ${true} | ${'locked'}
+ `('when $lockStatusText', ({ isLocked }) => {
+ beforeEach(() => {
+ createComponent({ props: { isLocked } });
+ });
- expect(vm2.$el.innerHTML.includes('Lock this merge request?')).toBe(true);
+ it(`the appropriate warning text is rendered`, () => {
+ expect(findWarningText().element).toMatchSnapshot();
+ });
+ });
});
});
diff --git a/spec/frontend/sidebar/lock/issuable_lock_form_spec.js b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
new file mode 100644
index 00000000000..ab1423a9bbb
--- /dev/null
+++ b/spec/frontend/sidebar/lock/issuable_lock_form_spec.js
@@ -0,0 +1,133 @@
+import { shallowMount } from '@vue/test-utils';
+import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
+import IssuableLockForm from '~/sidebar/components/lock/issuable_lock_form.vue';
+import EditForm from '~/sidebar/components/lock/edit_form.vue';
+import createStore from '~/notes/stores';
+import { createStore as createMrStore } from '~/mr_notes/stores';
+import { ISSUABLE_TYPE_ISSUE, ISSUABLE_TYPE_MR } from './constants';
+
+describe('IssuableLockForm', () => {
+ let wrapper;
+ let store;
+ let issuableType; // Either ISSUABLE_TYPE_ISSUE or ISSUABLE_TYPE_MR
+
+ const setIssuableType = pageType => {
+ issuableType = pageType;
+ };
+
+ const findSidebarCollapseIcon = () => wrapper.find('[data-testid="sidebar-collapse-icon"]');
+ const findLockStatus = () => wrapper.find('[data-testid="lock-status"]');
+ const findEditLink = () => wrapper.find('[data-testid="edit-link"]');
+ const findEditForm = () => wrapper.find(EditForm);
+
+ const initStore = isLocked => {
+ if (issuableType === ISSUABLE_TYPE_ISSUE) {
+ store = createStore();
+ store.getters.getNoteableData.targetType = 'issue';
+ } else {
+ store = createMrStore();
+ }
+ store.getters.getNoteableData.discussion_locked = isLocked;
+ };
+
+ const createComponent = ({ props = {} }) => {
+ wrapper = shallowMount(IssuableLockForm, {
+ store,
+ propsData: {
+ isEditable: true,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe.each`
+ pageType
+ ${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
+ `('In $pageType page', ({ pageType }) => {
+ beforeEach(() => {
+ setIssuableType(pageType);
+ });
+
+ describe.each`
+ isLocked
+ ${false} | ${true}
+ `(`renders for isLocked = $isLocked`, ({ isLocked }) => {
+ beforeEach(() => {
+ initStore(isLocked);
+ createComponent({});
+ });
+
+ it('shows the lock status', () => {
+ expect(findLockStatus().text()).toBe(isLocked ? 'Locked' : 'Unlocked');
+ });
+
+ describe('edit form', () => {
+ let isEditable;
+ beforeEach(() => {
+ isEditable = false;
+ createComponent({ props: { isEditable } });
+ });
+
+ describe('when not editable', () => {
+ it('does not display the edit form when opened if not editable', () => {
+ expect(findEditForm().exists()).toBe(false);
+ findSidebarCollapseIcon().trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findEditForm().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when editable', () => {
+ beforeEach(() => {
+ isEditable = true;
+ createComponent({ props: { isEditable } });
+ });
+
+ it('shows the editable status', () => {
+ expect(findEditLink().exists()).toBe(isEditable);
+ expect(findEditLink().text()).toBe('Edit');
+ });
+
+ describe("when 'Edit' is clicked", () => {
+ it('displays the edit form when editable', () => {
+ expect(findEditForm().exists()).toBe(false);
+ findEditLink().trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findEditForm().exists()).toBe(true);
+ });
+ });
+
+ it('tracks the event ', () => {
+ const spy = mockTracking('_category_', wrapper.element, jest.spyOn);
+ triggerEvent(findEditLink().element);
+
+ expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
+ label: 'right_sidebar',
+ property: 'lock_issue',
+ });
+ });
+ });
+
+ describe('When sidebar is collapsed', () => {
+ it('displays the edit form when opened', () => {
+ expect(findEditForm().exists()).toBe(false);
+ findSidebarCollapseIcon().trigger('click');
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(findEditForm().exists()).toBe(true);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/lock/lock_issue_sidebar_spec.js b/spec/frontend/sidebar/lock/lock_issue_sidebar_spec.js
deleted file mode 100644
index 00997326d87..00000000000
--- a/spec/frontend/sidebar/lock/lock_issue_sidebar_spec.js
+++ /dev/null
@@ -1,99 +0,0 @@
-import Vue from 'vue';
-import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
-import lockIssueSidebar from '~/sidebar/components/lock/lock_issue_sidebar.vue';
-
-describe('LockIssueSidebar', () => {
- let vm1;
- let vm2;
-
- beforeEach(() => {
- const Component = Vue.extend(lockIssueSidebar);
-
- const mediator = {
- service: {
- update: Promise.resolve(true),
- },
-
- store: {
- isLockDialogOpen: false,
- },
- };
-
- vm1 = new Component({
- propsData: {
- isLocked: true,
- isEditable: true,
- mediator,
- issuableType: 'issue',
- },
- }).$mount();
-
- vm2 = new Component({
- propsData: {
- isLocked: false,
- isEditable: false,
- mediator,
- issuableType: 'merge_request',
- },
- }).$mount();
- });
-
- it('shows if locked and/or editable', () => {
- expect(vm1.$el.innerHTML.includes('Edit')).toBe(true);
-
- expect(vm1.$el.innerHTML.includes('Locked')).toBe(true);
-
- expect(vm2.$el.innerHTML.includes('Unlocked')).toBe(true);
- });
-
- it('displays the edit form when editable', done => {
- expect(vm1.isLockDialogOpen).toBe(false);
-
- vm1.$el.querySelector('.lock-edit').click();
-
- expect(vm1.isLockDialogOpen).toBe(true);
-
- vm1.$nextTick(() => {
- expect(vm1.$el.innerHTML.includes('Unlock this issue?')).toBe(true);
-
- done();
- });
- });
-
- it('tracks an event when "Edit" is clicked', () => {
- const spy = mockTracking('_category_', vm1.$el, jest.spyOn);
- triggerEvent('.lock-edit');
-
- expect(spy).toHaveBeenCalledWith('_category_', 'click_edit_button', {
- label: 'right_sidebar',
- property: 'lock_issue',
- });
- });
-
- it('displays the edit form when opened from collapsed state', done => {
- expect(vm1.isLockDialogOpen).toBe(false);
-
- vm1.$el.querySelector('.sidebar-collapsed-icon').click();
-
- expect(vm1.isLockDialogOpen).toBe(true);
-
- setImmediate(() => {
- expect(vm1.$el.innerHTML.includes('Unlock this issue?')).toBe(true);
-
- done();
- });
- });
-
- it('does not display the edit form when opened from collapsed state if not editable', done => {
- expect(vm2.isLockDialogOpen).toBe(false);
-
- vm2.$el.querySelector('.sidebar-collapsed-icon').click();
-
- Vue.nextTick()
- .then(() => {
- expect(vm2.isLockDialogOpen).toBe(false);
- })
- .then(done)
- .catch(done.fail);
- });
-});
diff --git a/spec/frontend/sidebar/todo_spec.js b/spec/frontend/sidebar/todo_spec.js
index 18b621cd12d..e56a78989eb 100644
--- a/spec/frontend/sidebar/todo_spec.js
+++ b/spec/frontend/sidebar/todo_spec.js
@@ -36,7 +36,7 @@ describe('SidebarTodo', () => {
it.each`
isTodo | iconClass | label | icon
- ${false} | ${''} | ${'Add a To Do'} | ${'todo-add'}
+ ${false} | ${''} | ${'Add a To-Do'} | ${'todo-add'}
${true} | ${'todo-undone'} | ${'Mark as done'} | ${'todo-done'}
`(
'renders proper button when `isTodo` prop is `$isTodo`',
diff --git a/spec/frontend/snippet/collapsible_input_spec.js b/spec/frontend/snippet/collapsible_input_spec.js
index acd15164c95..aa017964437 100644
--- a/spec/frontend/snippet/collapsible_input_spec.js
+++ b/spec/frontend/snippet/collapsible_input_spec.js
@@ -1,5 +1,5 @@
-import setupCollapsibleInputs from '~/snippet/collapsible_input';
import { setHTMLFixture } from 'helpers/fixtures';
+import setupCollapsibleInputs from '~/snippet/collapsible_input';
describe('~/snippet/collapsible_input', () => {
let formEl;
diff --git a/spec/frontend/snippet/snippet_bundle_spec.js b/spec/frontend/snippet/snippet_bundle_spec.js
index 38d05243c65..ad69a91fe89 100644
--- a/spec/frontend/snippet/snippet_bundle_spec.js
+++ b/spec/frontend/snippet/snippet_bundle_spec.js
@@ -1,6 +1,6 @@
+import { setHTMLFixture } from 'helpers/fixtures';
import Editor from '~/editor/editor_lite';
import initEditor from '~/snippet/snippet_bundle';
-import { setHTMLFixture } from 'helpers/fixtures';
jest.mock('~/editor/editor_lite', () => jest.fn());
diff --git a/spec/frontend/snippet/snippet_edit_spec.js b/spec/frontend/snippet/snippet_edit_spec.js
index cfe5062c86b..7c12c0cac03 100644
--- a/spec/frontend/snippet/snippet_edit_spec.js
+++ b/spec/frontend/snippet/snippet_edit_spec.js
@@ -1,9 +1,8 @@
import '~/snippet/snippet_edit';
+import { triggerDOMEvent } from 'jest/helpers/dom_events_helper';
import { SnippetEditInit } from '~/snippets';
import initSnippet from '~/snippet/snippet_bundle';
-import { triggerDOMEvent } from 'jest/helpers/dom_events_helper';
-
jest.mock('~/snippet/snippet_bundle');
jest.mock('~/snippets');
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
index 959bc24eef6..1cf1ee74ddf 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
@@ -1,25 +1,21 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Snippet Blob Edit component rendering matches the snapshot 1`] = `
+exports[`Snippet Blob Edit component with loaded blob matches snapshot 1`] = `
<div
- class="form-group file-editor"
+ class="file-holder snippet"
>
- <label>
- File
- </label>
+ <blob-header-edit-stub
+ candelete="true"
+ data-qa-selector="file_name_field"
+ id="blob_local_7_file_path"
+ value="foo/bar/test.md"
+ />
- <div
- class="file-holder snippet"
- >
- <blob-header-edit-stub
- data-qa-selector="file_name_field"
- value="lorem.txt"
- />
-
- <blob-content-edit-stub
- filename="lorem.txt"
- value="Lorem ipsum dolor sit amet, consectetur adipiscing elit."
- />
- </div>
+ <blob-content-edit-stub
+ fileglobalid="blob_local_7"
+ filename="foo/bar/test.md"
+ value="Lorem ipsum dolar sit amet,
+consectetur adipiscing elit."
+ />
</div>
`;
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 297ad16b681..6020d595e3f 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
@@ -60,7 +60,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
<a
aria-label="Leave zen mode"
- class="zen-control zen-control-leave js-zen-leave gl-text-gray-700"
+ class="zen-control zen-control-leave js-zen-leave gl-text-gray-500"
href="#"
>
<icon-stub
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index d2265dfd506..980855a0615 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -1,134 +1,157 @@
-import { shallowMount } from '@vue/test-utils';
-import Flash from '~/flash';
-
+import { ApolloMutation } from 'vue-apollo';
import { GlLoadingIcon } from '@gitlab/ui';
-import { redirectTo } from '~/lib/utils/url_utility';
-
+import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import { deprecatedCreateFlash as Flash } from '~/flash';
+import * as urlUtils from '~/lib/utils/url_utility';
import SnippetEditApp from '~/snippets/components/edit.vue';
import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
-import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
+import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
import TitleField from '~/vue_shared/components/form/title.vue';
import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
-import { SNIPPET_CREATE_MUTATION_ERROR, SNIPPET_UPDATE_MUTATION_ERROR } from '~/snippets/constants';
-
+import { SNIPPET_VISIBILITY_PRIVATE } from '~/snippets/constants';
import UpdateSnippetMutation from '~/snippets/mutations/updateSnippet.mutation.graphql';
import CreateSnippetMutation from '~/snippets/mutations/createSnippet.mutation.graphql';
-
-import waitForPromises from 'helpers/wait_for_promises';
-import { ApolloMutation } from 'vue-apollo';
-
-jest.mock('~/lib/utils/url_utility', () => ({
- redirectTo: jest.fn().mockName('redirectTo'),
-}));
+import { testEntries } from '../test_utils';
jest.mock('~/flash');
-let flashSpy;
-
-const rawProjectPathMock = '/project/path';
-const newlyEditedSnippetUrl = 'http://foo.bar';
-const apiError = { message: 'Ufff' };
-const mutationError = 'Bummer';
-
-const attachedFilePath1 = 'foo/bar';
-const attachedFilePath2 = 'alpha/beta';
-
-const actionWithContent = {
- content: 'Foo Bar',
-};
-const actionWithoutContent = {
- content: '',
-};
+const TEST_UPLOADED_FILES = ['foo/bar.txt', 'alpha/beta.js'];
+const TEST_API_ERROR = 'Ufff';
+const TEST_MUTATION_ERROR = 'Bummer';
-const defaultProps = {
- snippetGid: 'gid://gitlab/PersonalSnippet/42',
- markdownPreviewPath: 'http://preview.foo.bar',
- markdownDocsPath: 'http://docs.foo.bar',
-};
-const defaultData = {
- blobsActions: {
- ...actionWithContent,
- action: '',
+const TEST_ACTIONS = {
+ NO_CONTENT: {
+ ...testEntries.created.diff,
+ content: '',
+ },
+ NO_PATH: {
+ ...testEntries.created.diff,
+ filePath: '',
+ },
+ VALID: {
+ ...testEntries.created.diff,
},
};
+const TEST_WEB_URL = '/snippets/7';
+
+const createTestSnippet = () => ({
+ webUrl: TEST_WEB_URL,
+ id: 7,
+ title: 'Snippet Title',
+ description: 'Lorem ipsum snippet desc',
+ visibilityLevel: SNIPPET_VISIBILITY_PRIVATE,
+});
+
describe('Snippet Edit app', () => {
let wrapper;
- const resolveMutate = jest.fn().mockResolvedValue({
- data: {
- updateSnippet: {
- errors: [],
- snippet: {
- webUrl: newlyEditedSnippetUrl,
+ const mutationTypes = {
+ RESOLVE: jest.fn().mockResolvedValue({
+ data: {
+ updateSnippet: {
+ errors: [],
+ snippet: createTestSnippet(),
},
},
- },
- });
-
- const resolveMutateWithErrors = jest.fn().mockResolvedValue({
- data: {
- updateSnippet: {
- errors: [mutationError],
- snippet: {
- webUrl: newlyEditedSnippetUrl,
+ }),
+ RESOLVE_WITH_ERRORS: jest.fn().mockResolvedValue({
+ data: {
+ updateSnippet: {
+ errors: [TEST_MUTATION_ERROR],
+ snippet: createTestSnippet(),
+ },
+ createSnippet: {
+ errors: [TEST_MUTATION_ERROR],
+ snippet: null,
},
},
- createSnippet: {
- errors: [mutationError],
- snippet: null,
- },
- },
- });
-
- const rejectMutation = jest.fn().mockRejectedValue(apiError);
-
- const mutationTypes = {
- RESOLVE: resolveMutate,
- RESOLVE_WITH_ERRORS: resolveMutateWithErrors,
- REJECT: rejectMutation,
+ }),
+ REJECT: jest.fn().mockRejectedValue(TEST_API_ERROR),
};
function createComponent({
- props = defaultProps,
- data = {},
+ props = {},
loading = false,
mutationRes = mutationTypes.RESOLVE,
} = {}) {
- const $apollo = {
- queries: {
- snippet: {
- loading,
- },
- },
- mutate: mutationRes,
- };
+ if (wrapper) {
+ throw new Error('wrapper already exists');
+ }
wrapper = shallowMount(SnippetEditApp, {
- mocks: { $apollo },
+ mocks: {
+ $apollo: {
+ queries: {
+ snippet: { loading },
+ },
+ mutate: mutationRes,
+ },
+ },
stubs: {
- FormFooterActions,
ApolloMutation,
+ FormFooterActions,
},
propsData: {
+ snippetGid: 'gid://gitlab/PersonalSnippet/42',
+ markdownPreviewPath: 'http://preview.foo.bar',
+ markdownDocsPath: 'http://docs.foo.bar',
...props,
},
- data() {
- return data;
- },
});
-
- flashSpy = jest.spyOn(wrapper.vm, 'flashAPIFailure');
}
+ beforeEach(() => {
+ jest.spyOn(urlUtils, 'redirectTo').mockImplementation();
+ });
+
afterEach(() => {
wrapper.destroy();
+ wrapper = null;
});
+ const findBlobActions = () => wrapper.find(SnippetBlobActionsEdit);
const findSubmitButton = () => wrapper.find('[data-testid="snippet-submit-btn"]');
- const findCancellButton = () => wrapper.find('[data-testid="snippet-cancel-btn"]');
+ const findCancelButton = () => wrapper.find('[data-testid="snippet-cancel-btn"]');
+ const hasDisabledSubmit = () => Boolean(findSubmitButton().attributes('disabled'));
+
const clickSubmitBtn = () => wrapper.find('[data-testid="snippet-edit-form"]').trigger('submit');
+ const triggerBlobActions = actions => findBlobActions().vm.$emit('actions', actions);
+ const setUploadFilesHtml = paths => {
+ wrapper.vm.$el.innerHTML = paths.map(path => `<input name="files[]" value="${path}">`).join('');
+ };
+ const getApiData = ({
+ id,
+ title = '',
+ description = '',
+ visibilityLevel = SNIPPET_VISIBILITY_PRIVATE,
+ } = {}) => ({
+ id,
+ title,
+ description,
+ visibilityLevel,
+ blobActions: [],
+ });
+
+ // Ideally we wouldn't call this method directly, but we don't have a way to trigger
+ // apollo responses yet.
+ const loadSnippet = (...edges) => {
+ if (edges.length) {
+ wrapper.setData({
+ snippet: edges[0],
+ });
+ }
+
+ wrapper.vm.onSnippetFetch({
+ data: {
+ snippets: {
+ edges,
+ },
+ },
+ });
+ };
describe('rendering', () => {
it('renders loader while the query is in flight', () => {
@@ -136,295 +159,163 @@ describe('Snippet Edit app', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
});
- it('renders all required components', () => {
- createComponent();
-
- expect(wrapper.contains(TitleField)).toBe(true);
- expect(wrapper.contains(SnippetDescriptionEdit)).toBe(true);
- expect(wrapper.contains(SnippetBlobEdit)).toBe(true);
- expect(wrapper.contains(SnippetVisibilityEdit)).toBe(true);
- expect(wrapper.contains(FormFooterActions)).toBe(true);
- });
-
- it('does not fail if there is no snippet yet (new snippet creation)', () => {
- const snippetGid = '';
- createComponent({
- props: {
- ...defaultProps,
- snippetGid,
- },
- });
-
- expect(wrapper.props('snippetGid')).toBe(snippetGid);
- });
+ it.each([[{}], [{ snippetGid: '' }]])(
+ 'should render all required components with %s',
+ props => {
+ createComponent(props);
- it.each`
- title | blobsActions | expectation
- ${''} | ${{}} | ${true}
- ${''} | ${{ actionWithContent }} | ${true}
- ${''} | ${{ actionWithoutContent }} | ${true}
- ${'foo'} | ${{}} | ${true}
- ${'foo'} | ${{ actionWithoutContent }} | ${true}
- ${'foo'} | ${{ actionWithoutContent, actionWithContent }} | ${true}
- ${'foo'} | ${{ actionWithContent }} | ${false}
- `(
- 'disables submit button unless both title and content for all blobs are present',
- ({ title, blobsActions, expectation }) => {
- createComponent({
- data: {
- snippet: { title },
- blobsActions,
- },
- });
- const isBtnDisabled = Boolean(findSubmitButton().attributes('disabled'));
- expect(isBtnDisabled).toBe(expectation);
+ expect(wrapper.contains(TitleField)).toBe(true);
+ expect(wrapper.contains(SnippetDescriptionEdit)).toBe(true);
+ expect(wrapper.contains(SnippetVisibilityEdit)).toBe(true);
+ expect(wrapper.contains(FormFooterActions)).toBe(true);
+ expect(findBlobActions().exists()).toBe(true);
},
);
it.each`
- isNew | status | expectation
- ${true} | ${`new`} | ${`/snippets`}
- ${false} | ${`existing`} | ${newlyEditedSnippetUrl}
- `('sets correct href for the cancel button on a $status snippet', ({ isNew, expectation }) => {
- createComponent({
- data: {
- snippet: { webUrl: newlyEditedSnippetUrl },
- newSnippet: isNew,
- },
- });
+ title | actions | shouldDisable
+ ${''} | ${[]} | ${true}
+ ${''} | ${[TEST_ACTIONS.VALID]} | ${true}
+ ${'foo'} | ${[]} | ${false}
+ ${'foo'} | ${[TEST_ACTIONS.VALID]} | ${false}
+ ${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_CONTENT]} | ${true}
+ ${'foo'} | ${[TEST_ACTIONS.VALID, TEST_ACTIONS.NO_PATH]} | ${true}
+ `(
+ 'should handle submit disable (title=$title, actions=$actions, shouldDisable=$shouldDisable)',
+ async ({ title, actions, shouldDisable }) => {
+ createComponent();
- expect(findCancellButton().attributes('href')).toBe(expectation);
- });
- });
+ loadSnippet({ title });
+ triggerBlobActions(actions);
- describe('functionality', () => {
- describe('form submission handling', () => {
- it('does not submit unchanged blobs', () => {
- const foo = {
- action: '',
- };
- const bar = {
- action: 'update',
- };
- createComponent({
- data: {
- blobsActions: {
- foo,
- bar,
- },
- },
- });
- clickSubmitBtn();
+ await wrapper.vm.$nextTick();
- return waitForPromises().then(() => {
- expect(resolveMutate).toHaveBeenCalledWith(
- expect.objectContaining({ variables: { input: { files: [bar] } } }),
- );
- });
- });
+ expect(hasDisabledSubmit()).toBe(shouldDisable);
+ },
+ );
- it.each`
- newSnippet | projectPath | mutation | mutationName
- ${true} | ${rawProjectPathMock} | ${CreateSnippetMutation} | ${'CreateSnippetMutation with projectPath'}
- ${true} | ${''} | ${CreateSnippetMutation} | ${'CreateSnippetMutation without projectPath'}
- ${false} | ${rawProjectPathMock} | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation with projectPath'}
- ${false} | ${''} | ${UpdateSnippetMutation} | ${'UpdateSnippetMutation without projectPath'}
- `('should submit $mutationName correctly', ({ newSnippet, projectPath, mutation }) => {
+ it.each`
+ projectPath | snippetArg | expectation
+ ${''} | ${[]} | ${'/-/snippets'}
+ ${'project/path'} | ${[]} | ${'/project/path/-/snippets'}
+ ${''} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
+ ${'project/path'} | ${[createTestSnippet()]} | ${TEST_WEB_URL}
+ `(
+ 'should set cancel href when (projectPath=$projectPath, snippet=$snippetArg)',
+ async ({ projectPath, snippetArg, expectation }) => {
createComponent({
- data: {
- newSnippet,
- ...defaultData,
- },
- props: {
- ...defaultProps,
- projectPath,
- },
+ props: { projectPath },
});
- const mutationPayload = {
- mutation,
- variables: {
- input: newSnippet ? expect.objectContaining({ projectPath }) : expect.any(Object),
- },
- };
-
- clickSubmitBtn();
-
- expect(resolveMutate).toHaveBeenCalledWith(mutationPayload);
- });
+ loadSnippet(...snippetArg);
- it('redirects to snippet view on successful mutation', () => {
- createComponent();
- clickSubmitBtn();
+ await wrapper.vm.$nextTick();
- return waitForPromises().then(() => {
- expect(redirectTo).toHaveBeenCalledWith(newlyEditedSnippetUrl);
- });
- });
+ expect(findCancelButton().attributes('href')).toBe(expectation);
+ },
+ );
+ });
+ describe('functionality', () => {
+ describe('form submission handling', () => {
it.each`
- newSnippet | projectPath | mutationName
- ${true} | ${rawProjectPathMock} | ${'CreateSnippetMutation with projectPath'}
- ${true} | ${''} | ${'CreateSnippetMutation without projectPath'}
- ${false} | ${rawProjectPathMock} | ${'UpdateSnippetMutation with projectPath'}
- ${false} | ${''} | ${'UpdateSnippetMutation without projectPath'}
+ snippetArg | projectPath | uploadedFiles | input | mutation
+ ${[]} | ${'project/path'} | ${[]} | ${{ ...getApiData(), projectPath: 'project/path', uploadedFiles: [] }} | ${CreateSnippetMutation}
+ ${[]} | ${''} | ${[]} | ${{ ...getApiData(), projectPath: '', uploadedFiles: [] }} | ${CreateSnippetMutation}
+ ${[]} | ${''} | ${TEST_UPLOADED_FILES} | ${{ ...getApiData(), projectPath: '', uploadedFiles: TEST_UPLOADED_FILES }} | ${CreateSnippetMutation}
+ ${[createTestSnippet()]} | ${'project/path'} | ${[]} | ${getApiData(createTestSnippet())} | ${UpdateSnippetMutation}
+ ${[createTestSnippet()]} | ${''} | ${[]} | ${getApiData(createTestSnippet())} | ${UpdateSnippetMutation}
`(
- 'does not redirect to snippet view if the seemingly successful' +
- ' $mutationName response contains errors',
- ({ newSnippet, projectPath }) => {
+ 'should submit mutation with (snippet=$snippetArg, projectPath=$projectPath, uploadedFiles=$uploadedFiles)',
+ async ({ snippetArg, projectPath, uploadedFiles, mutation, input }) => {
createComponent({
- data: {
- newSnippet,
- },
props: {
- ...defaultProps,
projectPath,
},
- mutationRes: mutationTypes.RESOLVE_WITH_ERRORS,
});
+ loadSnippet(...snippetArg);
+ setUploadFilesHtml(uploadedFiles);
+
+ await wrapper.vm.$nextTick();
clickSubmitBtn();
- return waitForPromises().then(() => {
- expect(redirectTo).not.toHaveBeenCalled();
- expect(flashSpy).toHaveBeenCalledWith(mutationError);
+ expect(mutationTypes.RESOLVE).toHaveBeenCalledWith({
+ mutation,
+ variables: {
+ input,
+ },
});
},
);
- it('flashes an error if mutation failed', () => {
- createComponent({
- mutationRes: mutationTypes.REJECT,
- });
+ it('should redirect to snippet view on successful mutation', async () => {
+ createComponent();
+ loadSnippet(createTestSnippet());
clickSubmitBtn();
- return waitForPromises().then(() => {
- expect(redirectTo).not.toHaveBeenCalled();
- expect(flashSpy).toHaveBeenCalledWith(apiError);
- });
+ await waitForPromises();
+
+ expect(urlUtils.redirectTo).toHaveBeenCalledWith(TEST_WEB_URL);
});
it.each`
- isNew | status | expectation
- ${true} | ${`new`} | ${SNIPPET_CREATE_MUTATION_ERROR.replace('%{err}', '')}
- ${false} | ${`existing`} | ${SNIPPET_UPDATE_MUTATION_ERROR.replace('%{err}', '')}
+ snippetArg | projectPath | mutationRes | expectMessage
+ ${[]} | ${'project/path'} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
+ ${[]} | ${''} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't create snippet: ${TEST_MUTATION_ERROR}`}
+ ${[]} | ${''} | ${mutationTypes.REJECT} | ${`Can't create snippet: ${TEST_API_ERROR}`}
+ ${[createTestSnippet()]} | ${'project/path'} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
+ ${[createTestSnippet()]} | ${''} | ${mutationTypes.RESOLVE_WITH_ERRORS} | ${`Can't update snippet: ${TEST_MUTATION_ERROR}`}
`(
- `renders the correct error message if mutation fails for $status snippet`,
- ({ isNew, expectation }) => {
+ 'should flash error with (snippet=$snippetArg, projectPath=$projectPath)',
+ async ({ snippetArg, projectPath, mutationRes, expectMessage }) => {
createComponent({
- data: {
- newSnippet: isNew,
+ props: {
+ projectPath,
},
- mutationRes: mutationTypes.REJECT,
+ mutationRes,
});
+ loadSnippet(...snippetArg);
clickSubmitBtn();
- return waitForPromises().then(() => {
- expect(Flash).toHaveBeenCalledWith(expect.stringContaining(expectation));
- });
+ await waitForPromises();
+
+ expect(urlUtils.redirectTo).not.toHaveBeenCalled();
+ expect(Flash).toHaveBeenCalledWith(expectMessage);
},
);
});
- describe('correctly includes attached files into the mutation', () => {
- const createMutationPayload = expectation => {
- return expect.objectContaining({
- variables: {
- input: expect.objectContaining({ uploadedFiles: expectation }),
- },
- });
- };
-
- const updateMutationPayload = () => {
- return expect.objectContaining({
- variables: {
- input: expect.not.objectContaining({ uploadedFiles: expect.anything() }),
- },
- });
- };
-
- it.each`
- paths | expectation
- ${[attachedFilePath1]} | ${[attachedFilePath1]}
- ${[attachedFilePath1, attachedFilePath2]} | ${[attachedFilePath1, attachedFilePath2]}
- ${[]} | ${[]}
- `(`correctly sends paths for $paths.length files`, ({ paths, expectation }) => {
- createComponent({
- data: {
- newSnippet: true,
- },
- });
-
- const fixtures = paths.map(path => {
- return path ? `<input name="files[]" value="${path}">` : undefined;
- });
- wrapper.vm.$el.innerHTML += fixtures.join('');
-
- clickSubmitBtn();
-
- expect(resolveMutate).toHaveBeenCalledWith(createMutationPayload(expectation));
- });
-
- it(`neither fails nor sends 'uploadedFiles' to update mutation`, () => {
- createComponent();
-
- clickSubmitBtn();
- expect(resolveMutate).toHaveBeenCalledWith(updateMutationPayload());
- });
- });
-
describe('on before unload', () => {
- let event;
- let returnValueSetter;
-
- const bootstrap = data => {
- createComponent({
- data,
- });
-
- event = new Event('beforeunload');
- returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
- };
-
- it('does not prevent page navigation if there are no blobs', () => {
- bootstrap();
- window.dispatchEvent(event);
-
- expect(returnValueSetter).not.toHaveBeenCalled();
- });
-
- it('does not prevent page navigation if there are no changes to the blobs content', () => {
- bootstrap({
- blobsActions: {
- foo: {
- ...actionWithContent,
- action: '',
- },
- },
- });
- window.dispatchEvent(event);
+ it.each`
+ condition | expectPrevented | action
+ ${'there are no actions'} | ${false} | ${() => triggerBlobActions([])}
+ ${'there are actions'} | ${true} | ${() => triggerBlobActions([testEntries.updated.diff])}
+ ${'the snippet is being saved'} | ${false} | ${() => clickSubmitBtn()}
+ `(
+ 'handles before unload prevent when $condition (expectPrevented=$expectPrevented)',
+ ({ expectPrevented, action }) => {
+ createComponent();
+ loadSnippet();
- expect(returnValueSetter).not.toHaveBeenCalled();
- });
+ action();
- it('prevents page navigation if there are some changes in the snippet content', () => {
- bootstrap({
- blobsActions: {
- foo: {
- ...actionWithContent,
- action: 'update',
- },
- },
- });
+ const event = new Event('beforeunload');
+ const returnValueSetter = jest.spyOn(event, 'returnValue', 'set');
- window.dispatchEvent(event);
+ window.dispatchEvent(event);
- expect(returnValueSetter).toHaveBeenCalledWith(
- 'Are you sure you want to lose unsaved changes?',
- );
- });
+ if (expectPrevented) {
+ expect(returnValueSetter).toHaveBeenCalledWith(
+ 'Are you sure you want to lose unsaved changes?',
+ );
+ } else {
+ expect(returnValueSetter).not.toHaveBeenCalled();
+ }
+ },
+ );
});
});
});
diff --git a/spec/frontend/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js
index b5446e70028..8cccbb83d54 100644
--- a/spec/frontend/snippets/components/show_spec.js
+++ b/spec/frontend/snippets/components/show_spec.js
@@ -1,19 +1,27 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
+import { shallowMount } from '@vue/test-utils';
import SnippetApp from '~/snippets/components/show.vue';
import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import SnippetHeader from '~/snippets/components/snippet_header.vue';
import SnippetTitle from '~/snippets/components/snippet_title.vue';
import SnippetBlob from '~/snippets/components/snippet_blob_view.vue';
-import { GlLoadingIcon } from '@gitlab/ui';
-import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
+import CloneDropdownButton from '~/vue_shared/components/clone_dropdown.vue';
-import { shallowMount } from '@vue/test-utils';
-import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
+import {
+ SNIPPET_VISIBILITY_INTERNAL,
+ SNIPPET_VISIBILITY_PRIVATE,
+ SNIPPET_VISIBILITY_PUBLIC,
+} from '~/snippets/constants';
describe('Snippet view app', () => {
let wrapper;
const defaultProps = {
snippetGid: 'gid://gitlab/PersonalSnippet/42',
};
+ const webUrl = 'http://foo.bar';
+ const dummyHTTPUrl = webUrl;
+ const dummySSHUrl = 'ssh://foo.bar';
function createComponent({ props = defaultProps, data = {}, loading = false } = {}) {
const $apollo = {
@@ -72,4 +80,47 @@ describe('Snippet view app', () => {
expect(blobs.at(0).props('blob')).toEqual(Blob);
expect(blobs.at(1).props('blob')).toEqual(BinaryBlob);
});
+
+ describe('Embed dropdown rendering', () => {
+ it.each`
+ visibilityLevel | condition | isRendered
+ ${SNIPPET_VISIBILITY_INTERNAL} | ${'not render'} | ${false}
+ ${SNIPPET_VISIBILITY_PRIVATE} | ${'not render'} | ${false}
+ ${'foo'} | ${'not render'} | ${false}
+ ${SNIPPET_VISIBILITY_PUBLIC} | ${'render'} | ${true}
+ `('does $condition blob-embeddable by default', ({ visibilityLevel, isRendered }) => {
+ createComponent({
+ data: {
+ snippet: {
+ visibilityLevel,
+ webUrl,
+ },
+ },
+ });
+ expect(wrapper.contains(BlobEmbeddable)).toBe(isRendered);
+ });
+ });
+
+ describe('Clone button rendering', () => {
+ it.each`
+ httpUrlToRepo | sshUrlToRepo | shouldRender | isRendered
+ ${null} | ${null} | ${'Should not'} | ${false}
+ ${null} | ${dummySSHUrl} | ${'Should'} | ${true}
+ ${dummyHTTPUrl} | ${null} | ${'Should'} | ${true}
+ ${dummyHTTPUrl} | ${dummySSHUrl} | ${'Should'} | ${true}
+ `(
+ '$shouldRender render "Clone" button when `httpUrlToRepo` is $httpUrlToRepo and `sshUrlToRepo` is $sshUrlToRepo',
+ ({ httpUrlToRepo, sshUrlToRepo, isRendered }) => {
+ createComponent({
+ data: {
+ snippet: {
+ sshUrlToRepo,
+ httpUrlToRepo,
+ },
+ },
+ });
+ expect(wrapper.contains(CloneDropdownButton)).toBe(isRendered);
+ },
+ );
+ });
});
diff --git a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
new file mode 100644
index 00000000000..8b2051008d7
--- /dev/null
+++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
@@ -0,0 +1,301 @@
+import { times } from 'lodash';
+import { shallowMount } from '@vue/test-utils';
+import SnippetBlobActionsEdit from '~/snippets/components/snippet_blob_actions_edit.vue';
+import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
+import {
+ SNIPPET_MAX_BLOBS,
+ SNIPPET_BLOB_ACTION_CREATE,
+ SNIPPET_BLOB_ACTION_MOVE,
+} from '~/snippets/constants';
+import { testEntries, createBlobFromTestEntry } from '../test_utils';
+
+const TEST_BLOBS = [
+ createBlobFromTestEntry(testEntries.updated),
+ createBlobFromTestEntry(testEntries.deleted),
+];
+
+const TEST_BLOBS_UNLOADED = TEST_BLOBS.map(blob => ({ ...blob, content: '', isLoaded: false }));
+
+describe('snippets/components/snippet_blob_actions_edit', () => {
+ let wrapper;
+
+ const createComponent = (props = {}, snippetMultipleFiles = true) => {
+ wrapper = shallowMount(SnippetBlobActionsEdit, {
+ propsData: {
+ initBlobs: TEST_BLOBS,
+ ...props,
+ },
+ provide: {
+ glFeatures: {
+ snippetMultipleFiles,
+ },
+ },
+ });
+ };
+
+ const findLabel = () => wrapper.find('label');
+ const findBlobEdits = () => wrapper.findAll(SnippetBlobEdit);
+ const findBlobsData = () =>
+ findBlobEdits().wrappers.map(x => ({
+ blob: x.props('blob'),
+ classes: x.classes(),
+ }));
+ const findFirstBlobEdit = () => findBlobEdits().at(0);
+ const findAddButton = () => wrapper.find('[data-testid="add_button"]');
+ const getLastActions = () => {
+ const events = wrapper.emitted().actions;
+
+ return events[events.length - 1]?.[0];
+ };
+ const buildBlobsDataExpectation = blobs =>
+ blobs.map((blob, index) => ({
+ blob: {
+ ...blob,
+ id: expect.stringMatching('blob_local_'),
+ },
+ classes: index > 0 ? ['gl-mt-3'] : [],
+ }));
+ const triggerBlobDelete = idx =>
+ findBlobEdits()
+ .at(idx)
+ .vm.$emit('delete');
+ const triggerBlobUpdate = (idx, props) =>
+ findBlobEdits()
+ .at(idx)
+ .vm.$emit('blob-updated', props);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe.each`
+ featureFlag | label | showDelete | showAdd
+ ${true} | ${'Files'} | ${true} | ${true}
+ ${false} | ${'File'} | ${false} | ${false}
+ `('with feature flag = $featureFlag', ({ featureFlag, label, showDelete, showAdd }) => {
+ beforeEach(() => {
+ createComponent({}, featureFlag);
+ });
+
+ it('renders label', () => {
+ expect(findLabel().text()).toBe(label);
+ });
+
+ it(`renders delete button (show=${showDelete})`, () => {
+ expect(findFirstBlobEdit().props()).toMatchObject({
+ showDelete,
+ canDelete: true,
+ });
+ });
+
+ it(`renders add button (show=${showAdd})`, () => {
+ expect(findAddButton().exists()).toBe(showAdd);
+ });
+ });
+
+ describe('with default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('emits no actions', () => {
+ expect(getLastActions()).toEqual([]);
+ });
+
+ it('shows blobs', () => {
+ expect(findBlobsData()).toEqual(buildBlobsDataExpectation(TEST_BLOBS_UNLOADED));
+ });
+
+ it('shows add button', () => {
+ const button = findAddButton();
+
+ expect(button.text()).toBe(`Add another file ${TEST_BLOBS.length}/${SNIPPET_MAX_BLOBS}`);
+ expect(button.props('disabled')).toBe(false);
+ });
+
+ describe('when add is clicked', () => {
+ beforeEach(() => {
+ findAddButton().vm.$emit('click');
+ });
+
+ it('adds blob with empty content', () => {
+ expect(findBlobsData()).toEqual(
+ buildBlobsDataExpectation([
+ ...TEST_BLOBS_UNLOADED,
+ {
+ content: '',
+ isLoaded: true,
+ path: '',
+ },
+ ]),
+ );
+ });
+
+ it('emits action', () => {
+ expect(getLastActions()).toEqual([
+ expect.objectContaining({
+ action: SNIPPET_BLOB_ACTION_CREATE,
+ }),
+ ]);
+ });
+ });
+
+ describe('when blob is deleted', () => {
+ beforeEach(() => {
+ triggerBlobDelete(1);
+ });
+
+ it('removes blob', () => {
+ expect(findBlobsData()).toEqual(buildBlobsDataExpectation(TEST_BLOBS_UNLOADED.slice(0, 1)));
+ });
+
+ it('emits action', () => {
+ expect(getLastActions()).toEqual([
+ expect.objectContaining({
+ ...testEntries.deleted.diff,
+ content: '',
+ }),
+ ]);
+ });
+ });
+
+ describe('when blob changes path', () => {
+ beforeEach(() => {
+ triggerBlobUpdate(0, { path: 'new/path' });
+ });
+
+ it('renames blob', () => {
+ expect(findBlobsData()[0]).toMatchObject({
+ blob: {
+ path: 'new/path',
+ },
+ });
+ });
+
+ it('emits action', () => {
+ expect(getLastActions()).toMatchObject([
+ {
+ action: SNIPPET_BLOB_ACTION_MOVE,
+ filePath: 'new/path',
+ previousPath: testEntries.updated.diff.filePath,
+ },
+ ]);
+ });
+ });
+
+ describe('when blob emits new content', () => {
+ const { content } = testEntries.updated.diff;
+ const originalContent = `${content}\noriginal content\n`;
+
+ beforeEach(() => {
+ triggerBlobUpdate(0, { content: originalContent });
+ });
+
+ it('loads new content', () => {
+ expect(findBlobsData()[0]).toMatchObject({
+ blob: {
+ content: originalContent,
+ isLoaded: true,
+ },
+ });
+ });
+
+ it('does not emit an action', () => {
+ expect(getLastActions()).toEqual([]);
+ });
+
+ it('emits an action when content changes again', async () => {
+ triggerBlobUpdate(0, { content });
+
+ await wrapper.vm.$nextTick();
+
+ expect(getLastActions()).toEqual([testEntries.updated.diff]);
+ });
+ });
+ });
+
+ describe('with 1 blob', () => {
+ beforeEach(() => {
+ createComponent({ initBlobs: [createBlobFromTestEntry(testEntries.created)] });
+ });
+
+ it('disables delete button', () => {
+ expect(findBlobEdits()).toHaveLength(1);
+ expect(
+ findBlobEdits()
+ .at(0)
+ .props(),
+ ).toMatchObject({
+ showDelete: true,
+ canDelete: false,
+ });
+ });
+
+ describe(`when added ${SNIPPET_MAX_BLOBS} files`, () => {
+ let addButton;
+
+ beforeEach(() => {
+ addButton = findAddButton();
+
+ times(SNIPPET_MAX_BLOBS - 1, () => addButton.vm.$emit('click'));
+ });
+
+ it('should have blobs', () => {
+ expect(findBlobsData()).toHaveLength(SNIPPET_MAX_BLOBS);
+ });
+
+ it('should disable add button', () => {
+ expect(addButton.props('disabled')).toBe(true);
+ });
+ });
+ });
+
+ describe('with 0 init blob', () => {
+ beforeEach(() => {
+ createComponent({ initBlobs: [] });
+ });
+
+ it('shows 1 blob by default', () => {
+ expect(findBlobsData()).toEqual([
+ expect.objectContaining({
+ blob: {
+ id: expect.stringMatching('blob_local_'),
+ content: '',
+ path: '',
+ isLoaded: true,
+ },
+ }),
+ ]);
+ });
+
+ it('emits create action', () => {
+ expect(getLastActions()).toEqual([
+ {
+ action: SNIPPET_BLOB_ACTION_CREATE,
+ content: '',
+ filePath: '',
+ previousPath: '',
+ },
+ ]);
+ });
+ });
+
+ describe(`with ${SNIPPET_MAX_BLOBS} files`, () => {
+ beforeEach(() => {
+ const initBlobs = Array(SNIPPET_MAX_BLOBS)
+ .fill(1)
+ .map(() => createBlobFromTestEntry(testEntries.created));
+
+ createComponent({ initBlobs });
+ });
+
+ it('should have blobs', () => {
+ expect(findBlobsData()).toHaveLength(SNIPPET_MAX_BLOBS);
+ });
+
+ it('should disable add button', () => {
+ expect(findAddButton().props('disabled')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
index 009074b4558..188f9ae5cf1 100644
--- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
@@ -1,165 +1,168 @@
-import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
-import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
-import BlobContentEdit from '~/blob/components/blob_edit_content.vue';
import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import { TEST_HOST } from 'helpers/test_constants';
+import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
+import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
+import BlobContentEdit from '~/blob/components/blob_edit_content.vue';
import axios from '~/lib/utils/axios_utils';
import { joinPaths } from '~/lib/utils/url_utility';
-import waitForPromises from 'helpers/wait_for_promises';
-
-jest.mock('~/blob/utils', () => jest.fn());
-
-jest.mock('~/lib/utils/url_utility', () => ({
- getBaseURL: jest.fn().mockReturnValue('foo/'),
- joinPaths: jest
- .fn()
- .mockName('joinPaths')
- .mockReturnValue('contentApiURL'),
-}));
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
-let flashSpy;
+const TEST_ID = 'blob_local_7';
+const TEST_PATH = 'foo/bar/test.md';
+const TEST_RAW_PATH = '/gitlab/raw/path/to/blob/7';
+const TEST_FULL_PATH = joinPaths(TEST_HOST, TEST_RAW_PATH);
+const TEST_CONTENT = 'Lorem ipsum dolar sit amet,\nconsectetur adipiscing elit.';
+
+const TEST_BLOB = {
+ id: TEST_ID,
+ rawPath: TEST_RAW_PATH,
+ path: TEST_PATH,
+ content: '',
+ isLoaded: false,
+};
+
+const TEST_BLOB_LOADED = {
+ ...TEST_BLOB,
+ content: TEST_CONTENT,
+ isLoaded: true,
+};
describe('Snippet Blob Edit component', () => {
let wrapper;
let axiosMock;
- const contentMock = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.';
- const pathMock = 'lorem.txt';
- const rawPathMock = 'foo/bar';
- const blob = {
- path: pathMock,
- content: contentMock,
- rawPath: rawPathMock,
- };
- const findComponent = component => wrapper.find(component);
- function createComponent(props = {}, data = { isContentLoading: false }) {
+ const createComponent = (props = {}) => {
wrapper = shallowMount(SnippetBlobEdit, {
propsData: {
+ blob: TEST_BLOB,
...props,
},
- data() {
- return {
- ...data,
- };
- },
});
- flashSpy = jest.spyOn(wrapper.vm, 'flashAPIFailure');
- }
+ };
+
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findHeader = () => wrapper.find(BlobHeaderEdit);
+ const findContent = () => wrapper.find(BlobContentEdit);
+ const getLastUpdatedArgs = () => {
+ const event = wrapper.emitted()['blob-updated'];
+
+ return event?.[event.length - 1][0];
+ };
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
- createComponent();
+ axiosMock.onGet(TEST_FULL_PATH).reply(200, TEST_CONTENT);
});
afterEach(() => {
- axiosMock.restore();
wrapper.destroy();
+ wrapper = null;
+ axiosMock.restore();
});
- describe('rendering', () => {
- it('matches the snapshot', () => {
- createComponent({ blob });
- expect(wrapper.element).toMatchSnapshot();
+ describe('with not loaded blob', () => {
+ beforeEach(async () => {
+ createComponent();
});
- it('renders required components', () => {
- expect(findComponent(BlobHeaderEdit).exists()).toBe(true);
- expect(findComponent(BlobContentEdit).exists()).toBe(true);
+ it('shows blob header', () => {
+ expect(findHeader().props()).toMatchObject({
+ value: TEST_BLOB.path,
+ });
+ expect(findHeader().attributes('id')).toBe(`${TEST_ID}_file_path`);
});
- it('renders loader if existing blob is supplied but no content is fetched yet', () => {
- createComponent({ blob }, { isContentLoading: true });
- expect(wrapper.contains(GlLoadingIcon)).toBe(true);
- expect(findComponent(BlobContentEdit).exists()).toBe(false);
+ it('emits delete when deleted', () => {
+ expect(wrapper.emitted().delete).toBeUndefined();
+
+ findHeader().vm.$emit('delete');
+
+ expect(wrapper.emitted().delete).toHaveLength(1);
});
- it('does not render loader if when blob is not supplied', () => {
- createComponent();
- expect(wrapper.contains(GlLoadingIcon)).toBe(false);
- expect(findComponent(BlobContentEdit).exists()).toBe(true);
+ it('emits update when path changes', () => {
+ const newPath = 'new/path.md';
+
+ findHeader().vm.$emit('input', newPath);
+
+ expect(getLastUpdatedArgs()).toEqual({ path: newPath });
});
- });
- describe('functionality', () => {
- it('does not fail without blob', () => {
- const spy = jest.spyOn(global.console, 'error');
- createComponent({ blob: undefined });
+ it('emits update when content is loaded', async () => {
+ await waitForPromises();
- expect(spy).not.toHaveBeenCalled();
- expect(findComponent(BlobContentEdit).exists()).toBe(true);
+ expect(getLastUpdatedArgs()).toEqual({ content: TEST_CONTENT });
});
+ });
- it.each`
- emitter | prop
- ${BlobHeaderEdit} | ${'filePath'}
- ${BlobContentEdit} | ${'content'}
- `('emits "blob-updated" event when the $prop gets changed', ({ emitter, prop }) => {
- expect(wrapper.emitted('blob-updated')).toBeUndefined();
- const newValue = 'foo.bar';
- findComponent(emitter).vm.$emit('input', newValue);
-
- return nextTick().then(() => {
- expect(wrapper.emitted('blob-updated')[0]).toEqual([
- expect.objectContaining({
- [prop]: newValue,
- }),
- ]);
- });
+ describe('with error', () => {
+ beforeEach(() => {
+ axiosMock.reset();
+ axiosMock.onGet(TEST_FULL_PATH).replyOnce(500);
+ createComponent();
});
- describe('fetching blob content', () => {
- const bootstrapForExistingSnippet = resp => {
- createComponent({
- blob: {
- ...blob,
- content: '',
- },
- });
+ it('should call flash', async () => {
+ await waitForPromises();
- if (resp === 500) {
- axiosMock.onGet('contentApiURL').reply(500);
- } else {
- axiosMock.onGet('contentApiURL').reply(200, contentMock);
- }
- };
+ expect(createFlash).toHaveBeenCalledWith(
+ "Can't fetch content for the blob: Error: Request failed with status code 500",
+ );
+ });
+ });
- const bootstrapForNewSnippet = () => {
- createComponent();
- };
+ describe('with loaded blob', () => {
+ beforeEach(() => {
+ createComponent({ blob: TEST_BLOB_LOADED });
+ });
- it('fetches blob content with the additional query', () => {
- bootstrapForExistingSnippet();
+ it('matches snapshot', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
- return waitForPromises().then(() => {
- expect(joinPaths).toHaveBeenCalledWith('foo/', rawPathMock);
- expect(findComponent(BlobHeaderEdit).props('value')).toBe(pathMock);
- expect(findComponent(BlobContentEdit).props('value')).toBe(contentMock);
- });
- });
+ it('does not make API request', () => {
+ expect(axiosMock.history.get).toHaveLength(0);
+ });
+ });
- it('flashes the error message if fetching content fails', () => {
- bootstrapForExistingSnippet(500);
+ describe.each`
+ props | showLoading | showContent
+ ${{ blob: TEST_BLOB, canDelete: true, showDelete: true }} | ${true} | ${false}
+ ${{ blob: TEST_BLOB, canDelete: false, showDelete: false }} | ${true} | ${false}
+ ${{ blob: TEST_BLOB_LOADED }} | ${false} | ${true}
+ `('with $props', ({ props, showLoading, showContent }) => {
+ beforeEach(() => {
+ createComponent(props);
+ });
- return waitForPromises().then(() => {
- expect(flashSpy).toHaveBeenCalled();
- expect(findComponent(BlobContentEdit).props('value')).toBe('');
- });
+ it('shows blob header', () => {
+ const { canDelete = true, showDelete = false } = props;
+
+ expect(findHeader().props()).toMatchObject({
+ canDelete,
+ showDelete,
});
+ });
- it('does not fetch content for new snippet', () => {
- bootstrapForNewSnippet();
+ it(`handles loading icon (show=${showLoading})`, () => {
+ expect(findLoadingIcon().exists()).toBe(showLoading);
+ });
- return waitForPromises().then(() => {
- // we keep using waitForPromises to make sure we do not run failed test
- expect(findComponent(BlobHeaderEdit).props('value')).toBe('');
- expect(findComponent(BlobContentEdit).props('value')).toBe('');
- expect(joinPaths).not.toHaveBeenCalled();
+ it(`handles content (show=${showContent})`, () => {
+ expect(findContent().exists()).toBe(showContent);
+
+ if (showContent) {
+ expect(findContent().props()).toEqual({
+ value: TEST_BLOB_LOADED.content,
+ fileGlobalId: TEST_BLOB_LOADED.id,
+ fileName: TEST_BLOB_LOADED.path,
});
- });
+ }
});
});
});
diff --git a/spec/frontend/snippets/components/snippet_blob_view_spec.js b/spec/frontend/snippets/components/snippet_blob_view_spec.js
index c8f1c8fc8a9..9c4b2734a3f 100644
--- a/spec/frontend/snippets/components/snippet_blob_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_view_spec.js
@@ -1,7 +1,14 @@
+import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
+import {
+ Blob as BlobMock,
+ SimpleViewerMock,
+ RichViewerMock,
+ RichBlobContentMock,
+ SimpleBlobContentMock,
+} from 'jest/blob/components/mock_data';
import SnippetBlobView from '~/snippets/components/snippet_blob_view.vue';
import BlobHeader from '~/blob/components/blob_header.vue';
-import BlobEmbeddable from '~/blob/components/blob_embeddable.vue';
import BlobContent from '~/blob/components/blob_content.vue';
import {
BLOB_RENDER_EVENT_LOAD,
@@ -9,13 +16,7 @@ import {
BLOB_RENDER_ERRORS,
} from '~/blob/components/constants';
import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
-import {
- SNIPPET_VISIBILITY_PRIVATE,
- SNIPPET_VISIBILITY_INTERNAL,
- SNIPPET_VISIBILITY_PUBLIC,
-} from '~/snippets/constants';
-
-import { Blob as BlobMock, SimpleViewerMock, RichViewerMock } from 'jest/blob/components/mock_data';
+import { SNIPPET_VISIBILITY_PUBLIC } from '~/snippets/constants';
describe('Blob Embeddable', () => {
let wrapper;
@@ -72,18 +73,6 @@ describe('Blob Embeddable', () => {
expect(wrapper.find(BlobContent).exists()).toBe(true);
});
- it.each([SNIPPET_VISIBILITY_INTERNAL, SNIPPET_VISIBILITY_PRIVATE, 'foo'])(
- 'does not render blob-embeddable by default',
- visibilityLevel => {
- createComponent({
- snippetProps: {
- visibilityLevel,
- },
- });
- expect(wrapper.find(BlobEmbeddable).exists()).toBe(false);
- },
- );
-
it('sets simple viewer correctly', () => {
createComponent();
expect(wrapper.find(SimpleViewer).exists()).toBe(true);
@@ -128,6 +117,59 @@ describe('Blob Embeddable', () => {
expect(wrapper.find(BlobHeader).props('hasRenderError')).toBe(true);
});
+ describe('bob content in multi-file scenario', () => {
+ const SimpleBlobContentMock2 = {
+ ...SimpleBlobContentMock,
+ plainData: 'Another Plain Foo',
+ };
+ const RichBlobContentMock2 = {
+ ...SimpleBlobContentMock,
+ richData: 'Another Rich Foo',
+ };
+
+ it.each`
+ snippetBlobs | description | currentBlob | expectedContent
+ ${[SimpleBlobContentMock]} | ${'one existing textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData}
+ ${[RichBlobContentMock]} | ${'one existing rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData}
+ ${[SimpleBlobContentMock, RichBlobContentMock]} | ${'mixed blobs with current textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData}
+ ${[SimpleBlobContentMock, RichBlobContentMock]} | ${'mixed blobs with current rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData}
+ ${[SimpleBlobContentMock, SimpleBlobContentMock2]} | ${'textual blobs with current textual blob'} | ${SimpleBlobContentMock} | ${SimpleBlobContentMock.plainData}
+ ${[RichBlobContentMock, RichBlobContentMock2]} | ${'rich blobs with current rich blob'} | ${RichBlobContentMock} | ${RichBlobContentMock.richData}
+ `(
+ 'renders correct content for $description',
+ async ({ snippetBlobs, currentBlob, expectedContent }) => {
+ const apolloData = {
+ snippets: {
+ edges: [
+ {
+ node: {
+ blobs: snippetBlobs,
+ },
+ },
+ ],
+ },
+ };
+ createComponent({
+ blob: {
+ ...BlobMock,
+ path: currentBlob.path,
+ },
+ });
+
+ // mimic apollo's update
+ wrapper.setData({
+ blobContent: wrapper.vm.onContentUpdate(apolloData),
+ });
+
+ await nextTick();
+
+ const findContent = () => wrapper.find(BlobContent);
+
+ expect(findContent().props('content')).toBe(expectedContent);
+ },
+ );
+ });
+
describe('URLS with hash', () => {
beforeEach(() => {
window.location.hash = '#LC2';
diff --git a/spec/frontend/snippets/components/snippet_description_edit_spec.js b/spec/frontend/snippets/components/snippet_description_edit_spec.js
index 816ab4e48de..ff75515e71a 100644
--- a/spec/frontend/snippets/components/snippet_description_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_description_edit_spec.js
@@ -1,6 +1,6 @@
+import { shallowMount } from '@vue/test-utils';
import SnippetDescriptionEdit from '~/snippets/components/snippet_description_edit.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { shallowMount } from '@vue/test-utils';
describe('Snippet Description Edit component', () => {
let wrapper;
diff --git a/spec/frontend/snippets/components/snippet_description_view_spec.js b/spec/frontend/snippets/components/snippet_description_view_spec.js
index 46467ef311e..14f116f2aaf 100644
--- a/spec/frontend/snippets/components/snippet_description_view_spec.js
+++ b/spec/frontend/snippets/components/snippet_description_view_spec.js
@@ -1,5 +1,5 @@
-import SnippetDescription from '~/snippets/components/snippet_description_view.vue';
import { shallowMount } from '@vue/test-utils';
+import SnippetDescription from '~/snippets/components/snippet_description_view.vue';
describe('Snippet Description component', () => {
let wrapper;
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 0825da92118..da8cb2e6a8d 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -1,46 +1,19 @@
-import SnippetHeader from '~/snippets/components/snippet_header.vue';
-import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
import { ApolloMutation } from 'vue-apollo';
import { GlButton, GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
+import waitForPromises from 'helpers/wait_for_promises';
+import DeleteSnippetMutation from '~/snippets/mutations/deleteSnippet.mutation.graphql';
+import SnippetHeader from '~/snippets/components/snippet_header.vue';
describe('Snippet header component', () => {
let wrapper;
- const snippet = {
- id: 'gid://gitlab/PersonalSnippet/50',
- title: 'The property of Thor',
- visibilityLevel: 'private',
- webUrl: 'http://personal.dev.null/42',
- userPermissions: {
- adminSnippet: true,
- updateSnippet: true,
- reportSnippet: false,
- },
- project: null,
- author: {
- name: 'Thor Odinson',
- },
- blobs: [Blob],
- };
- const mutationVariables = {
- mutation: DeleteSnippetMutation,
- variables: {
- id: snippet.id,
- },
- };
- const errorMsg = 'Foo bar';
- const err = { message: errorMsg };
-
- const resolveMutate = jest.fn(() =>
- Promise.resolve({ data: { destroySnippet: { errors: [] } } }),
- );
- const rejectMutation = jest.fn(() => Promise.reject(err));
-
- const mutationTypes = {
- RESOLVE: resolveMutate,
- REJECT: rejectMutation,
- };
+ let snippet;
+ let mutationTypes;
+ let mutationVariables;
+
+ let errorMsg;
+ let err;
function createComponent({
loading = false,
@@ -63,7 +36,7 @@ describe('Snippet header component', () => {
mutate: mutationRes,
};
- wrapper = shallowMount(SnippetHeader, {
+ wrapper = mount(SnippetHeader, {
mocks: { $apollo },
propsData: {
snippet: {
@@ -76,6 +49,41 @@ describe('Snippet header component', () => {
});
}
+ beforeEach(() => {
+ snippet = {
+ id: 'gid://gitlab/PersonalSnippet/50',
+ title: 'The property of Thor',
+ visibilityLevel: 'private',
+ webUrl: 'http://personal.dev.null/42',
+ userPermissions: {
+ adminSnippet: true,
+ updateSnippet: true,
+ reportSnippet: false,
+ },
+ project: null,
+ author: {
+ name: 'Thor Odinson',
+ },
+ blobs: [Blob],
+ createdAt: new Date(Date.now() - 32 * 24 * 3600 * 1000).toISOString(),
+ };
+
+ mutationVariables = {
+ mutation: DeleteSnippetMutation,
+ variables: {
+ id: snippet.id,
+ },
+ };
+
+ errorMsg = 'Foo bar';
+ err = { message: errorMsg };
+
+ mutationTypes = {
+ RESOLVE: jest.fn(() => Promise.resolve({ data: { destroySnippet: { errors: [] } } })),
+ REJECT: jest.fn(() => Promise.reject(err)),
+ };
+ });
+
afterEach(() => {
wrapper.destroy();
});
@@ -85,6 +93,23 @@ describe('Snippet header component', () => {
expect(wrapper.find('.detail-page-header').exists()).toBe(true);
});
+ it('renders a message showing snippet creation date and author', () => {
+ createComponent();
+
+ const text = wrapper.find('[data-testid="authored-message"]').text();
+ expect(text).toContain('Authored 1 month ago by');
+ expect(text).toContain('Thor Odinson');
+ });
+
+ it('renders a message showing only snippet creation date if author is null', () => {
+ snippet.author = null;
+
+ createComponent();
+
+ const text = wrapper.find('[data-testid="authored-message"]').text();
+ expect(text).toBe('Authored 1 month ago');
+ });
+
it('renders action buttons based on permissions', () => {
createComponent({
permissions: {
@@ -163,14 +188,15 @@ describe('Snippet header component', () => {
expect(mutationTypes.RESOLVE).toHaveBeenCalledWith(mutationVariables);
});
- it('sets error message if mutation fails', () => {
+ it('sets error message if mutation fails', async () => {
createComponent({ mutationRes: mutationTypes.REJECT });
expect(Boolean(wrapper.vm.errorMessage)).toBe(false);
wrapper.vm.deleteSnippet();
- return wrapper.vm.$nextTick().then(() => {
- expect(wrapper.vm.errorMessage).toEqual(errorMsg);
- });
+
+ await waitForPromises();
+
+ expect(wrapper.vm.errorMessage).toEqual(errorMsg);
});
describe('in case of successful mutation, closes modal and redirects to correct listing', () => {
@@ -199,7 +225,7 @@ describe('Snippet header component', () => {
},
}).then(() => {
expect(wrapper.vm.closeDeleteModal).toHaveBeenCalled();
- expect(window.location.pathname).toBe(`${fullPath}/snippets`);
+ expect(window.location.pathname).toBe(`${fullPath}/-/snippets`);
});
});
});
diff --git a/spec/frontend/snippets/components/snippet_title_spec.js b/spec/frontend/snippets/components/snippet_title_spec.js
index 88261a75f6c..f201cfb19b7 100644
--- a/spec/frontend/snippets/components/snippet_title_spec.js
+++ b/spec/frontend/snippets/components/snippet_title_spec.js
@@ -1,7 +1,7 @@
-import SnippetTitle from '~/snippets/components/snippet_title.vue';
-import SnippetDescription from '~/snippets/components/snippet_description_view.vue';
import { GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import SnippetTitle from '~/snippets/components/snippet_title.vue';
+import SnippetDescription from '~/snippets/components/snippet_description_view.vue';
describe('Snippet header component', () => {
let wrapper;
diff --git a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
index 0bdef71bc08..a8df13787a5 100644
--- a/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_visibility_edit_spec.js
@@ -1,12 +1,12 @@
-import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import { GlFormRadio, GlIcon, GlFormRadioGroup, GlLink } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import SnippetVisibilityEdit from '~/snippets/components/snippet_visibility_edit.vue';
import {
SNIPPET_VISIBILITY,
SNIPPET_VISIBILITY_PRIVATE,
SNIPPET_VISIBILITY_INTERNAL,
SNIPPET_VISIBILITY_PUBLIC,
} from '~/snippets/constants';
-import { mount, shallowMount } from '@vue/test-utils';
describe('Snippet Visibility Edit component', () => {
let wrapper;
diff --git a/spec/frontend/snippets/test_utils.js b/spec/frontend/snippets/test_utils.js
new file mode 100644
index 00000000000..86262723157
--- /dev/null
+++ b/spec/frontend/snippets/test_utils.js
@@ -0,0 +1,76 @@
+import {
+ SNIPPET_BLOB_ACTION_CREATE,
+ SNIPPET_BLOB_ACTION_UPDATE,
+ SNIPPET_BLOB_ACTION_MOVE,
+ SNIPPET_BLOB_ACTION_DELETE,
+} from '~/snippets/constants';
+
+const CONTENT_1 = 'Lorem ipsum dolar\nSit amit\n\nGoodbye!\n';
+const CONTENT_2 = 'Lorem ipsum dolar sit amit.\n\nGoodbye!\n';
+
+export const testEntries = {
+ created: {
+ id: 'blob_1',
+ diff: {
+ action: SNIPPET_BLOB_ACTION_CREATE,
+ filePath: '/new/file',
+ previousPath: '/new/file',
+ content: CONTENT_1,
+ },
+ },
+ deleted: {
+ id: 'blob_2',
+ diff: {
+ action: SNIPPET_BLOB_ACTION_DELETE,
+ filePath: '/src/delete/me',
+ previousPath: '/src/delete/me',
+ content: CONTENT_1,
+ },
+ },
+ updated: {
+ id: 'blob_3',
+ origContent: CONTENT_1,
+ diff: {
+ action: SNIPPET_BLOB_ACTION_UPDATE,
+ filePath: '/lorem.md',
+ previousPath: '/lorem.md',
+ content: CONTENT_2,
+ },
+ },
+ renamed: {
+ id: 'blob_4',
+ diff: {
+ action: SNIPPET_BLOB_ACTION_MOVE,
+ filePath: '/dolar.md',
+ previousPath: '/ipsum.md',
+ content: CONTENT_1,
+ },
+ },
+ renamedAndUpdated: {
+ id: 'blob_5',
+ origContent: CONTENT_1,
+ diff: {
+ action: SNIPPET_BLOB_ACTION_MOVE,
+ filePath: '/sit.md',
+ previousPath: '/sit/amit.md',
+ content: CONTENT_2,
+ },
+ },
+};
+
+export const createBlobFromTestEntry = ({ diff, origContent }, isOrig = false) => ({
+ content: isOrig && origContent ? origContent : diff.content,
+ path: isOrig ? diff.previousPath : diff.filePath,
+});
+
+export const createBlobsFromTestEntries = (entries, isOrig = false) =>
+ entries.reduce(
+ (acc, entry) =>
+ Object.assign(acc, {
+ [entry.id]: {
+ id: entry.id,
+ ...createBlobFromTestEntry(entry, isOrig),
+ },
+ }),
+ {},
+ );
diff --git a/spec/frontend/snippets/utils/blob_spec.js b/spec/frontend/snippets/utils/blob_spec.js
new file mode 100644
index 00000000000..c20cf2e6102
--- /dev/null
+++ b/spec/frontend/snippets/utils/blob_spec.js
@@ -0,0 +1,63 @@
+import { cloneDeep } from 'lodash';
+import { decorateBlob, createBlob, diffAll } from '~/snippets/utils/blob';
+import { testEntries, createBlobsFromTestEntries } from '../test_utils';
+
+jest.mock('lodash/uniqueId', () => arg => `${arg}fakeUniqueId`);
+
+const TEST_RAW_BLOB = {
+ rawPath: '/test/blob/7/raw',
+};
+
+describe('~/snippets/utils/blob', () => {
+ describe('decorateBlob', () => {
+ it('should decorate the given object with local blob properties', () => {
+ const orig = cloneDeep(TEST_RAW_BLOB);
+
+ expect(decorateBlob(orig)).toEqual({
+ ...TEST_RAW_BLOB,
+ id: 'blob_local_fakeUniqueId',
+ isLoaded: false,
+ content: '',
+ });
+ });
+ });
+
+ describe('createBlob', () => {
+ it('should create an empty local blob', () => {
+ expect(createBlob()).toEqual({
+ id: 'blob_local_fakeUniqueId',
+ isLoaded: true,
+ content: '',
+ path: '',
+ });
+ });
+ });
+
+ describe('diffAll', () => {
+ it('should create diff from original files', () => {
+ const origBlobs = createBlobsFromTestEntries(
+ [
+ testEntries.deleted,
+ testEntries.updated,
+ testEntries.renamed,
+ testEntries.renamedAndUpdated,
+ ],
+ true,
+ );
+ const blobs = createBlobsFromTestEntries([
+ testEntries.created,
+ testEntries.updated,
+ testEntries.renamed,
+ testEntries.renamedAndUpdated,
+ ]);
+
+ expect(diffAll(blobs, origBlobs)).toEqual([
+ testEntries.deleted.diff,
+ testEntries.created.diff,
+ testEntries.updated.diff,
+ testEntries.renamed.diff,
+ testEntries.renamedAndUpdated.diff,
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/snippets_spec.js b/spec/frontend/snippets_spec.js
index 5b391606371..6c39ff0da27 100644
--- a/spec/frontend/snippets_spec.js
+++ b/spec/frontend/snippets_spec.js
@@ -7,7 +7,7 @@ describe('Snippets', () => {
let shareBtn;
let scriptTag;
- const snippetUrl = 'http://test.host/snippets/1';
+ const snippetUrl = 'http://test.host/-/snippets/1';
beforeEach(() => {
loadHTMLFixture('snippets/show.html');
diff --git a/spec/frontend/static_site_editor/components/app_spec.js b/spec/frontend/static_site_editor/components/app_spec.js
new file mode 100644
index 00000000000..bbdffeae68f
--- /dev/null
+++ b/spec/frontend/static_site_editor/components/app_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+import App from '~/static_site_editor/components/app.vue';
+
+describe('static_site_editor/components/app', () => {
+ const mergeRequestsIllustrationPath = 'illustrations/merge_requests.svg';
+ const RouterView = {
+ template: '<div></div>',
+ };
+ let wrapper;
+
+ const buildWrapper = () => {
+ wrapper = shallowMount(App, {
+ stubs: {
+ RouterView,
+ },
+ propsData: {
+ mergeRequestsIllustrationPath,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('passes merge request illustration path to the router view component', () => {
+ buildWrapper();
+
+ expect(wrapper.find(RouterView).attributes()).toMatchObject({
+ 'merge-requests-illustration-path': mergeRequestsIllustrationPath,
+ });
+ });
+});
diff --git a/spec/frontend/static_site_editor/components/edit_area_spec.js b/spec/frontend/static_site_editor/components/edit_area_spec.js
index 11c5abf1b08..f4be911171e 100644
--- a/spec/frontend/static_site_editor/components/edit_area_spec.js
+++ b/spec/frontend/static_site_editor/components/edit_area_spec.js
@@ -15,8 +15,11 @@ import {
returnUrl,
} from '../mock_data';
+jest.mock('~/static_site_editor/services/formatter', () => jest.fn(str => `${str} format-pass`));
+
describe('~/static_site_editor/components/edit_area.vue', () => {
let wrapper;
+ const formattedBody = `${body} format-pass`;
const savingChanges = true;
const newBody = `new ${body}`;
@@ -50,9 +53,9 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
expect(findEditHeader().props('title')).toBe(title);
});
- it('renders rich content editor', () => {
+ it('renders rich content editor with a format pass', () => {
expect(findRichContentEditor().exists()).toBe(true);
- expect(findRichContentEditor().props('content')).toBe(body);
+ expect(findRichContentEditor().props('content')).toBe(formattedBody);
});
it('renders publish toolbar', () => {
@@ -94,7 +97,7 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
});
it('sets publish toolbar as not saveable when content changes are rollback', () => {
- findRichContentEditor().vm.$emit('input', body);
+ findRichContentEditor().vm.$emit('input', formattedBody);
return wrapper.vm.$nextTick().then(() => {
expect(findPublishToolbar().props('saveable')).toBe(false);
@@ -103,31 +106,53 @@ describe('~/static_site_editor/components/edit_area.vue', () => {
});
describe('when the mode changes', () => {
+ let resetInitialValue;
+
const setInitialMode = mode => {
wrapper.setData({ editorMode: mode });
};
+ const buildResetInitialValue = () => {
+ resetInitialValue = jest.fn();
+ findRichContentEditor().setMethods({ resetInitialValue });
+ };
+
afterEach(() => {
setInitialMode(EDITOR_TYPES.wysiwyg);
+ resetInitialValue = null;
});
it.each`
initialMode | targetMode | resetValue
- ${EDITOR_TYPES.wysiwyg} | ${EDITOR_TYPES.markdown} | ${content}
- ${EDITOR_TYPES.markdown} | ${EDITOR_TYPES.wysiwyg} | ${body}
+ ${EDITOR_TYPES.wysiwyg} | ${EDITOR_TYPES.markdown} | ${`${content} format-pass format-pass`}
+ ${EDITOR_TYPES.markdown} | ${EDITOR_TYPES.wysiwyg} | ${`${body} format-pass format-pass`}
`(
'sets editorMode from $initialMode to $targetMode',
({ initialMode, targetMode, resetValue }) => {
setInitialMode(initialMode);
+ buildResetInitialValue();
- const resetInitialValue = jest.fn();
-
- findRichContentEditor().setMethods({ resetInitialValue });
findRichContentEditor().vm.$emit('modeChange', targetMode);
expect(resetInitialValue).toHaveBeenCalledWith(resetValue);
expect(wrapper.vm.editorMode).toBe(targetMode);
},
);
+
+ it('should format the content', () => {
+ buildResetInitialValue();
+
+ findRichContentEditor().vm.$emit('modeChange', EDITOR_TYPES.markdown);
+
+ expect(resetInitialValue).toHaveBeenCalledWith(`${content} format-pass format-pass`);
+ });
+ });
+
+ describe('when content is submitted', () => {
+ it('should format the content', () => {
+ findPublishToolbar().vm.$emit('submit', content);
+
+ expect(wrapper.emitted('submit')[0][0].content).toBe(`${content} format-pass format-pass`);
+ });
});
});
diff --git a/spec/frontend/static_site_editor/components/saved_changes_message_spec.js b/spec/frontend/static_site_editor/components/saved_changes_message_spec.js
deleted file mode 100644
index a63c3a83395..00000000000
--- a/spec/frontend/static_site_editor/components/saved_changes_message_spec.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-
-import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue';
-
-import { returnUrl, savedContentMeta } from '../mock_data';
-
-describe('~/static_site_editor/components/saved_changes_message.vue', () => {
- let wrapper;
- const { branch, commit, mergeRequest } = savedContentMeta;
- const props = {
- branch,
- commit,
- mergeRequest,
- returnUrl,
- };
- const findReturnToSiteButton = () => wrapper.find({ ref: 'returnToSiteButton' });
- const findMergeRequestButton = () => wrapper.find({ ref: 'mergeRequestButton' });
- const findBranchLink = () => wrapper.find({ ref: 'branchLink' });
- const findCommitLink = () => wrapper.find({ ref: 'commitLink' });
- const findMergeRequestLink = () => wrapper.find({ ref: 'mergeRequestLink' });
-
- beforeEach(() => {
- wrapper = shallowMount(SavedChangesMessage, {
- propsData: props,
- });
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it.each`
- text | findEl | url
- ${'Return to site'} | ${findReturnToSiteButton} | ${props.returnUrl}
- ${'View merge request'} | ${findMergeRequestButton} | ${props.mergeRequest.url}
- `('renders "$text" button link', ({ text, findEl, url }) => {
- const btn = findEl();
-
- expect(btn.exists()).toBe(true);
- expect(btn.text()).toBe(text);
- expect(btn.attributes('href')).toBe(url);
- });
-
- it.each`
- desc | findEl | prop
- ${'branch'} | ${findBranchLink} | ${props.branch}
- ${'commit'} | ${findCommitLink} | ${props.commit}
- ${'merge request'} | ${findMergeRequestLink} | ${props.mergeRequest}
- `('renders $desc link', ({ findEl, prop }) => {
- const el = findEl();
-
- expect(el.exists()).toBe(true);
- expect(el.text()).toBe(prop.label);
- expect(el.attributes('href')).toBe(prop.url);
- });
-});
diff --git a/spec/frontend/static_site_editor/pages/home_spec.js b/spec/frontend/static_site_editor/pages/home_spec.js
index d3ee70785d1..c5473596df8 100644
--- a/spec/frontend/static_site_editor/pages/home_spec.js
+++ b/spec/frontend/static_site_editor/pages/home_spec.js
@@ -1,5 +1,6 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Home from '~/static_site_editor/pages/home.vue';
import SkeletonLoader from '~/static_site_editor/components/skeleton_loader.vue';
import EditArea from '~/static_site_editor/components/edit_area.vue';
@@ -7,7 +8,6 @@ import InvalidContentMessage from '~/static_site_editor/components/invalid_conte
import SubmitChangesError from '~/static_site_editor/components/submit_changes_error.vue';
import submitContentChangesMutation from '~/static_site_editor/graphql/mutations/submit_content_changes.mutation.graphql';
import { SUCCESS_ROUTE } from '~/static_site_editor/router/constants';
-import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { TRACKING_ACTION_INITIALIZE_EDITOR } from '~/static_site_editor/constants';
import {
diff --git a/spec/frontend/static_site_editor/pages/success_spec.js b/spec/frontend/static_site_editor/pages/success_spec.js
index d62b67bfa83..3e19e2413e7 100644
--- a/spec/frontend/static_site_editor/pages/success_spec.js
+++ b/spec/frontend/static_site_editor/pages/success_spec.js
@@ -1,17 +1,12 @@
-import Vuex from 'vuex';
-import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import { GlEmptyState, GlButton } from '@gitlab/ui';
import Success from '~/static_site_editor/pages/success.vue';
-import SavedChangesMessage from '~/static_site_editor/components/saved_changes_message.vue';
-import { savedContentMeta, returnUrl } from '../mock_data';
+import { savedContentMeta, returnUrl, sourcePath } from '../mock_data';
import { HOME_ROUTE } from '~/static_site_editor/router/constants';
-const localVue = createLocalVue();
-
-localVue.use(Vuex);
-
describe('static_site_editor/pages/success', () => {
+ const mergeRequestsIllustrationPath = 'illustrations/merge_requests.svg';
let wrapper;
- let store;
let router;
const buildRouter = () => {
@@ -22,16 +17,22 @@ describe('static_site_editor/pages/success', () => {
const buildWrapper = (data = {}) => {
wrapper = shallowMount(Success, {
- localVue,
- store,
mocks: {
$router: router,
},
+ stubs: {
+ GlEmptyState,
+ GlButton,
+ },
+ propsData: {
+ mergeRequestsIllustrationPath,
+ },
data() {
return {
savedContentMeta,
appData: {
returnUrl,
+ sourcePath,
},
...data,
};
@@ -39,7 +40,8 @@ describe('static_site_editor/pages/success', () => {
});
};
- const findSavedChangesMessage = () => wrapper.find(SavedChangesMessage);
+ const findEmptyState = () => wrapper.find(GlEmptyState);
+ const findReturnUrlButton = () => wrapper.find(GlButton);
beforeEach(() => {
buildRouter();
@@ -50,29 +52,50 @@ describe('static_site_editor/pages/success', () => {
wrapper = null;
});
- it('renders saved changes message', () => {
+ it('renders empty state with a link to the created merge request', () => {
+ buildWrapper();
+
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findEmptyState().props()).toMatchObject({
+ primaryButtonText: 'View merge request',
+ primaryButtonLink: savedContentMeta.mergeRequest.url,
+ title: 'Your merge request has been created',
+ svgPath: mergeRequestsIllustrationPath,
+ });
+ });
+
+ it('displays merge request instructions in the empty state', () => {
buildWrapper();
- expect(findSavedChangesMessage().exists()).toBe(true);
+ expect(findEmptyState().text()).toContain(
+ 'To see your changes live you will need to do the following things:',
+ );
+ expect(findEmptyState().text()).toContain('1. Add a clear title to describe the change.');
+ expect(findEmptyState().text()).toContain(
+ '2. Add a description to explain why the change is being made.',
+ );
+ expect(findEmptyState().text()).toContain(
+ '3. Assign a person to review and accept the merge request.',
+ );
});
- it('passes returnUrl to the saved changes message', () => {
+ it('displays return to site button', () => {
buildWrapper();
- expect(findSavedChangesMessage().props('returnUrl')).toBe(returnUrl);
+ expect(findReturnUrlButton().text()).toBe('Return to site');
+ expect(findReturnUrlButton().attributes().href).toBe(returnUrl);
});
- it('passes saved content metadata to the saved changes message', () => {
+ it('displays source path', () => {
buildWrapper();
- expect(findSavedChangesMessage().props('branch')).toBe(savedContentMeta.branch);
- expect(findSavedChangesMessage().props('commit')).toBe(savedContentMeta.commit);
- expect(findSavedChangesMessage().props('mergeRequest')).toBe(savedContentMeta.mergeRequest);
+ expect(wrapper.text()).toContain(`Update ${sourcePath} file`);
});
it('redirects to the HOME route when content has not been submitted', () => {
buildWrapper({ savedContentMeta: null });
expect(router.push).toHaveBeenCalledWith(HOME_ROUTE);
+ expect(wrapper.html()).toBe('');
});
});
diff --git a/spec/frontend/static_site_editor/services/formatter_spec.js b/spec/frontend/static_site_editor/services/formatter_spec.js
new file mode 100644
index 00000000000..b7600798db9
--- /dev/null
+++ b/spec/frontend/static_site_editor/services/formatter_spec.js
@@ -0,0 +1,26 @@
+import formatter from '~/static_site_editor/services/formatter';
+
+describe('formatter', () => {
+ const source = `Some text
+<br>
+
+And some more text
+
+
+<br>
+
+
+And even more text`;
+ const sourceWithoutBrTags = `Some text
+
+And some more text
+
+
+
+
+And even more text`;
+
+ it('removes extraneous <br> tags', () => {
+ expect(formatter(source)).toMatch(sourceWithoutBrTags);
+ });
+});
diff --git a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
index a9169eb3e16..645ccedf7e7 100644
--- a/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
+++ b/spec/frontend/static_site_editor/services/submit_content_changes_spec.js
@@ -1,6 +1,6 @@
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import Api from '~/api';
import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
-import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import {
DEFAULT_TARGET_BRANCH,
diff --git a/spec/frontend/static_site_editor/services/templater_spec.js b/spec/frontend/static_site_editor/services/templater_spec.js
new file mode 100644
index 00000000000..1e7ae872b7e
--- /dev/null
+++ b/spec/frontend/static_site_editor/services/templater_spec.js
@@ -0,0 +1,104 @@
+/* eslint-disable no-useless-escape */
+import templater from '~/static_site_editor/services/templater';
+
+describe('templater', () => {
+ const source = `Below this line is a simple ERB (single-line erb block) example.
+
+<% some erb code %>
+
+Below this line is a complex ERB (multi-line erb block) example.
+
+<% if apptype.maturity && (apptype.maturity != "planned") %>
+ <% maturity = "This application type is at the \"#{apptype.maturity}\" level of maturity." %>
+<% end %>
+
+Below this line is a non-erb (single-line HTML) markup example that also has erb.
+
+<a href="<%= compensation_roadmap.role_path %>"><%= compensation_roadmap.role_path %></a>
+
+Below this line is a non-erb (multi-line HTML block) markup example that also has erb.
+
+<ul>
+<% compensation_roadmap.recommendation.recommendations.each do |recommendation| %>
+ <li><%= recommendation %></li>
+<% end %>
+</ul>
+
+Below this line is a block of HTML.
+
+<div>
+ <h1>Heading</h1>
+ <p>Some paragraph...</p>
+</div>
+
+Below this line is a codeblock of the same HTML that should be ignored and preserved.
+
+\`\`\` html
+<div>
+ <h1>Heading</h1>
+ <p>Some paragraph...</p>
+</div>
+\`\`\`
+`;
+ const sourceTemplated = `Below this line is a simple ERB (single-line erb block) example.
+
+\`\`\` sse
+<% some erb code %>
+\`\`\`
+
+Below this line is a complex ERB (multi-line erb block) example.
+
+\`\`\` sse
+<% if apptype.maturity && (apptype.maturity != "planned") %>
+ <% maturity = "This application type is at the \"#{apptype.maturity}\" level of maturity." %>
+<% end %>
+\`\`\`
+
+Below this line is a non-erb (single-line HTML) markup example that also has erb.
+
+\`\`\` sse
+<a href="<%= compensation_roadmap.role_path %>"><%= compensation_roadmap.role_path %></a>
+\`\`\`
+
+Below this line is a non-erb (multi-line HTML block) markup example that also has erb.
+
+\`\`\` sse
+<ul>
+<% compensation_roadmap.recommendation.recommendations.each do |recommendation| %>
+ <li><%= recommendation %></li>
+<% end %>
+</ul>
+\`\`\`
+
+Below this line is a block of HTML.
+
+\`\`\` sse
+<div>
+ <h1>Heading</h1>
+ <p>Some paragraph...</p>
+</div>
+\`\`\`
+
+Below this line is a codeblock of the same HTML that should be ignored and preserved.
+
+\`\`\` html
+<div>
+ <h1>Heading</h1>
+ <p>Some paragraph...</p>
+</div>
+\`\`\`
+`;
+
+ it.each`
+ fn | initial | target
+ ${'wrap'} | ${source} | ${sourceTemplated}
+ ${'wrap'} | ${sourceTemplated} | ${sourceTemplated}
+ ${'unwrap'} | ${sourceTemplated} | ${source}
+ ${'unwrap'} | ${source} | ${source}
+ `(
+ 'wraps $initial in a templated sse codeblocks if $fn is wrap, unwraps otherwise',
+ ({ fn, initial, target }) => {
+ expect(templater[fn](initial)).toMatch(target);
+ },
+ );
+});
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index 49eae715a45..544c19da57b 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -24,7 +24,7 @@ afterEach(() =>
}),
);
-initializeTestTimeout(process.env.CI ? 5000 : 500);
+initializeTestTimeout(process.env.CI ? 6000 : 500);
Vue.config.devtools = false;
Vue.config.productionTip = false;
diff --git a/spec/frontend/vue_alerts_spec.js b/spec/frontend/vue_alerts_spec.js
index b2ee6f895a8..b52737e6106 100644
--- a/spec/frontend/vue_alerts_spec.js
+++ b/spec/frontend/vue_alerts_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
-import initVueAlerts from '~/vue_alerts';
import { setHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
+import initVueAlerts from '~/vue_alerts';
describe('VueAlerts', () => {
const alerts = [
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
index e39f66d3f30..65ca3639dcc 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_mr_widget/components/approvals/approvals_spec.js
@@ -3,7 +3,7 @@ import { GlButton } from '@gitlab/ui';
import Approvals from '~/vue_merge_request_widget/components/approvals/approvals.vue';
import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
FETCH_LOADING,
FETCH_ERROR,
diff --git a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js b/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js
index 77fad7f51ab..d9a5230f55f 100644
--- a/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js
+++ b/spec/frontend/vue_mr_widget/components/approvals/approvals_summary_optional_spec.js
@@ -1,9 +1,5 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
-import {
- OPTIONAL,
- OPTIONAL_CAN_APPROVE,
-} from '~/vue_merge_request_widget/components/approvals/messages';
import ApprovalsSummaryOptional from '~/vue_merge_request_widget/components/approvals/approvals_summary_optional.vue';
const TEST_HELP_PATH = 'help/path';
@@ -29,10 +25,6 @@ describe('MRWidget approvals summary optional', () => {
createComponent({ canApprove: true, helpPath: TEST_HELP_PATH });
});
- it('shows optional can approve message', () => {
- expect(wrapper.text()).toEqual(OPTIONAL_CAN_APPROVE);
- });
-
it('shows help link', () => {
const link = findHelpLink();
@@ -46,10 +38,6 @@ describe('MRWidget approvals summary optional', () => {
createComponent({ canApprove: false, helpPath: TEST_HELP_PATH });
});
- it('shows optional message', () => {
- expect(wrapper.text()).toEqual(OPTIONAL);
- });
-
it('does not show help link', () => {
expect(findHelpLink().exists()).toBe(false);
});
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
index 5f3a8654990..d67f1adadf2 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_pipeline_container_spec.js
@@ -1,9 +1,9 @@
import { mount } from '@vue/test-utils';
+import MockAdapter from 'axios-mock-adapter';
import MrWidgetPipelineContainer from '~/vue_merge_request_widget/components/mr_widget_pipeline_container.vue';
import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import ArtifactsApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
import { mockStore } from '../mock_data';
-import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
describe('MrWidgetPipelineContainer', () => {
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
index d6c996f7501..8fcc982ac99 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js
@@ -1,119 +1,156 @@
-import { mount } from '@vue/test-utils';
+import { mount, shallowMount } from '@vue/test-utils';
import { GlLink, GlSprintf } from '@gitlab/ui';
+import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
+import MockAdapter from 'axios-mock-adapter';
import suggestPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue';
import MrWidgetIcon from '~/vue_merge_request_widget/components/mr_widget_icon.vue';
-import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
-import { popoverProps, iconName } from './pipeline_tour_mock_data';
+import dismissibleContainer from '~/vue_shared/components/dismissible_container.vue';
+import { suggestProps, iconName } from './pipeline_tour_mock_data';
+import axios from '~/lib/utils/axios_utils';
+import {
+ SP_TRACK_LABEL,
+ SP_LINK_TRACK_EVENT,
+ SP_SHOW_TRACK_EVENT,
+ SP_LINK_TRACK_VALUE,
+ SP_SHOW_TRACK_VALUE,
+ SP_HELP_URL,
+} from '~/vue_merge_request_widget/constants';
describe('MRWidgetSuggestPipeline', () => {
- let wrapper;
- let trackingSpy;
-
- const mockTrackingOnWrapper = () => {
- unmockTracking();
- trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
- };
-
- beforeEach(() => {
- document.body.dataset.page = 'projects:merge_requests:show';
- trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
-
- wrapper = mount(suggestPipelineComponent, {
- propsData: popoverProps,
- stubs: {
- GlSprintf,
- },
+ describe('template', () => {
+ let wrapper;
+
+ afterEach(() => {
+ wrapper.destroy();
});
- });
- afterEach(() => {
- wrapper.destroy();
- unmockTracking();
- });
+ describe('core functionality', () => {
+ const findOkBtn = () => wrapper.find('[data-testid="ok"]');
+ let trackingSpy;
+ let mockAxios;
+
+ const mockTrackingOnWrapper = () => {
+ unmockTracking();
+ trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
+ };
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ document.body.dataset.page = 'projects:merge_requests:show';
+ trackingSpy = mockTracking('_category_', undefined, jest.spyOn);
+
+ wrapper = mount(suggestPipelineComponent, {
+ propsData: suggestProps,
+ stubs: {
+ GlSprintf,
+ },
+ });
+ });
- describe('template', () => {
- const findOkBtn = () => wrapper.find('[data-testid="ok"]');
+ afterEach(() => {
+ unmockTracking();
+ mockAxios.restore();
+ });
- it('renders add pipeline file link', () => {
- const link = wrapper.find(GlLink);
+ it('renders add pipeline file link', () => {
+ const link = wrapper.find(GlLink);
- expect(link.exists()).toBe(true);
- expect(link.attributes().href).toBe(popoverProps.pipelinePath);
- });
+ expect(link.exists()).toBe(true);
+ expect(link.attributes().href).toBe(suggestProps.pipelinePath);
+ });
- it('renders the expected text', () => {
- const messageText = /\s*No pipeline\s*Add the .gitlab-ci.yml file\s*to create one./;
+ it('renders the expected text', () => {
+ const messageText = /\s*No pipeline\s*Add the .gitlab-ci.yml file\s*to create one./;
- expect(wrapper.text()).toMatch(messageText);
- });
+ expect(wrapper.text()).toMatch(messageText);
+ });
- it('renders widget icon', () => {
- const icon = wrapper.find(MrWidgetIcon);
+ it('renders widget icon', () => {
+ const icon = wrapper.find(MrWidgetIcon);
- expect(icon.exists()).toBe(true);
- expect(icon.props()).toEqual(
- expect.objectContaining({
- name: iconName,
- }),
- );
- });
+ expect(icon.exists()).toBe(true);
+ expect(icon.props()).toEqual(
+ expect.objectContaining({
+ name: iconName,
+ }),
+ );
+ });
- it('renders the show me how button', () => {
- const button = findOkBtn();
+ it('renders the show me how button', () => {
+ const button = findOkBtn();
- expect(button.exists()).toBe(true);
- expect(button.classes('btn-info')).toEqual(true);
- expect(button.attributes('href')).toBe(popoverProps.pipelinePath);
- });
+ expect(button.exists()).toBe(true);
+ expect(button.classes('btn-info')).toEqual(true);
+ expect(button.attributes('href')).toBe(suggestProps.pipelinePath);
+ });
- it('renders the help link', () => {
- const link = wrapper.find('[data-testid="help"]');
+ it('renders the help link', () => {
+ const link = wrapper.find('[data-testid="help"]');
- expect(link.exists()).toBe(true);
- expect(link.attributes('href')).toBe(wrapper.vm.$options.helpURL);
- });
+ expect(link.exists()).toBe(true);
+ expect(link.attributes('href')).toBe(SP_HELP_URL);
+ });
- it('renders the empty pipelines image', () => {
- const image = wrapper.find('[data-testid="pipeline-image"]');
+ it('renders the empty pipelines image', () => {
+ const image = wrapper.find('[data-testid="pipeline-image"]');
- expect(image.exists()).toBe(true);
- expect(image.attributes().src).toBe(popoverProps.pipelineSvgPath);
- });
+ expect(image.exists()).toBe(true);
+ expect(image.attributes().src).toBe(suggestProps.pipelineSvgPath);
+ });
- describe('tracking', () => {
- it('send event for basic view of the suggest pipeline widget', () => {
- const expectedCategory = undefined;
- const expectedAction = undefined;
+ describe('tracking', () => {
+ it('send event for basic view of the suggest pipeline widget', () => {
+ const expectedCategory = undefined;
+ const expectedAction = undefined;
- expect(trackingSpy).toHaveBeenCalledWith(expectedCategory, expectedAction, {
- label: wrapper.vm.$options.trackLabel,
- property: popoverProps.humanAccess,
+ expect(trackingSpy).toHaveBeenCalledWith(expectedCategory, expectedAction, {
+ label: SP_TRACK_LABEL,
+ property: suggestProps.humanAccess,
+ });
});
- });
- it('send an event when add pipeline link is clicked', () => {
- mockTrackingOnWrapper();
- const link = wrapper.find('[data-testid="add-pipeline-link"]');
- triggerEvent(link.element);
+ it('send an event when add pipeline link is clicked', () => {
+ mockTrackingOnWrapper();
+ const link = wrapper.find('[data-testid="add-pipeline-link"]');
+ triggerEvent(link.element);
- expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_link', {
- label: wrapper.vm.$options.trackLabel,
- property: popoverProps.humanAccess,
- value: '30',
+ expect(trackingSpy).toHaveBeenCalledWith('_category_', SP_LINK_TRACK_EVENT, {
+ label: SP_TRACK_LABEL,
+ property: suggestProps.humanAccess,
+ value: SP_LINK_TRACK_VALUE.toString(),
+ });
});
- });
- it('send an event when ok button is clicked', () => {
- mockTrackingOnWrapper();
- const okBtn = findOkBtn();
- triggerEvent(okBtn.element);
+ it('send an event when ok button is clicked', () => {
+ mockTrackingOnWrapper();
+ const okBtn = findOkBtn();
+ triggerEvent(okBtn.element);
- expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_button', {
- label: wrapper.vm.$options.trackLabel,
- property: popoverProps.humanAccess,
- value: '10',
+ expect(trackingSpy).toHaveBeenCalledWith('_category_', SP_SHOW_TRACK_EVENT, {
+ label: SP_TRACK_LABEL,
+ property: suggestProps.humanAccess,
+ value: SP_SHOW_TRACK_VALUE.toString(),
+ });
});
});
});
+
+ describe('dismissible', () => {
+ const findDismissContainer = () => wrapper.find(dismissibleContainer);
+
+ beforeEach(() => {
+ wrapper = shallowMount(suggestPipelineComponent, { propsData: suggestProps });
+ });
+
+ it('renders the dismissal container', () => {
+ expect(findDismissContainer().exists()).toBe(true);
+ });
+
+ it('emits dismiss upon dismissal button click', () => {
+ findDismissContainer().vm.$emit('dismiss');
+
+ expect(wrapper.emitted().dismiss).toBeTruthy();
+ });
+ });
});
});
diff --git a/spec/frontend/vue_mr_widget/components/pipeline_tour_mock_data.js b/spec/frontend/vue_mr_widget/components/pipeline_tour_mock_data.js
index c749c434079..eef087d62b8 100644
--- a/spec/frontend/vue_mr_widget/components/pipeline_tour_mock_data.js
+++ b/spec/frontend/vue_mr_widget/components/pipeline_tour_mock_data.js
@@ -1,7 +1,9 @@
-export const popoverProps = {
+export const suggestProps = {
pipelinePath: '/foo/bar/add/pipeline/path',
pipelineSvgPath: 'assets/illustrations/something.svg',
humanAccess: 'maintainer',
+ userCalloutsPath: 'some/callout/path',
+ userCalloutFeatureId: 'suggest_pipeline',
};
export const iconName = 'status_notfound';
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
index 56832f82b05..5c7e6a87c16 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_commit_message_dropdown_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdownItem } from '@gitlab/ui';
import CommitMessageDropdown from '~/vue_merge_request_widget/components/states/commit_message_dropdown.vue';
const commits = [
@@ -39,7 +39,7 @@ describe('Commits message dropdown component', () => {
wrapper.destroy();
});
- const findDropdownElements = () => wrapper.findAll(GlDropdownItem);
+ const findDropdownElements = () => wrapper.findAll(GlDeprecatedDropdownItem);
const findFirstDropdownElement = () => findDropdownElements().at(0);
it('should have 3 elements in dropdown list', () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index d3482b457ad..c3a16a776a7 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import { createLocalVue, shallowMount } from '@vue/test-utils';
import { removeBreakLine } from 'helpers/text_helper';
-import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import { TEST_HOST } from 'helpers/test_constants';
+import ConflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
describe('MRWidgetConflicts', () => {
let vm;
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
index 1542b0939aa..4c213899dbd 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_squash_before_merge_spec.js
@@ -79,7 +79,7 @@ describe('Squash before merge component', () => {
});
it(expectation, () => {
- expect(findLabel().classes('gl-text-gray-600')).toBe(isDisabled);
+ expect(findLabel().classes('gl-text-gray-400')).toBe(isDisabled);
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
index 33e52f4fd36..a5531577a8c 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_unresolved_discussions_spec.js
@@ -1,46 +1,68 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import UnresolvedDiscussions from '~/vue_merge_request_widget/components/states/unresolved_discussions.vue';
+import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
+import UnresolvedDiscussions from '~/vue_merge_request_widget/components/states/unresolved_discussions.vue';
+import notesEventHub from '~/notes/event_hub';
+
+function createComponent({ path = '' } = {}) {
+ return mount(UnresolvedDiscussions, {
+ propsData: {
+ mr: {
+ createIssueToResolveDiscussionsPath: path,
+ },
+ },
+ });
+}
describe('UnresolvedDiscussions', () => {
- const Component = Vue.extend(UnresolvedDiscussions);
- let vm;
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = createComponent();
+ });
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ });
+
+ it('triggers the correct notes event when the jump to first unresolved discussion button is clicked', () => {
+ jest.spyOn(notesEventHub, '$emit');
+
+ wrapper.find('[data-testid="jump-to-first"]').trigger('click');
+
+ expect(notesEventHub.$emit).toHaveBeenCalledWith('jumpToFirstUnresolvedDiscussion');
});
describe('with threads path', () => {
beforeEach(() => {
- vm = mountComponent(Component, {
- mr: {
- createIssueToResolveDiscussionsPath: TEST_HOST,
- },
- });
+ wrapper = createComponent({ path: TEST_HOST });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
});
it('should have correct elements', () => {
- expect(vm.$el.innerText).toContain(
- 'There are unresolved threads. Please resolve these threads',
+ expect(wrapper.element.innerText).toContain(
+ `Before this can be merged, one or more threads must be resolved.`,
);
- expect(vm.$el.innerText).toContain('Create an issue to resolve them later');
- expect(vm.$el.querySelector('.js-create-issue').getAttribute('href')).toEqual(TEST_HOST);
+ expect(wrapper.element.innerText).toContain('Jump to first unresolved thread');
+ expect(wrapper.element.innerText).toContain('Resolve all threads in new issue');
+ expect(wrapper.element.querySelector('.js-create-issue').getAttribute('href')).toEqual(
+ TEST_HOST,
+ );
});
});
describe('without threads path', () => {
- beforeEach(() => {
- vm = mountComponent(Component, { mr: {} });
- });
-
it('should not show create issue link if user cannot create issue', () => {
- expect(vm.$el.innerText).toContain(
- 'There are unresolved threads. Please resolve these threads',
+ expect(wrapper.element.innerText).toContain(
+ `Before this can be merged, one or more threads must be resolved.`,
);
- expect(vm.$el.querySelector('.js-create-issue')).toEqual(null);
+ expect(wrapper.element.innerText).toContain('Jump to first unresolved thread');
+ expect(wrapper.element.innerText).not.toContain('Resolve all threads in new issue');
+ expect(wrapper.element.querySelector('.js-create-issue')).toEqual(null);
});
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
index 6fa555b4fc4..6ccf1e1f56b 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_wip_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
@@ -84,11 +84,11 @@ describe('Wip', () => {
it('should have correct elements', () => {
expect(el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(el.innerText).toContain('This is a Work in Progress');
+ expect(el.innerText).toContain('This merge request is still a work in progress.');
expect(el.querySelector('button').getAttribute('disabled')).toBeTruthy();
expect(el.querySelector('button').innerText).toContain('Merge');
expect(el.querySelector('.js-remove-wip').innerText.replace(/\s\s+/g, ' ')).toContain(
- 'Resolve WIP status',
+ 'Mark as ready',
);
});
diff --git a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
index be43f10c03e..ffcf9b1477a 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js
@@ -1,8 +1,8 @@
import { GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
-import { invalidPlanWithName, plans, validPlanWithName } from './mock_data';
import { shallowMount } from '@vue/test-utils';
-import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
+import { invalidPlanWithName, plans, validPlanWithName } from './mock_data';
+import axios from '~/lib/utils/axios_utils';
import MrWidgetExpanableSection from '~/vue_merge_request_widget/components/mr_widget_expandable_section.vue';
import MrWidgetTerraformContainer from '~/vue_merge_request_widget/components/terraform/mr_widget_terraform_container.vue';
import Poll from '~/lib/utils/poll';
diff --git a/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js b/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js
index 6449272e6ed..1711efb5512 100644
--- a/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js
+++ b/spec/frontend/vue_mr_widget/deployment/deployment_actions_spec.js
@@ -1,5 +1,5 @@
import { mount } from '@vue/test-utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import { visitUrl } from '~/lib/utils/url_utility';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
import DeploymentActions from '~/vue_merge_request_widget/components/deployment/deployment_actions.vue';
diff --git a/spec/frontend/vue_mr_widget/mock_data.js b/spec/frontend/vue_mr_widget/mock_data.js
index e00456a78b5..d64a7f88b6b 100644
--- a/spec/frontend/vue_mr_widget/mock_data.js
+++ b/spec/frontend/vue_mr_widget/mock_data.js
@@ -37,6 +37,9 @@ export default {
target_project_id: 19,
target_project_full_path: '/group2/project2',
merge_request_add_ci_config_path: '/group2/project2/new/pipeline',
+ is_dismissed_suggest_pipeline: false,
+ user_callouts_path: 'some/callout/path',
+ suggest_pipeline_feature_id: 'suggest_pipeline',
new_project_pipeline_path: '/group2/project2/pipelines/new',
metrics: {
merged_by: {
diff --git a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
index 93659fa54fb..0bbe040d031 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -62,6 +62,9 @@ describe('mrWidgetOptions', () => {
return axios.waitForAll();
};
+ const findSuggestPipeline = () => vm.$el.querySelector('[data-testid="mr-suggest-pipeline"]');
+ const findSuggestPipelineButton = () => findSuggestPipeline().querySelector('button');
+
describe('default', () => {
beforeEach(() => {
return createComponent();
@@ -804,42 +807,48 @@ describe('mrWidgetOptions', () => {
});
});
- it('should not suggest pipelines', () => {
- vm.mr.mergeRequestAddCiConfigPath = null;
-
- expect(vm.shouldSuggestPipelines).toBeFalsy();
+ it('should not suggest pipelines when feature flag is not present', () => {
+ expect(findSuggestPipeline()).toBeNull();
});
});
describe('given suggestPipeline feature flag is enabled', () => {
beforeEach(() => {
+ mock.onAny().reply(200);
+
// This is needed because some grandchildren Bootstrap components throw warnings
// https://gitlab.com/gitlab-org/gitlab/issues/208458
jest.spyOn(console, 'warn').mockImplementation();
gon.features = { suggestPipeline: true };
- return createComponent();
- });
- it('should suggest pipelines when none exist', () => {
- vm.mr.mergeRequestAddCiConfigPath = 'some/path';
+ createComponent();
+
vm.mr.hasCI = false;
+ });
- expect(vm.shouldSuggestPipelines).toBeTruthy();
+ it('should suggest pipelines when none exist', () => {
+ expect(findSuggestPipeline()).toEqual(expect.any(Element));
});
- it('should not suggest pipelines when they exist', () => {
- vm.mr.mergeRequestAddCiConfigPath = null;
- vm.mr.hasCI = false;
+ it.each([
+ { isDismissedSuggestPipeline: true },
+ { mergeRequestAddCiConfigPath: null },
+ { hasCI: true },
+ ])('with %s, should not suggest pipeline', async obj => {
+ Object.assign(vm.mr, obj);
+
+ await vm.$nextTick();
- expect(vm.shouldSuggestPipelines).toBeFalsy();
+ expect(findSuggestPipeline()).toBeNull();
});
- it('should not suggest pipelines hasCI is true', () => {
- vm.mr.mergeRequestAddCiConfigPath = 'some/path';
- vm.mr.hasCI = true;
+ it('should allow dismiss of the suggest pipeline message', async () => {
+ findSuggestPipelineButton().click();
+
+ await vm.$nextTick();
- expect(vm.shouldSuggestPipelines).toBeFalsy();
+ expect(findSuggestPipeline()).toBeNull();
});
});
});
diff --git a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
index 1cb2c6c669b..128e0f39c41 100644
--- a/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
+++ b/spec/frontend/vue_mr_widget/stores/get_state_key_spec.js
@@ -11,15 +11,13 @@ describe('getStateKey', () => {
hasMergeableDiscussionsState: false,
isPipelineBlocked: false,
canBeMerged: false,
+ projectArchived: false,
+ branchMissing: false,
+ commitsCount: 2,
+ hasConflicts: false,
+ workInProgress: false,
};
- const data = {
- project_archived: false,
- branch_missing: false,
- commits_count: 2,
- has_conflicts: false,
- work_in_progress: false,
- };
- const bound = getStateKey.bind(context, data);
+ const bound = getStateKey.bind(context);
expect(bound()).toEqual(null);
@@ -49,7 +47,7 @@ describe('getStateKey', () => {
expect(bound()).toEqual('unresolvedDiscussions');
- data.work_in_progress = true;
+ context.workInProgress = true;
expect(bound()).toEqual('workInProgress');
@@ -62,7 +60,7 @@ describe('getStateKey', () => {
expect(bound()).toEqual('rebase');
- data.has_conflicts = true;
+ context.hasConflicts = true;
expect(bound()).toEqual('conflicts');
@@ -70,15 +68,15 @@ describe('getStateKey', () => {
expect(bound()).toEqual('checking');
- data.commits_count = 0;
+ context.commitsCount = 0;
expect(bound()).toEqual('nothingToMerge');
- data.branch_missing = true;
+ context.branchMissing = true;
expect(bound()).toEqual('missingBranch');
- data.project_archived = true;
+ context.projectArchived = true;
expect(bound()).toEqual('archived');
});
@@ -94,15 +92,13 @@ describe('getStateKey', () => {
isPipelineBlocked: false,
canBeMerged: false,
shouldBeRebased: true,
+ projectArchived: false,
+ branchMissing: false,
+ commitsCount: 2,
+ hasConflicts: false,
+ workInProgress: false,
};
- const data = {
- project_archived: false,
- branch_missing: false,
- commits_count: 2,
- has_conflicts: false,
- work_in_progress: false,
- };
- const bound = getStateKey.bind(context, data);
+ const bound = getStateKey.bind(context);
expect(bound()).toEqual('rebase');
});
@@ -115,15 +111,11 @@ describe('getStateKey', () => {
`(
'returns $stateKey when canMerge is $canMerge and isSHAMismatch is $isSHAMismatch',
({ canMerge, isSHAMismatch, stateKey }) => {
- const bound = getStateKey.bind(
- {
- canMerge,
- isSHAMismatch,
- },
- {
- commits_count: 2,
- },
- );
+ const bound = getStateKey.bind({
+ canMerge,
+ isSHAMismatch,
+ commitsCount: 2,
+ });
expect(bound()).toEqual(stateKey);
},
diff --git a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
index 408f9d57147..e84eb7789d3 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/clone_dropdown_spec.js.snap
@@ -4,6 +4,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
<gl-new-dropdown-stub
category="primary"
headertext=""
+ right=""
size="medium"
text="Clone"
variant="info"
@@ -38,7 +39,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
tag="div"
>
<gl-button-stub
- category="tertiary"
+ category="primary"
class="d-inline-flex"
data-clipboard-text="ssh://foo.bar"
data-qa-selector="copy_ssh_url_button"
@@ -79,7 +80,7 @@ exports[`Clone Dropdown Button rendering matches the snapshot 1`] = `
tag="div"
>
<gl-button-stub
- category="tertiary"
+ category="primary"
class="d-inline-flex"
data-clipboard-text="http://foo.bar"
data-qa-selector="copy_http_url_button"
diff --git a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
index 1f54405928b..cd4728baeaa 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/expand_button_spec.js.snap
@@ -4,20 +4,22 @@ exports[`Expand button on click when short text is provided renders button after
<span>
<button
aria-label="Click to expand text"
- class="btn js-text-expander-prepend text-expander btn-blank btn-secondary btn-md"
+ class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button"
style="display: none;"
type="button"
>
<!---->
<svg
- aria-hidden="true"
- class="s12 ic-ellipsis_h"
+ class="gl-icon s16"
+ data-testid="ellipsis_h-icon"
>
<use
- xlink:href="#ellipsis_h"
+ href="#ellipsis_h"
/>
</svg>
+
+ <!---->
</button>
<!---->
@@ -30,20 +32,22 @@ exports[`Expand button on click when short text is provided renders button after
<button
aria-label="Click to expand text"
- class="btn js-text-expander-append text-expander btn-blank btn-secondary btn-md"
+ class="btn js-text-expander-append text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button"
style=""
type="button"
>
<!---->
<svg
- aria-hidden="true"
- class="s12 ic-ellipsis_h"
+ class="gl-icon s16"
+ data-testid="ellipsis_h-icon"
>
<use
- xlink:href="#ellipsis_h"
+ href="#ellipsis_h"
/>
</svg>
+
+ <!---->
</button>
</span>
`;
@@ -52,19 +56,21 @@ exports[`Expand button when short text is provided renders button before text 1`
<span>
<button
aria-label="Click to expand text"
- class="btn js-text-expander-prepend text-expander btn-blank btn-secondary btn-md"
+ class="btn js-text-expander-prepend text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button"
type="button"
>
<!---->
<svg
- aria-hidden="true"
- class="s12 ic-ellipsis_h"
+ class="gl-icon s16"
+ data-testid="ellipsis_h-icon"
>
<use
- xlink:href="#ellipsis_h"
+ href="#ellipsis_h"
/>
</svg>
+
+ <!---->
</button>
<span>
@@ -77,20 +83,22 @@ exports[`Expand button when short text is provided renders button before text 1`
<button
aria-label="Click to expand text"
- class="btn js-text-expander-append text-expander btn-blank btn-secondary btn-md"
+ class="btn js-text-expander-append text-expander btn-blank btn-default btn-md btn-icon button-ellipsis-horizontal gl-button"
style="display: none;"
type="button"
>
<!---->
<svg
- aria-hidden="true"
- class="s12 ic-ellipsis_h"
+ class="gl-icon s16"
+ data-testid="ellipsis_h-icon"
>
<use
- xlink:href="#ellipsis_h"
+ href="#ellipsis_h"
/>
</svg>
+
+ <!---->
</button>
</span>
`;
diff --git a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
index 74f71c23d02..fcb9c4b8b02 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/split_button_spec.js.snap
@@ -1,13 +1,13 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`SplitButton renders actionItems 1`] = `
-<gl-dropdown-stub
+<gl-deprecated-dropdown-stub
menu-class="dropdown-menu-selectable "
split="true"
text="professor"
variant="secondary"
>
- <gl-dropdown-item-stub
+ <gl-deprecated-dropdown-item-stub
active="true"
active-class="is-active"
>
@@ -18,10 +18,10 @@ exports[`SplitButton renders actionItems 1`] = `
<div>
very symphonic
</div>
- </gl-dropdown-item-stub>
+ </gl-deprecated-dropdown-item-stub>
- <gl-dropdown-divider-stub />
- <gl-dropdown-item-stub
+ <gl-deprecated-dropdown-divider-stub />
+ <gl-deprecated-dropdown-item-stub
active-class="is-active"
>
<strong>
@@ -31,8 +31,8 @@ exports[`SplitButton renders actionItems 1`] = `
<div>
warp drive
</div>
- </gl-dropdown-item-stub>
+ </gl-deprecated-dropdown-item-stub>
<!---->
-</gl-dropdown-stub>
+</gl-deprecated-dropdown-stub>
`;
diff --git a/spec/frontend/vue_shared/components/clone_dropdown_spec.js b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
index 38e0cadfe83..d9829874b93 100644
--- a/spec/frontend/vue_shared/components/clone_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/clone_dropdown_spec.js
@@ -1,6 +1,6 @@
-import CloneDropdown from '~/vue_shared/components/clone_dropdown.vue';
import { shallowMount } from '@vue/test-utils';
import { GlFormInputGroup, GlNewDropdownHeader } from '@gitlab/ui';
+import CloneDropdown from '~/vue_shared/components/clone_dropdown.vue';
describe('Clone Dropdown Button', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
index 8d3fcdd48d2..c75891c9ed3 100644
--- a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
-import axios from '~/lib/utils/axios_utils';
import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
import MarkdownViewer from '~/vue_shared/components/content_viewer/viewers/markdown_viewer.vue';
describe('MarkdownViewer', () => {
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
index ceea8d2fa92..223e22d650b 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_spec.js
@@ -13,9 +13,9 @@ describe('DateTimePicker', () => {
const dropdownToggle = () => wrapper.find('.dropdown-toggle');
const dropdownMenu = () => wrapper.find('.dropdown-menu');
+ const cancelButton = () => wrapper.find('[data-testid="cancelButton"]');
const applyButtonElement = () => wrapper.find('button.btn-success').element;
const findQuickRangeItems = () => wrapper.findAll('.dropdown-item');
- const cancelButtonElement = () => wrapper.find('button.btn-secondary').element;
const createComponent = props => {
wrapper = mount(DateTimePicker, {
@@ -260,7 +260,7 @@ describe('DateTimePicker', () => {
dropdownToggle().trigger('click');
return wrapper.vm.$nextTick(() => {
- cancelButtonElement().click();
+ cancelButton().trigger('click');
return wrapper.vm.$nextTick(() => {
expect(dropdownMenu().classes('show')).toBe(false);
diff --git a/spec/frontend/vue_shared/components/dismissible_container_spec.js b/spec/frontend/vue_shared/components/dismissible_container_spec.js
new file mode 100644
index 00000000000..e49ca1e2285
--- /dev/null
+++ b/spec/frontend/vue_shared/components/dismissible_container_spec.js
@@ -0,0 +1,58 @@
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMount } from '@vue/test-utils';
+import axios from '~/lib/utils/axios_utils';
+import dismissibleContainer from '~/vue_shared/components/dismissible_container.vue';
+
+describe('DismissibleContainer', () => {
+ let wrapper;
+ const propsData = {
+ path: 'some/path',
+ featureId: 'some-feature-id',
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('template', () => {
+ const findBtn = () => wrapper.find('[data-testid="close"]');
+ let mockAxios;
+
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ wrapper = shallowMount(dismissibleContainer, { propsData });
+ });
+
+ afterEach(() => {
+ mockAxios.restore();
+ });
+
+ it('successfully dismisses', () => {
+ mockAxios.onPost(propsData.path).replyOnce(200);
+ const button = findBtn();
+
+ button.trigger('click');
+
+ expect(wrapper.emitted().dismiss).toBeTruthy();
+ });
+ });
+
+ describe('slots', () => {
+ const slots = {
+ title: 'Foo Title',
+ default: 'default slot',
+ };
+
+ it.each(Object.keys(slots))('renders the %s slot', slot => {
+ const slotContent = slots[slot];
+ wrapper = shallowMount(dismissibleContainer, {
+ propsData,
+ slots: {
+ [slot]: `<span>${slotContent}</span>`,
+ },
+ });
+
+ expect(wrapper.text()).toContain(slotContent);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
new file mode 100644
index 00000000000..4c4baf23120
--- /dev/null
+++ b/spec/frontend/vue_shared/components/dismissible_feedback_alert_spec.js
@@ -0,0 +1,91 @@
+import { mount, shallowMount } from '@vue/test-utils';
+import { GlAlert, GlSprintf, GlLink } from '@gitlab/ui';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import Component from '~/vue_shared/components/dismissible_feedback_alert.vue';
+
+describe('Dismissible Feedback Alert', () => {
+ useLocalStorageSpy();
+
+ let wrapper;
+
+ const defaultProps = {
+ featureName: 'Dependency List',
+ feedbackLink: 'https://gitlab.link',
+ };
+
+ const STORAGE_DISMISSAL_KEY = 'dependency_list_feedback_dismissed';
+
+ const createComponent = ({ props, shallow } = {}) => {
+ const mountFn = shallow ? shallowMount : mount;
+
+ wrapper = mountFn(Component, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findAlert = () => wrapper.find(GlAlert);
+ const findLink = () => wrapper.find(GlLink);
+
+ describe('with default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('contains feature name', () => {
+ expect(findAlert().text()).toContain(defaultProps.featureName);
+ });
+
+ it('contains provided link', () => {
+ const link = findLink();
+
+ expect(link.attributes('href')).toBe(defaultProps.feedbackLink);
+ expect(link.attributes('target')).toBe('_blank');
+ });
+
+ it('should have the storage key set', () => {
+ expect(wrapper.vm.storageKey).toBe(STORAGE_DISMISSAL_KEY);
+ });
+ });
+
+ describe('dismissible', () => {
+ describe('after dismissal', () => {
+ beforeEach(() => {
+ createComponent({ shallow: false });
+ findAlert().vm.$emit('dismiss');
+ });
+
+ it('hides the alert', () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('should remember the dismissal state', () => {
+ expect(localStorage.setItem).toHaveBeenCalledWith(STORAGE_DISMISSAL_KEY, 'true');
+ });
+ });
+
+ describe('already dismissed', () => {
+ it('should not show the alert once dismissed', async () => {
+ localStorage.setItem(STORAGE_DISMISSAL_KEY, 'true');
+ createComponent({ shallow: false });
+ await wrapper.vm.$nextTick();
+
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/file_finder/item_spec.js b/spec/frontend/vue_shared/components/file_finder/item_spec.js
index 63f2614106d..5a45a5dbba1 100644
--- a/spec/frontend/vue_shared/components/file_finder/item_spec.js
+++ b/spec/frontend/vue_shared/components/file_finder/item_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { file } from 'jest/ide/helpers';
-import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
import createComponent from 'helpers/vue_mount_component_helper';
+import ItemComponent from '~/vue_shared/components/file_finder/item.vue';
describe('File finder item spec', () => {
const Component = Vue.extend(ItemComponent);
diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js
index adf0da21f9f..e55449dc684 100644
--- a/spec/frontend/vue_shared/components/file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/file_icon_spec.js
@@ -36,6 +36,9 @@ describe('File Icon component', () => {
fileName | iconName
${'test.js'} | ${'javascript'}
${'test.png'} | ${'image'}
+ ${'test.PNG'} | ${'image'}
+ ${'.npmrc'} | ${'npm'}
+ ${'.Npmrc'} | ${'file'}
${'webpack.js'} | ${'webpack'}
`('should render a $iconName icon based on file ending', ({ fileName, iconName }) => {
createComponent({ fileName });
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index 46df2d2aaf1..1acd2e05464 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -1,8 +1,8 @@
import { file } from 'jest/ide/helpers';
-import FileRow from '~/vue_shared/components/file_row.vue';
-import FileHeader from '~/vue_shared/components/file_row_header.vue';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import FileRow from '~/vue_shared/components/file_row.vue';
+import FileHeader from '~/vue_shared/components/file_row_header.vue';
import { escapeFileUrl } from '~/lib/utils/url_utility';
describe('File row component', () => {
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 05508d14209..73dbecadd89 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
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
import {
GlFilteredSearch,
GlButtonGroup,
@@ -16,13 +16,16 @@ import RecentSearchesService from '~/filtered_search/services/recent_searches_se
import { mockAvailableTokens, mockSortOptions, mockHistoryItems } from './mock_data';
const createComponent = ({
+ shallow = true,
namespace = 'gitlab-org/gitlab-test',
recentSearchesStorageKey = 'requirements',
tokens = mockAvailableTokens,
- sortOptions = mockSortOptions,
+ sortOptions,
searchInputPlaceholder = 'Filter requirements',
-} = {}) =>
- shallowMount(FilteredSearchBarRoot, {
+} = {}) => {
+ const mountMethod = shallow ? shallowMount : mount;
+
+ return mountMethod(FilteredSearchBarRoot, {
propsData: {
namespace,
recentSearchesStorageKey,
@@ -31,12 +34,13 @@ const createComponent = ({
searchInputPlaceholder,
},
});
+};
describe('FilteredSearchBarRoot', () => {
let wrapper;
beforeEach(() => {
- wrapper = createComponent();
+ wrapper = createComponent({ sortOptions: mockSortOptions });
});
afterEach(() => {
@@ -44,23 +48,38 @@ describe('FilteredSearchBarRoot', () => {
});
describe('data', () => {
- it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props', () => {
+ it('initializes `filterValue`, `selectedSortOption` and `selectedSortDirection` data props and displays the sort dropdown', () => {
expect(wrapper.vm.filterValue).toEqual([]);
expect(wrapper.vm.selectedSortOption).toBe(mockSortOptions[0].sortDirection.descending);
expect(wrapper.vm.selectedSortDirection).toBe(SortDirection.descending);
+ expect(wrapper.contains(GlButtonGroup)).toBe(true);
+ expect(wrapper.contains(GlButton)).toBe(true);
+ expect(wrapper.contains(GlDropdown)).toBe(true);
+ expect(wrapper.contains(GlDropdownItem)).toBe(true);
+ });
+
+ it('does not initialize `selectedSortOption` and `selectedSortDirection` when `sortOptions` is not applied and hides the sort dropdown', () => {
+ const wrapperNoSort = createComponent();
+
+ expect(wrapperNoSort.vm.filterValue).toEqual([]);
+ expect(wrapperNoSort.vm.selectedSortOption).toBe(undefined);
+ expect(wrapperNoSort.contains(GlButtonGroup)).toBe(false);
+ expect(wrapperNoSort.contains(GlButton)).toBe(false);
+ expect(wrapperNoSort.contains(GlDropdown)).toBe(false);
+ expect(wrapperNoSort.contains(GlDropdownItem)).toBe(false);
});
});
describe('computed', () => {
describe('tokenSymbols', () => {
it('returns a map containing type and symbols from `tokens` prop', () => {
- expect(wrapper.vm.tokenSymbols).toEqual({ author_username: '@' });
+ expect(wrapper.vm.tokenSymbols).toEqual({ author_username: '@', label_name: '~' });
});
});
describe('tokenTitles', () => {
it('returns a map containing type and title from `tokens` prop', () => {
- expect(wrapper.vm.tokenTitles).toEqual({ author_username: 'Author' });
+ expect(wrapper.vm.tokenTitles).toEqual({ author_username: 'Author', label_name: 'Label' });
});
});
@@ -99,6 +118,29 @@ describe('FilteredSearchBarRoot', () => {
expect(wrapper.vm.sortDirectionTooltip).toBe('Sort direction: Descending');
});
});
+
+ describe('filteredRecentSearches', () => {
+ it('returns array of recent searches filtering out any string type (unsupported) items', async () => {
+ wrapper.setData({
+ recentSearches: [{ foo: 'bar' }, 'foo'],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.filteredRecentSearches).toHaveLength(1);
+ expect(wrapper.vm.filteredRecentSearches[0]).toEqual({ foo: 'bar' });
+ });
+
+ it('returns undefined when recentSearchesStorageKey prop is not set on component', async () => {
+ wrapper.setProps({
+ recentSearchesStorageKey: '',
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.filteredRecentSearches).not.toBeDefined();
+ });
+ });
});
describe('watchers', () => {
@@ -139,6 +181,46 @@ describe('FilteredSearchBarRoot', () => {
});
});
+ describe('removeQuotesEnclosure', () => {
+ const mockFilters = [
+ {
+ type: 'author_username',
+ value: {
+ data: 'root',
+ operator: '=',
+ },
+ },
+ {
+ type: 'label_name',
+ value: {
+ data: '"Documentation Update"',
+ operator: '=',
+ },
+ },
+ 'foo',
+ ];
+
+ it('returns filter array with unescaped strings for values which have spaces', () => {
+ expect(wrapper.vm.removeQuotesEnclosure(mockFilters)).toEqual([
+ {
+ type: 'author_username',
+ value: {
+ data: 'root',
+ operator: '=',
+ },
+ },
+ {
+ type: 'label_name',
+ value: {
+ data: 'Documentation Update',
+ operator: '=',
+ },
+ },
+ 'foo',
+ ]);
+ });
+ });
+
describe('handleSortOptionClick', () => {
it('emits component event `onSort` with selected sort by value', () => {
wrapper.vm.handleSortOptionClick(mockSortOptions[1]);
@@ -172,9 +254,12 @@ describe('FilteredSearchBarRoot', () => {
describe('handleHistoryItemSelected', () => {
it('emits `onFilter` event with provided filters param', () => {
+ jest.spyOn(wrapper.vm, 'removeQuotesEnclosure');
+
wrapper.vm.handleHistoryItemSelected(mockHistoryItems[0]);
expect(wrapper.emitted('onFilter')[0]).toEqual([mockHistoryItems[0]]);
+ expect(wrapper.vm.removeQuotesEnclosure).toHaveBeenCalledWith(mockHistoryItems[0]);
});
});
@@ -233,10 +318,21 @@ describe('FilteredSearchBarRoot', () => {
});
});
+ it('calls `blurSearchInput` method to remove focus from filter input field', () => {
+ jest.spyOn(wrapper.vm, 'blurSearchInput');
+
+ wrapper.find(GlFilteredSearch).vm.$emit('submit', mockFilters);
+
+ expect(wrapper.vm.blurSearchInput).toHaveBeenCalled();
+ });
+
it('emits component event `onFilter` with provided filters param', () => {
+ jest.spyOn(wrapper.vm, 'removeQuotesEnclosure');
+
wrapper.vm.handleFilterSubmit(mockFilters);
expect(wrapper.emitted('onFilter')[0]).toEqual([mockFilters]);
+ expect(wrapper.vm.removeQuotesEnclosure).toHaveBeenCalledWith(mockFilters);
});
});
});
@@ -260,13 +356,28 @@ describe('FilteredSearchBarRoot', () => {
expect(glFilteredSearchEl.props('historyItems')).toEqual(mockHistoryItems);
});
+ it('renders search history items dropdown with formatting done using token symbols', async () => {
+ const wrapperFullMount = createComponent({ sortOptions: mockSortOptions, shallow: false });
+ wrapperFullMount.vm.recentSearchesStore.addRecentSearch(mockHistoryItems[0]);
+
+ await wrapperFullMount.vm.$nextTick();
+
+ const searchHistoryItemsEl = wrapperFullMount.findAll(
+ '.gl-search-box-by-click-menu .gl-search-box-by-click-history-item',
+ );
+
+ expect(searchHistoryItemsEl.at(0).text()).toBe('Author := @tobyLabel := ~Bug"duo"');
+
+ wrapperFullMount.destroy();
+ });
+
it('renders sort dropdown component', () => {
expect(wrapper.find(GlButtonGroup).exists()).toBe(true);
expect(wrapper.find(GlDropdown).exists()).toBe(true);
expect(wrapper.find(GlDropdown).props('text')).toBe(mockSortOptions[0].title);
});
- it('renders dropdown items', () => {
+ it('renders sort dropdown items', () => {
const dropdownItemsEl = wrapper.findAll(GlDropdownItem);
expect(dropdownItemsEl).toHaveLength(mockSortOptions.length);
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
new file mode 100644
index 00000000000..a857f84adf1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/filtered_search_utils_spec.js
@@ -0,0 +1,19 @@
+import * as filteredSearchUtils from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
+
+describe('Filtered Search Utils', () => {
+ describe('stripQuotes', () => {
+ it.each`
+ inputValue | outputValue
+ ${'"Foo Bar"'} | ${'Foo Bar'}
+ ${"'Foo Bar'"} | ${'Foo Bar'}
+ ${'FooBar'} | ${'FooBar'}
+ ${"Foo'Bar"} | ${"Foo'Bar"}
+ ${'Foo"Bar'} | ${'Foo"Bar'}
+ `(
+ 'returns string $outputValue when called with string $inputValue',
+ ({ inputValue, outputValue }) => {
+ expect(filteredSearchUtils.stripQuotes(inputValue)).toBe(outputValue);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
index 7e28c4e11e1..dcccb1f49b6 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -1,5 +1,8 @@
+import { mockLabels } from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
import Api from '~/api';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
+import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
+import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
export const mockAuthor1 = {
id: 1,
@@ -30,6 +33,28 @@ export const mockAuthor3 = {
export const mockAuthors = [mockAuthor1, mockAuthor2, mockAuthor3];
+export const mockRegularMilestone = {
+ id: 1,
+ name: '4.0',
+ title: '4.0',
+};
+
+export const mockEscapedMilestone = {
+ id: 3,
+ name: '5.0 RC1',
+ title: '5.0 RC1',
+};
+
+export const mockMilestones = [
+ {
+ id: 2,
+ name: '5.0',
+ title: '5.0',
+ },
+ mockRegularMilestone,
+ mockEscapedMilestone,
+];
+
export const mockAuthorToken = {
type: 'author_username',
icon: 'user',
@@ -42,7 +67,29 @@ export const mockAuthorToken = {
fetchAuthors: Api.projectUsers.bind(Api),
};
-export const mockAvailableTokens = [mockAuthorToken];
+export const mockLabelToken = {
+ type: 'label_name',
+ icon: 'labels',
+ title: 'Label',
+ unique: false,
+ symbol: '~',
+ token: LabelToken,
+ operators: [{ value: '=', description: 'is', default: 'true' }],
+ fetchLabels: () => Promise.resolve(mockLabels),
+};
+
+export const mockMilestoneToken = {
+ type: 'milestone_title',
+ icon: 'clock',
+ title: 'Milestone',
+ unique: true,
+ symbol: '%',
+ token: MilestoneToken,
+ operators: [{ value: '=', description: 'is', default: 'true' }],
+ fetchMilestones: () => Promise.resolve({ data: mockMilestones }),
+};
+
+export const mockAvailableTokens = [mockAuthorToken, mockLabelToken];
export const mockHistoryItems = [
[
@@ -53,6 +100,13 @@ export const mockHistoryItems = [
operator: '=',
},
},
+ {
+ type: 'label_name',
+ value: {
+ data: 'Bug',
+ operator: '=',
+ },
+ },
'duo',
],
[
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
index 45294096eda..160febf9d06 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/author_token_spec.js
@@ -4,7 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
-import createFlash from '~/flash';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import AuthorToken from '~/vue_shared/components/filtered_search_bar/tokens/author_token.vue';
import { mockAuthorToken, mockAuthors } from '../mock_data';
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
new file mode 100644
index 00000000000..0e60ee99327
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/label_token_spec.js
@@ -0,0 +1,170 @@
+import { mount } from '@vue/test-utils';
+import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import {
+ mockRegularLabel,
+ mockLabels,
+} from 'jest/vue_shared/components/sidebar/labels_select_vue/mock_data';
+import axios from '~/lib/utils/axios_utils';
+
+import { deprecatedCreateFlash as createFlash } from '~/flash';
+import LabelToken from '~/vue_shared/components/filtered_search_bar/tokens/label_token.vue';
+
+import { mockLabelToken } from '../mock_data';
+
+jest.mock('~/flash');
+
+const createComponent = ({ config = mockLabelToken, value = { data: '' }, active = false } = {}) =>
+ mount(LabelToken, {
+ propsData: {
+ config,
+ value,
+ active,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ },
+ stubs: {
+ Portal: {
+ template: '<div><slot></slot></div>',
+ },
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+ },
+ });
+
+describe('LabelToken', () => {
+ let mock;
+ let wrapper;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ beforeEach(async () => {
+ // Label title with spaces is always enclosed in quotations by component.
+ wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
+
+ wrapper.setData({
+ labels: mockLabels,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ describe('currentValue', () => {
+ it('returns lowercase string for `value.data`', () => {
+ expect(wrapper.vm.currentValue).toBe('"foo label"');
+ });
+ });
+
+ describe('activeLabel', () => {
+ it('returns object for currently present `value.data`', () => {
+ expect(wrapper.vm.activeLabel).toEqual(mockRegularLabel);
+ });
+ });
+
+ describe('containerStyle', () => {
+ it('returns object containing `backgroundColor` and `color` properties based on `activeLabel` value', () => {
+ expect(wrapper.vm.containerStyle).toEqual({
+ backgroundColor: mockRegularLabel.color,
+ color: mockRegularLabel.textColor,
+ });
+ });
+
+ it('returns empty object when `activeLabel` is not set', async () => {
+ wrapper.setData({
+ labels: [],
+ });
+
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.containerStyle).toEqual({});
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('fetchLabelBySearchTerm', () => {
+ it('calls `config.fetchLabels` with provided searchTerm param', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchLabels');
+
+ wrapper.vm.fetchLabelBySearchTerm('foo');
+
+ expect(wrapper.vm.config.fetchLabels).toHaveBeenCalledWith('foo');
+ });
+
+ it('sets response to `labels` when request is succesful', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchLabels').mockResolvedValue(mockLabels);
+
+ wrapper.vm.fetchLabelBySearchTerm('foo');
+
+ return waitForPromises().then(() => {
+ expect(wrapper.vm.labels).toEqual(mockLabels);
+ });
+ });
+
+ it('calls `createFlash` with flash error message when request fails', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({});
+
+ wrapper.vm.fetchLabelBySearchTerm('foo');
+
+ return waitForPromises().then(() => {
+ expect(createFlash).toHaveBeenCalledWith('There was a problem fetching labels.');
+ });
+ });
+
+ it('sets `loading` to false when request completes', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchLabels').mockRejectedValue({});
+
+ wrapper.vm.fetchLabelBySearchTerm('foo');
+
+ return waitForPromises().then(() => {
+ expect(wrapper.vm.loading).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('template', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({ value: { data: `"${mockRegularLabel.title}"` } });
+
+ wrapper.setData({
+ labels: mockLabels,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ it('renders gl-filtered-search-token component', () => {
+ expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
+ });
+
+ it('renders token item when value is selected', () => {
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3); // Label, =, "Foo Label"
+ expect(tokenSegments.at(2).text()).toBe(`~${mockRegularLabel.title}`); // "Foo Label"
+ expect(
+ tokenSegments
+ .at(2)
+ .find('.gl-token')
+ .attributes('style'),
+ ).toBe('background-color: rgb(186, 218, 85); color: rgb(255, 255, 255);');
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
new file mode 100644
index 00000000000..de893bf44c8
--- /dev/null
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/milestone_token_spec.js
@@ -0,0 +1,152 @@
+import { mount } from '@vue/test-utils';
+import { GlFilteredSearchToken, GlFilteredSearchTokenSegment } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
+import axios from '~/lib/utils/axios_utils';
+
+import createFlash from '~/flash';
+import MilestoneToken from '~/vue_shared/components/filtered_search_bar/tokens/milestone_token.vue';
+
+import {
+ mockMilestoneToken,
+ mockMilestones,
+ mockRegularMilestone,
+ mockEscapedMilestone,
+} from '../mock_data';
+
+jest.mock('~/flash');
+
+const createComponent = ({
+ config = mockMilestoneToken,
+ value = { data: '' },
+ active = false,
+} = {}) =>
+ mount(MilestoneToken, {
+ propsData: {
+ config,
+ value,
+ active,
+ },
+ provide: {
+ portalName: 'fake target',
+ alignSuggestions: function fakeAlignSuggestions() {},
+ },
+ stubs: {
+ Portal: {
+ template: '<div><slot></slot></div>',
+ },
+ GlFilteredSearchSuggestionList: {
+ template: '<div></div>',
+ methods: {
+ getValue: () => '=',
+ },
+ },
+ },
+ });
+
+describe('MilestoneToken', () => {
+ let mock;
+ let wrapper;
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ wrapper = createComponent();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ });
+
+ describe('computed', () => {
+ beforeEach(async () => {
+ // Milestone title with spaces is always enclosed in quotations by component.
+ wrapper = createComponent({ value: { data: `"${mockEscapedMilestone.title}"` } });
+
+ wrapper.setData({
+ milestones: mockMilestones,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ describe('currentValue', () => {
+ it('returns lowercase string for `value.data`', () => {
+ expect(wrapper.vm.currentValue).toBe('"5.0 rc1"');
+ });
+ });
+
+ describe('activeMilestone', () => {
+ it('returns object for currently present `value.data`', () => {
+ expect(wrapper.vm.activeMilestone).toEqual(mockEscapedMilestone);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('fetchMilestoneBySearchTerm', () => {
+ it('calls `config.fetchMilestones` with provided searchTerm param', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchMilestones');
+
+ wrapper.vm.fetchMilestoneBySearchTerm('foo');
+
+ expect(wrapper.vm.config.fetchMilestones).toHaveBeenCalledWith('foo');
+ });
+
+ it('sets response to `milestones` when request is successful', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockResolvedValue({
+ data: mockMilestones,
+ });
+
+ wrapper.vm.fetchMilestoneBySearchTerm();
+
+ return waitForPromises().then(() => {
+ expect(wrapper.vm.milestones).toEqual(mockMilestones);
+ });
+ });
+
+ it('calls `createFlash` with flash error message when request fails', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({});
+
+ wrapper.vm.fetchMilestoneBySearchTerm('foo');
+
+ return waitForPromises().then(() => {
+ expect(createFlash).toHaveBeenCalledWith('There was a problem fetching milestones.');
+ });
+ });
+
+ it('sets `loading` to false when request completes', () => {
+ jest.spyOn(wrapper.vm.config, 'fetchMilestones').mockRejectedValue({});
+
+ wrapper.vm.fetchMilestoneBySearchTerm('foo');
+
+ return waitForPromises().then(() => {
+ expect(wrapper.vm.loading).toBe(false);
+ });
+ });
+ });
+ });
+
+ describe('template', () => {
+ beforeEach(async () => {
+ wrapper = createComponent({ value: { data: `"${mockRegularMilestone.title}"` } });
+
+ wrapper.setData({
+ milestones: mockMilestones,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ it('renders gl-filtered-search-token component', () => {
+ expect(wrapper.find(GlFilteredSearchToken).exists()).toBe(true);
+ });
+
+ it('renders token item when value is selected', () => {
+ const tokenSegments = wrapper.findAll(GlFilteredSearchTokenSegment);
+
+ expect(tokenSegments).toHaveLength(3); // Milestone, =, '%"4.0"'
+ expect(tokenSegments.at(2).text()).toBe(`%"${mockRegularMilestone.title}"`); // "4.0 RC1"
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js b/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js
index 30e16bd12da..361b162b6a0 100644
--- a/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js
+++ b/spec/frontend/vue_shared/components/form/form_footer_actions_spec.js
@@ -1,5 +1,5 @@
-import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
import { shallowMount } from '@vue/test-utils';
+import FormFooterActions from '~/vue_shared/components/form/form_footer_actions.vue';
describe('Form Footer Actions', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/form/title_spec.js b/spec/frontend/vue_shared/components/form/title_spec.js
index 38ef1bb3aa7..452f3723e76 100644
--- a/spec/frontend/vue_shared/components/form/title_spec.js
+++ b/spec/frontend/vue_shared/components/form/title_spec.js
@@ -1,5 +1,5 @@
-import TitleField from '~/vue_shared/components/form/title.vue';
import { shallowMount } from '@vue/test-utils';
+import TitleField from '~/vue_shared/components/form/title.vue';
describe('Title edit field', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js
index 216563165d6..5233a64ce5e 100644
--- a/spec/frontend/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -35,7 +35,7 @@ describe('Header CI Component', () => {
vm.$destroy();
});
- const findActionButtons = () => vm.$el.querySelector('.header-action-buttons');
+ const findActionButtons = () => vm.$el.querySelector('[data-testid="headerButtons"]');
describe('render', () => {
beforeEach(() => {
diff --git a/spec/frontend/vue_shared/components/icon_spec.js b/spec/frontend/vue_shared/components/icon_spec.js
index a448953cc8e..16728e1705a 100644
--- a/spec/frontend/vue_shared/components/icon_spec.js
+++ b/spec/frontend/vue_shared/components/icon_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import { mount } from '@vue/test-utils';
import mountComponent from 'helpers/vue_mount_component_helper';
-import Icon from '~/vue_shared/components/icon.vue';
import iconsPath from '@gitlab/svgs/dist/icons.svg';
+import Icon from '~/vue_shared/components/icon.vue';
jest.mock('@gitlab/svgs/dist/icons.svg', () => 'testing');
diff --git a/spec/frontend/vue_shared/components/identicon_spec.js b/spec/frontend/vue_shared/components/identicon_spec.js
index 53a55dcd6bd..24fc3713e2b 100644
--- a/spec/frontend/vue_shared/components/identicon_spec.js
+++ b/spec/frontend/vue_shared/components/identicon_spec.js
@@ -25,7 +25,7 @@ describe('Identicon', () => {
});
describe('entity id is a number', () => {
- beforeEach(createComponent);
+ beforeEach(() => createComponent());
it('matches snapshot', () => {
expect(wrapper.element).toMatchSnapshot();
diff --git a/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js b/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js
new file mode 100644
index 00000000000..2f910a10bc6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/issuable/issuable_header_warnings_spec.js
@@ -0,0 +1,73 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import Vuex from 'vuex';
+import IssuableHeaderWarnings from '~/vue_shared/components/issuable/issuable_header_warnings.vue';
+import createIssueStore from '~/notes/stores';
+import { createStore as createMrStore } from '~/mr_notes/stores';
+
+const ISSUABLE_TYPE_ISSUE = 'issue';
+const ISSUABLE_TYPE_MR = 'merge request';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('IssuableHeaderWarnings', () => {
+ let wrapper;
+ let store;
+
+ const findConfidentialIcon = () => wrapper.find('[data-testid="confidential"]');
+ const findLockedIcon = () => wrapper.find('[data-testid="locked"]');
+
+ const renderTestMessage = renders => (renders ? 'renders' : 'does not render');
+
+ const setLock = locked => {
+ store.getters.getNoteableData.discussion_locked = locked;
+ };
+
+ const setConfidential = confidential => {
+ store.getters.getNoteableData.confidential = confidential;
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMount(IssuableHeaderWarnings, { store, localVue });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ store = null;
+ });
+
+ describe.each`
+ issuableType
+ ${ISSUABLE_TYPE_ISSUE} | ${ISSUABLE_TYPE_MR}
+ `(`when issuableType=$issuableType`, ({ issuableType }) => {
+ beforeEach(() => {
+ store = issuableType === ISSUABLE_TYPE_ISSUE ? createIssueStore() : createMrStore();
+ createComponent();
+ });
+
+ describe.each`
+ lockStatus | confidentialStatus
+ ${true} | ${true}
+ ${true} | ${false}
+ ${false} | ${true}
+ ${false} | ${false}
+ `(
+ `when locked=$lockStatus and confidential=$confidentialStatus`,
+ ({ lockStatus, confidentialStatus }) => {
+ beforeEach(() => {
+ setLock(lockStatus);
+ setConfidential(confidentialStatus);
+ });
+
+ it(`${renderTestMessage(lockStatus)} the locked icon`, () => {
+ expect(findLockedIcon().exists()).toBe(lockStatus);
+ });
+
+ it(`${renderTestMessage(confidentialStatus)} the confidential icon`, () => {
+ expect(findConfidentialIcon().exists()).toBe(confidentialStatus);
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
index 548d4476c0f..192e33d8b00 100644
--- a/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_assignees_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
+import { mockAssigneesList } from 'jest/boards/mock_data';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
-import { mockAssigneesList } from 'jest/boards/mock_data';
const TEST_CSS_CLASSES = 'test-classes';
const TEST_MAX_VISIBLE = 4;
@@ -21,6 +21,11 @@ describe('IssueAssigneesComponent', () => {
vm = wrapper.vm;
};
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
const findTooltipText = () => wrapper.find('.js-assignee-tooltip').text();
const findAvatars = () => wrapper.findAll(UserAvatarLink);
const findOverflowCounter = () => wrapper.find('.avatar-counter');
@@ -123,6 +128,22 @@ describe('IssueAssigneesComponent', () => {
it('renders assignee @username', () => {
expect(findTooltipText()).toContain('@monserrate.gleichner');
});
+
+ it('does not render `@` when username not available', () => {
+ const userName = 'User without username';
+ factory({
+ assignees: [
+ {
+ name: userName,
+ },
+ ],
+ });
+
+ const tooltipText = findTooltipText();
+
+ expect(tooltipText).toContain(userName);
+ expect(tooltipText).not.toContain('@');
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
index 69d8c1a5918..b72f78c4f60 100644
--- a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -1,11 +1,10 @@
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
+import { mockMilestone } from 'jest/boards/mock_data';
import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
import Icon from '~/vue_shared/components/icon.vue';
-import { mockMilestone } from 'jest/boards/mock_data';
-
const createComponent = (milestone = mockMilestone) => {
const Component = Vue.extend(IssueMilestone);
diff --git a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
index fe9a5156539..fb9487d0bf8 100644
--- a/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
+++ b/spec/frontend/vue_shared/components/issue/related_issuable_item_spec.js
@@ -1,9 +1,9 @@
-import Vue from 'vue';
import { mount } from '@vue/test-utils';
+import { TEST_HOST } from 'jest/helpers/test_constants';
import { formatDate } from '~/lib/utils/datetime_utility';
import RelatedIssuableItem from '~/vue_shared/components/issue/related_issuable_item.vue';
+import IssueDueDate from '~/boards/components/issue_due_date.vue';
import { defaultAssignees, defaultMilestone } from './related_issuable_mock_data';
-import { TEST_HOST } from 'jest/helpers/test_constants';
describe('RelatedIssuableItem', () => {
let wrapper;
@@ -71,85 +71,65 @@ describe('RelatedIssuableItem', () => {
});
describe('token state', () => {
- let tokenState;
+ const tokenState = () => wrapper.find({ ref: 'iconElementXL' });
- beforeEach(done => {
+ beforeEach(() => {
wrapper.setProps({ state: 'opened' });
-
- Vue.nextTick(() => {
- tokenState = wrapper.find('.issue-token-state-icon-open');
-
- done();
- });
});
it('renders if hasState', () => {
- expect(tokenState.exists()).toBe(true);
+ expect(tokenState().exists()).toBe(true);
});
it('renders state title', () => {
- const stateTitle = tokenState.attributes('title');
+ const stateTitle = tokenState().attributes('title');
const formattedCreateDate = formatDate(props.createdAt);
expect(stateTitle).toContain('<span class="bold">Opened</span>');
-
expect(stateTitle).toContain(`<span class="text-tertiary">${formattedCreateDate}</span>`);
});
it('renders aria label', () => {
- expect(tokenState.attributes('aria-label')).toEqual('opened');
+ expect(tokenState().attributes('aria-label')).toEqual('opened');
});
it('renders open icon when open state', () => {
- expect(tokenState.classes('issue-token-state-icon-open')).toBe(true);
+ expect(tokenState().classes('issue-token-state-icon-open')).toBe(true);
});
- it('renders close icon when close state', done => {
+ it('renders close icon when close state', async () => {
wrapper.setProps({
state: 'closed',
closedAt: '2018-12-01T00:00:00.00Z',
});
+ await wrapper.vm.$nextTick();
- Vue.nextTick(() => {
- expect(tokenState.classes('issue-token-state-icon-closed')).toBe(true);
-
- done();
- });
+ expect(tokenState().classes('issue-token-state-icon-closed')).toBe(true);
});
});
describe('token metadata', () => {
- let tokenMetadata;
-
- beforeEach(done => {
- Vue.nextTick(() => {
- tokenMetadata = wrapper.find('.item-meta');
-
- done();
- });
- });
+ const tokenMetadata = () => wrapper.find('.item-meta');
it('renders item path and ID', () => {
- const pathAndID = tokenMetadata.find('.item-path-id').text();
+ const pathAndID = tokenMetadata()
+ .find('.item-path-id')
+ .text();
expect(pathAndID).toContain('gitlab-org/gitlab-test');
expect(pathAndID).toContain('#1');
});
it('renders milestone icon and name', () => {
- const milestoneIcon = tokenMetadata.find('.item-milestone svg use');
- const milestoneTitle = tokenMetadata.find('.item-milestone .milestone-title');
+ const milestoneIcon = tokenMetadata().find('.item-milestone svg use');
+ const milestoneTitle = tokenMetadata().find('.item-milestone .milestone-title');
expect(milestoneIcon.attributes('href')).toContain('clock');
expect(milestoneTitle.text()).toContain('Milestone title');
});
- it('renders due date component', () => {
- expect(tokenMetadata.find('.js-due-date-slot').exists()).toBe(true);
- });
-
- it('renders weight component', () => {
- expect(tokenMetadata.find('.js-weight-slot').exists()).toBe(true);
+ it('renders due date component with correct due date', () => {
+ expect(wrapper.find(IssueDueDate).props('date')).toBe(props.dueDate);
});
});
@@ -163,40 +143,30 @@ describe('RelatedIssuableItem', () => {
});
describe('remove button', () => {
- let removeBtn;
+ const removeButton = () => wrapper.find({ ref: 'removeButton' });
- beforeEach(done => {
+ beforeEach(() => {
wrapper.setProps({ canRemove: true });
- Vue.nextTick(() => {
- removeBtn = wrapper.find({ ref: 'removeButton' });
-
- done();
- });
});
it('renders if canRemove', () => {
- expect(removeBtn.exists()).toBe(true);
+ expect(removeButton().exists()).toBe(true);
});
- it('renders disabled button when removeDisabled', done => {
- wrapper.vm.removeDisabled = true;
-
- Vue.nextTick(() => {
- expect(removeBtn.attributes('disabled')).toEqual('disabled');
+ it('renders disabled button when removeDisabled', async () => {
+ wrapper.setData({ removeDisabled: true });
+ await wrapper.vm.$nextTick();
- done();
- });
+ expect(removeButton().attributes('disabled')).toEqual('disabled');
});
- it('triggers onRemoveRequest when clicked', () => {
- removeBtn.trigger('click');
+ it('triggers onRemoveRequest when clicked', async () => {
+ removeButton().trigger('click');
+ await wrapper.vm.$nextTick();
+ const { relatedIssueRemoveRequest } = wrapper.emitted();
- return wrapper.vm.$nextTick().then(() => {
- const { relatedIssueRemoveRequest } = wrapper.emitted();
-
- expect(relatedIssueRemoveRequest.length).toBe(1);
- expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]);
- });
+ expect(relatedIssueRemoveRequest.length).toBe(1);
+ expect(relatedIssueRemoveRequest[0]).toEqual([props.idKey]);
});
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 74be5f8230e..3da0a35f05a 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
-import fieldComponent from '~/vue_shared/components/markdown/field.vue';
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import fieldComponent from '~/vue_shared/components/markdown/field.vue';
import axios from '~/lib/utils/axios_utils';
const markdownPreviewPath = `${TEST_HOST}/preview`;
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js
index 78f27c9948b..16f60b5ff21 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/editor_service_spec.js
@@ -5,10 +5,13 @@ import {
registerHTMLToMarkdownRenderer,
addImage,
getMarkdown,
+ getEditorOptions,
} from '~/vue_shared/components/rich_content_editor/services/editor_service';
import buildHTMLToMarkdownRenderer from '~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer';
+import buildCustomRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer';
jest.mock('~/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer');
+jest.mock('~/vue_shared/components/rich_content_editor/services/build_custom_renderer');
describe('Editor Service', () => {
let mockInstance;
@@ -120,4 +123,25 @@ describe('Editor Service', () => {
expect(mockInstance.toMarkOptions.renderer).toBe(extendedRenderer);
});
});
+
+ describe('getEditorOptions', () => {
+ const externalOptions = {
+ customRenderers: {},
+ };
+ const renderer = {};
+
+ beforeEach(() => {
+ buildCustomRenderer.mockReturnValueOnce(renderer);
+ });
+
+ it('generates a configuration object with a custom HTML renderer and toolbarItems', () => {
+ expect(getEditorOptions()).toHaveProp('customHTMLRenderer', renderer);
+ expect(getEditorOptions()).toHaveProp('toolbarItems');
+ });
+
+ it('passes external renderers to the buildCustomRenderers function', () => {
+ getEditorOptions(externalOptions);
+ expect(buildCustomRenderer).toHaveBeenCalledWith(externalOptions.customRenderers);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js
new file mode 100644
index 00000000000..b9b93b274d2
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_integration_spec.js
@@ -0,0 +1,69 @@
+import Editor from '@toast-ui/editor';
+import { registerHTMLToMarkdownRenderer } from '~/vue_shared/components/rich_content_editor/services/editor_service';
+import buildMarkdownToHTMLRenderer from '~/vue_shared/components/rich_content_editor/services/build_custom_renderer';
+
+describe('vue_shared/components/rich_content_editor', () => {
+ let editor;
+
+ const buildEditor = () => {
+ editor = new Editor({
+ el: document.body,
+ customHTMLRenderer: buildMarkdownToHTMLRenderer(),
+ });
+
+ registerHTMLToMarkdownRenderer(editor);
+ };
+
+ beforeEach(() => {
+ buildEditor();
+ });
+
+ describe('HTML to Markdown', () => {
+ it('uses "-" character list marker in unordered lists', () => {
+ editor.setHtml('<ul><li>List item 1</li><li>List item 2</li></ul>');
+
+ const markdown = editor.getMarkdown();
+
+ expect(markdown).toBe('- List item 1\n- List item 2');
+ });
+
+ it('does not increment the list marker in ordered lists', () => {
+ editor.setHtml('<ol><li>List item 1</li><li>List item 2</li></ol>');
+
+ const markdown = editor.getMarkdown();
+
+ expect(markdown).toBe('1. List item 1\n1. List item 2');
+ });
+
+ it('indents lists using four spaces', () => {
+ editor.setHtml('<ul><li>List item 1</li><ul><li>List item 2</li></ul></ul>');
+
+ const markdown = editor.getMarkdown();
+
+ expect(markdown).toBe('- List item 1\n - List item 2');
+ });
+
+ it('uses * for strong and _ for emphasis text', () => {
+ editor.setHtml('<strong>strong text</strong><i>emphasis text</i>');
+
+ const markdown = editor.getMarkdown();
+
+ expect(markdown).toBe('**strong text**_emphasis text_');
+ });
+ });
+
+ describe('Markdown to HTML', () => {
+ it.each`
+ input | output
+ ${'markdown with _emphasized\ntext_'} | ${'<p>markdown with <em>emphasized text</em></p>\n'}
+ ${'markdown with **strong\ntext**'} | ${'<p>markdown with <strong>strong text</strong></p>\n'}
+ `(
+ 'does not transform softbreaks inside (_) and strong (**) nodes into <br/> tags',
+ ({ input, output }) => {
+ editor.setMarkdown(input);
+
+ expect(editor.getHtml()).toBe(output);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
index b6ff6aa767c..3d54db7fe5c 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/rich_content_editor_spec.js
@@ -2,7 +2,6 @@ import { shallowMount } from '@vue/test-utils';
import RichContentEditor from '~/vue_shared/components/rich_content_editor/rich_content_editor.vue';
import AddImageModal from '~/vue_shared/components/rich_content_editor/modals/add_image/add_image_modal.vue';
import {
- EDITOR_OPTIONS,
EDITOR_TYPES,
EDITOR_HEIGHT,
EDITOR_PREVIEW_STYLE,
@@ -14,6 +13,7 @@ import {
removeCustomEventListener,
addImage,
registerHTMLToMarkdownRenderer,
+ getEditorOptions,
} from '~/vue_shared/components/rich_content_editor/services/editor_service';
jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service', () => ({
@@ -22,6 +22,7 @@ jest.mock('~/vue_shared/components/rich_content_editor/services/editor_service',
removeCustomEventListener: jest.fn(),
addImage: jest.fn(),
registerHTMLToMarkdownRenderer: jest.fn(),
+ getEditorOptions: jest.fn(),
}));
describe('Rich Content Editor', () => {
@@ -32,13 +33,25 @@ describe('Rich Content Editor', () => {
const findEditor = () => wrapper.find({ ref: 'editor' });
const findAddImageModal = () => wrapper.find(AddImageModal);
- beforeEach(() => {
+ const buildWrapper = () => {
wrapper = shallowMount(RichContentEditor, {
propsData: { content, imageRoot },
});
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
});
describe('when content is loaded', () => {
+ const editorOptions = {};
+
+ beforeEach(() => {
+ getEditorOptions.mockReturnValueOnce(editorOptions);
+ buildWrapper();
+ });
+
it('renders an editor', () => {
expect(findEditor().exists()).toBe(true);
});
@@ -47,8 +60,8 @@ describe('Rich Content Editor', () => {
expect(findEditor().props().initialValue).toBe(content);
});
- it('provides the correct editor options', () => {
- expect(findEditor().props().options).toEqual(EDITOR_OPTIONS);
+ it('provides options generated by the getEditorOptions service', () => {
+ expect(findEditor().props().options).toBe(editorOptions);
});
it('has the correct preview style', () => {
@@ -65,6 +78,10 @@ describe('Rich Content Editor', () => {
});
describe('when content is changed', () => {
+ beforeEach(() => {
+ buildWrapper();
+ });
+
it('emits an input event with the changed content', () => {
const changedMarkdown = '## Changed Markdown';
const getMarkdownMock = jest.fn().mockReturnValueOnce(changedMarkdown);
@@ -77,6 +94,10 @@ describe('Rich Content Editor', () => {
});
describe('when content is reset', () => {
+ beforeEach(() => {
+ buildWrapper();
+ });
+
it('should reset the content via setMarkdown', () => {
const newContent = 'Just the body content excluding the front matter for example';
const mockInstance = { invoke: jest.fn() };
@@ -89,35 +110,33 @@ describe('Rich Content Editor', () => {
});
describe('when editor is loaded', () => {
- let mockEditorApi;
-
beforeEach(() => {
- mockEditorApi = { eventManager: { addEventType: jest.fn(), listen: jest.fn() } };
- findEditor().vm.$emit('load', mockEditorApi);
+ buildWrapper();
});
it('adds the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
expect(addCustomEventListener).toHaveBeenCalledWith(
- mockEditorApi,
+ wrapper.vm.editorApi,
CUSTOM_EVENTS.openAddImageModal,
wrapper.vm.onOpenAddImageModal,
);
});
it('registers HTML to markdown renderer', () => {
- expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(mockEditorApi);
+ expect(registerHTMLToMarkdownRenderer).toHaveBeenCalledWith(wrapper.vm.editorApi);
});
});
describe('when editor is destroyed', () => {
- it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
- const mockEditorApi = { eventManager: { removeEventHandler: jest.fn() } };
+ beforeEach(() => {
+ buildWrapper();
+ });
- wrapper.vm.editorApi = mockEditorApi;
+ it('removes the CUSTOM_EVENTS.openAddImageModal custom event listener', () => {
wrapper.vm.$destroy();
expect(removeCustomEventListener).toHaveBeenCalledWith(
- mockEditorApi,
+ wrapper.vm.editorApi,
CUSTOM_EVENTS.openAddImageModal,
wrapper.vm.onOpenAddImageModal,
);
@@ -125,6 +144,10 @@ describe('Rich Content Editor', () => {
});
describe('add image modal', () => {
+ beforeEach(() => {
+ buildWrapper();
+ });
+
it('renders an addImageModal component', () => {
expect(findAddImageModal().exists()).toBe(true);
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
index 0e8610a22f5..a90d3528d60 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/build_html_to_markdown_renderer_spec.js
@@ -47,4 +47,87 @@ describe('HTMLToMarkdownRenderer', () => {
expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, list);
});
});
+
+ describe('UL LI visitor', () => {
+ it.each`
+ listItem | unorderedListBulletChar | result | bulletChar
+ ${'* list item'} | ${undefined} | ${'- list item'} | ${'default'}
+ ${' - list item'} | ${'*'} | ${' * list item'} | ${'*'}
+ ${' * list item'} | ${'-'} | ${' - list item'} | ${'-'}
+ `(
+ 'uses $bulletChar bullet char in unordered list items when $unorderedListBulletChar is set in config',
+ ({ listItem, unorderedListBulletChar, result }) => {
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
+ unorderedListBulletChar,
+ });
+ baseRenderer.convert.mockReturnValueOnce(listItem);
+
+ expect(htmlToMarkdownRenderer['UL LI'](NODE, listItem)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, listItem);
+ },
+ );
+ });
+
+ describe('OL LI visitor', () => {
+ it.each`
+ listItem | result | incrementListMarker | action
+ ${'2. list item'} | ${'1. list item'} | ${false} | ${'increments'}
+ ${' 3. list item'} | ${' 1. list item'} | ${false} | ${'increments'}
+ ${' 123. list item'} | ${' 1. list item'} | ${false} | ${'increments'}
+ ${'3. list item'} | ${'3. list item'} | ${true} | ${'does not increment'}
+ `(
+ '$action a list item counter when incrementListMaker is $incrementListMarker',
+ ({ listItem, result, incrementListMarker }) => {
+ const subContent = null;
+
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
+ incrementListMarker,
+ });
+ baseRenderer.convert.mockReturnValueOnce(listItem);
+
+ expect(htmlToMarkdownRenderer['OL LI'](NODE, subContent)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, subContent);
+ },
+ );
+ });
+
+ describe('STRONG, B visitor', () => {
+ it.each`
+ input | strongCharacter | result
+ ${'**strong text**'} | ${'_'} | ${'__strong text__'}
+ ${'__strong text__'} | ${'*'} | ${'**strong text**'}
+ `(
+ 'converts $input to $result when strong character is $strongCharacter',
+ ({ input, strongCharacter, result }) => {
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
+ strong: strongCharacter,
+ });
+
+ baseRenderer.convert.mockReturnValueOnce(input);
+
+ expect(htmlToMarkdownRenderer['STRONG, B'](NODE, input)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input);
+ },
+ );
+ });
+
+ describe('EM, I visitor', () => {
+ it.each`
+ input | emphasisCharacter | result
+ ${'*strong text*'} | ${'_'} | ${'_strong text_'}
+ ${'_strong text_'} | ${'*'} | ${'*strong text*'}
+ `(
+ 'converts $input to $result when emphasis character is $emphasisCharacter',
+ ({ input, emphasisCharacter, result }) => {
+ htmlToMarkdownRenderer = buildHTMLToMarkdownRenderer(baseRenderer, {
+ emphasis: emphasisCharacter,
+ });
+
+ baseRenderer.convert.mockReturnValueOnce(input);
+
+ expect(htmlToMarkdownRenderer['EM, I'](NODE, input)).toBe(result);
+ expect(baseRenderer.convert).toHaveBeenCalledWith(NODE, input);
+ },
+ );
+ });
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js
index 18dff0a39bb..7a7e3055520 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token_spec.js
@@ -3,7 +3,7 @@ import {
buildUneditableOpenTokens,
buildUneditableCloseToken,
buildUneditableCloseTokens,
- buildUneditableTokens,
+ buildUneditableBlockTokens,
buildUneditableInlineTokens,
buildUneditableHtmlAsTextTokens,
} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
@@ -51,9 +51,9 @@ describe('Build Uneditable Token renderer helper', () => {
});
});
- describe('buildUneditableTokens', () => {
+ describe('buildUneditableBlockTokens', () => {
it('returns a 3-item array of tokens with the originToken wrapped in the middle of block tokens', () => {
- const result = buildUneditableTokens(originToken);
+ const result = buildUneditableBlockTokens(originToken);
expect(result).toHaveLength(3);
expect(result).toStrictEqual(uneditableTokens);
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js
index b723ee8c8a0..0c59d9f569b 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_spec.js
@@ -1,5 +1,5 @@
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_embedded_ruby_text';
-import { buildUneditableTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+import { renderUneditableLeaf } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
import { buildMockTextNode, normalTextNode } from './mock_data';
@@ -17,14 +17,8 @@ describe('Render Embedded Ruby Text renderer', () => {
});
describe('render', () => {
- const origin = jest.fn();
-
- it('should return uneditable tokens', () => {
- const context = { origin };
-
- expect(renderer.render(embeddedRubyTextNode, context)).toStrictEqual(
- buildUneditableTokens(origin()),
- );
+ it('should delegate rendering to the renderUneditableLeaf util', () => {
+ expect(renderer.render).toBe(renderUneditableLeaf);
});
});
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
index 320589e4de3..f4a06b91a10 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph_spec.js
@@ -1,8 +1,5 @@
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_identifier_paragraph';
-import {
- buildUneditableOpenTokens,
- buildUneditableCloseToken,
-} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
import { buildMockTextNode } from './mock_data';
@@ -40,26 +37,8 @@ describe('Render Identifier Paragraph renderer', () => {
});
describe('render', () => {
- let origin;
-
- beforeEach(() => {
- origin = jest.fn();
- });
-
- it('should return uneditable open tokens when entering', () => {
- const context = { entering: true, origin };
-
- expect(renderer.render(identifierParagraphNode, context)).toStrictEqual(
- buildUneditableOpenTokens(origin()),
- );
- });
-
- it('should return an uneditable close tokens when exiting', () => {
- const context = { entering: false, origin };
-
- expect(renderer.render(identifierParagraphNode, context)).toStrictEqual(
- buildUneditableCloseToken(origin()),
- );
+ it('should delegate rendering to the renderUneditableBranch util', () => {
+ expect(renderer.render).toBe(renderUneditableBranch);
});
});
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js
index e60bf1c8c92..7d427108ba6 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list_spec.js
@@ -1,8 +1,5 @@
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_list';
-import {
- buildUneditableOpenTokens,
- buildUneditableCloseToken,
-} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+import { renderUneditableBranch } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
import { buildMockTextNode } from './mock_data';
@@ -34,22 +31,8 @@ describe('Render Kramdown List renderer', () => {
});
describe('render', () => {
- const origin = jest.fn();
-
- it('should return uneditable open tokens when entering', () => {
- const context = { entering: true, origin };
-
- expect(renderer.render(kramdownListNode, context)).toStrictEqual(
- buildUneditableOpenTokens(origin()),
- );
- });
-
- it('should return an uneditable close tokens when exiting', () => {
- const context = { entering: false, origin };
-
- expect(renderer.render(kramdownListNode, context)).toStrictEqual(
- buildUneditableCloseToken(origin()),
- );
+ it('should delegate rendering to the renderUneditableBranch util', () => {
+ expect(renderer.render).toBe(renderUneditableBranch);
});
});
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js
index 97ff9794e69..1d2d152ffc3 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text_spec.js
@@ -1,5 +1,5 @@
import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_kramdown_text';
-import { buildUneditableTokens } from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+import { renderUneditableLeaf } from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
import { buildMockTextNode, normalTextNode } from './mock_data';
@@ -17,14 +17,8 @@ describe('Render Kramdown Text renderer', () => {
});
describe('render', () => {
- const origin = jest.fn();
-
- it('should return uneditable tokens', () => {
- const context = { origin };
-
- expect(renderer.render(kramdownTextNode, context)).toStrictEqual(
- buildUneditableTokens(origin()),
- );
+ it('should delegate rendering to the renderUneditableLeaf util', () => {
+ expect(renderer.render).toBe(renderUneditableLeaf);
});
});
});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js
new file mode 100644
index 00000000000..3c3d2354cb9
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_softbreak_spec.js
@@ -0,0 +1,23 @@
+import renderer from '~/vue_shared/components/rich_content_editor/services/renderers/render_softbreak';
+
+describe('Render softbreak renderer', () => {
+ describe('canRender', () => {
+ it.each`
+ node | parentType | result
+ ${{ parent: { type: 'emph' } }} | ${'emph'} | ${true}
+ ${{ parent: { type: 'strong' } }} | ${'strong'} | ${true}
+ ${{ parent: { type: 'paragraph' } }} | ${'paragraph'} | ${false}
+ `('returns $result when node parent type is $parentType ', ({ node, result }) => {
+ expect(renderer.canRender(node)).toBe(result);
+ });
+ });
+
+ describe('render', () => {
+ it('returns text node with a break line', () => {
+ expect(renderer.render()).toEqual({
+ type: 'text',
+ content: ' ',
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js
new file mode 100644
index 00000000000..92435b3e4e3
--- /dev/null
+++ b/spec/frontend/vue_shared/components/rich_content_editor/services/renderers/render_utils_spec.js
@@ -0,0 +1,44 @@
+import {
+ renderUneditableLeaf,
+ renderUneditableBranch,
+} from '~/vue_shared/components/rich_content_editor/services/renderers/render_utils';
+
+import {
+ buildUneditableBlockTokens,
+ buildUneditableOpenTokens,
+} from '~/vue_shared/components/rich_content_editor/services/renderers/build_uneditable_token';
+
+import { originToken, uneditableCloseToken } from './mock_data';
+
+describe('Render utils', () => {
+ describe('renderUneditableLeaf', () => {
+ it('should return uneditable block tokens around an origin token', () => {
+ const context = { origin: jest.fn().mockReturnValueOnce(originToken) };
+ const result = renderUneditableLeaf({}, context);
+
+ expect(result).toStrictEqual(buildUneditableBlockTokens(originToken));
+ });
+ });
+
+ describe('renderUneditableBranch', () => {
+ let origin;
+
+ beforeEach(() => {
+ origin = jest.fn().mockReturnValueOnce(originToken);
+ });
+
+ it('should return uneditable block open token followed by the origin token when entering', () => {
+ const context = { entering: true, origin };
+ const result = renderUneditableBranch({}, context);
+
+ expect(result).toStrictEqual(buildUneditableOpenTokens(originToken));
+ });
+
+ it('should return uneditable block closing token when exiting', () => {
+ const context = { entering: false, origin };
+ const result = renderUneditableBranch({}, context);
+
+ expect(result).toStrictEqual(uneditableCloseToken);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
index c33cffb421d..53e8a0e1278 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
-import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
import { GlLabel } from '@gitlab/ui';
+import DropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
import { mockConfig, mockLabels } from './mock_data';
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
index 68c9d26bb1a..cb758797c63 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_button_spec.js
@@ -41,23 +41,20 @@ describe('DropdownButton', () => {
describe('methods', () => {
describe('handleButtonClick', () => {
it.each`
- variant
- ${'standalone'}
- ${'embedded'}
+ variant | expectPropagationStopped
+ ${'standalone'} | ${true}
+ ${'embedded'} | ${false}
`(
- 'toggles dropdown content and stops event propagation when `state.variant` is "$variant"',
- ({ variant }) => {
+ 'toggles dropdown content and handles event propagation when `state.variant` is "$variant"',
+ ({ variant, expectPropagationStopped }) => {
const event = { stopPropagation: jest.fn() };
- wrapper = createComponent({
- ...mockConfig,
- variant,
- });
+ wrapper = createComponent({ ...mockConfig, variant });
findDropdownButton().vm.$emit('click', event);
expect(store.state.showDropdownContents).toBe(true);
- expect(event.stopPropagation).toHaveBeenCalled();
+ expect(event.stopPropagation).toHaveBeenCalledTimes(expectPropagationStopped ? 1 : 0);
},
);
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
index 9b01e0b9637..589be0ad7a4 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_labels_view_spec.js
@@ -17,53 +17,47 @@ import { mockConfig, mockLabels, mockRegularLabel } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
-const createComponent = (initialState = mockConfig) => {
- const store = new Vuex.Store({
- getters,
- mutations,
- state: {
- ...defaultState(),
- footerCreateLabelTitle: 'Create label',
- footerManageLabelTitle: 'Manage labels',
- },
- actions: {
- ...actions,
- fetchLabels: jest.fn(),
- },
- });
-
- store.dispatch('setInitialState', initialState);
- store.dispatch('receiveLabelsSuccess', mockLabels);
-
- return shallowMount(DropdownContentsLabelsView, {
- localVue,
- store,
- });
-};
-
describe('DropdownContentsLabelsView', () => {
let wrapper;
- let wrapperStandalone;
- let wrapperEmbedded;
- beforeEach(() => {
- wrapper = createComponent();
- wrapperStandalone = createComponent({
- ...mockConfig,
- variant: 'standalone',
+ const createComponent = (initialState = mockConfig) => {
+ const store = new Vuex.Store({
+ getters,
+ mutations,
+ state: {
+ ...defaultState(),
+ footerCreateLabelTitle: 'Create label',
+ footerManageLabelTitle: 'Manage labels',
+ },
+ actions: {
+ ...actions,
+ fetchLabels: jest.fn(),
+ },
});
- wrapperEmbedded = createComponent({
- ...mockConfig,
- variant: 'embedded',
+
+ store.dispatch('setInitialState', initialState);
+ store.dispatch('receiveLabelsSuccess', mockLabels);
+
+ wrapper = shallowMount(DropdownContentsLabelsView, {
+ localVue,
+ store,
});
+ };
+
+ beforeEach(() => {
+ createComponent();
});
afterEach(() => {
wrapper.destroy();
- wrapperStandalone.destroy();
- wrapperEmbedded.destroy();
+ wrapper = null;
});
+ const findDropdownContent = () => wrapper.find('[data-testid="dropdown-content"]');
+ const findDropdownTitle = () => wrapper.find('[data-testid="dropdown-title"]');
+ const findDropdownFooter = () => wrapper.find('[data-testid="dropdown-footer"]');
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+
describe('computed', () => {
describe('visibleLabels', () => {
it('returns matching labels filtered with `searchKey`', () => {
@@ -83,6 +77,24 @@ describe('DropdownContentsLabelsView', () => {
expect(wrapper.vm.visibleLabels.length).toBe(mockLabels.length);
});
});
+
+ describe('showListContainer', () => {
+ it.each`
+ variant | loading | showList
+ ${'sidebar'} | ${false} | ${true}
+ ${'sidebar'} | ${true} | ${false}
+ ${'not-sidebar'} | ${true} | ${true}
+ ${'not-sidebar'} | ${false} | ${true}
+ `(
+ 'returns $showList if `state.variant` is "$variant" and `labelsFetchInProgress` is $loading',
+ ({ variant, loading, showList }) => {
+ createComponent({ ...mockConfig, variant });
+ wrapper.vm.$store.state.labelsFetchInProgress = loading;
+
+ expect(wrapper.vm.showListContainer).toBe(showList);
+ },
+ );
+ });
});
describe('methods', () => {
@@ -199,7 +211,7 @@ describe('DropdownContentsLabelsView', () => {
wrapper.vm.$store.dispatch('requestLabels');
return wrapper.vm.$nextTick(() => {
- const loadingIconEl = wrapper.find(GlLoadingIcon);
+ const loadingIconEl = findLoadingIcon();
expect(loadingIconEl.exists()).toBe(true);
expect(loadingIconEl.attributes('class')).toContain('labels-fetch-loading');
@@ -207,22 +219,24 @@ describe('DropdownContentsLabelsView', () => {
});
it('renders dropdown title element', () => {
- const titleEl = wrapper.find('.dropdown-title > span');
+ const titleEl = findDropdownTitle();
expect(titleEl.exists()).toBe(true);
expect(titleEl.text()).toBe('Assign labels');
});
it('does not render dropdown title element when `state.variant` is "standalone"', () => {
- expect(wrapperStandalone.find('.dropdown-title').exists()).toBe(false);
+ createComponent({ ...mockConfig, variant: 'standalone' });
+ expect(findDropdownTitle().exists()).toBe(false);
});
it('renders dropdown title element when `state.variant` is "embedded"', () => {
- expect(wrapperEmbedded.find('.dropdown-title').exists()).toBe(true);
+ createComponent({ ...mockConfig, variant: 'embedded' });
+ expect(findDropdownTitle().exists()).toBe(true);
});
it('renders dropdown close button element', () => {
- const closeButtonEl = wrapper.find('.dropdown-title').find(GlButton);
+ const closeButtonEl = findDropdownTitle().find(GlButton);
expect(closeButtonEl.exists()).toBe(true);
expect(closeButtonEl.props('icon')).toBe('close');
@@ -249,8 +263,7 @@ describe('DropdownContentsLabelsView', () => {
});
return wrapper.vm.$nextTick(() => {
- const labelsEl = wrapper.findAll('.dropdown-content li');
- const labelItemEl = labelsEl.at(0).find(LabelItem);
+ const labelItemEl = findDropdownContent().find(LabelItem);
expect(labelItemEl.props('highlight')).toBe(true);
});
@@ -262,22 +275,28 @@ describe('DropdownContentsLabelsView', () => {
});
return wrapper.vm.$nextTick(() => {
- const noMatchEl = wrapper.find('.dropdown-content li');
+ const noMatchEl = findDropdownContent().find('li');
expect(noMatchEl.isVisible()).toBe(true);
expect(noMatchEl.text()).toContain('No matching results');
});
});
+ it('renders empty content while loading', () => {
+ wrapper.vm.$store.state.labelsFetchInProgress = true;
+
+ return wrapper.vm.$nextTick(() => {
+ const dropdownContent = findDropdownContent();
+
+ expect(dropdownContent.exists()).toBe(true);
+ expect(dropdownContent.isVisible()).toBe(false);
+ });
+ });
+
it('renders footer list items', () => {
- const createLabelLink = wrapper
- .find('.dropdown-footer')
- .findAll(GlLink)
- .at(0);
- const manageLabelsLink = wrapper
- .find('.dropdown-footer')
- .findAll(GlLink)
- .at(1);
+ const footerLinks = findDropdownFooter().findAll(GlLink);
+ const createLabelLink = footerLinks.at(0);
+ const manageLabelsLink = footerLinks.at(1);
expect(createLabelLink.exists()).toBe(true);
expect(createLabelLink.text()).toBe('Create label');
@@ -289,8 +308,7 @@ describe('DropdownContentsLabelsView', () => {
wrapper.vm.$store.state.allowLabelCreate = false;
return wrapper.vm.$nextTick(() => {
- const createLabelLink = wrapper
- .find('.dropdown-footer')
+ const createLabelLink = findDropdownFooter()
.findAll(GlLink)
.at(0);
@@ -299,11 +317,12 @@ describe('DropdownContentsLabelsView', () => {
});
it('does not render footer list items when `state.variant` is "standalone"', () => {
- expect(wrapperStandalone.find('.dropdown-footer').exists()).toBe(false);
+ createComponent({ ...mockConfig, variant: 'standalone' });
+ expect(findDropdownFooter().exists()).toBe(false);
});
it('renders footer list items when `state.variant` is "embedded"', () => {
- expect(wrapperEmbedded.find('.dropdown-footer').exists()).toBe(true);
+ expect(findDropdownFooter().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js
index bb462acf11c..97946993857 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_contents_spec.js
@@ -10,12 +10,13 @@ import { mockConfig } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
-const createComponent = (initialState = mockConfig) => {
+const createComponent = (initialState = mockConfig, propsData = {}) => {
const store = new Vuex.Store(labelsSelectModule());
store.dispatch('setInitialState', initialState);
return shallowMount(DropdownContents, {
+ propsData,
localVue,
store,
});
@@ -47,8 +48,15 @@ describe('DropdownContent', () => {
});
describe('template', () => {
- it('renders component container element with class `labels-select-dropdown-contents`', () => {
+ it('renders component container element with class `labels-select-dropdown-contents` and no styles', () => {
expect(wrapper.attributes('class')).toContain('labels-select-dropdown-contents');
+ expect(wrapper.attributes('style')).toBe(undefined);
+ });
+
+ it('renders component container element with styles when `renderOnTop` is true', () => {
+ wrapper = createComponent(mockConfig, { renderOnTop: true });
+
+ expect(wrapper.attributes('style')).toContain('bottom: 100%');
});
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
index 0717fd829a0..c1d9be7393c 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/dropdown_title_spec.js
@@ -1,7 +1,7 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlDeprecatedButton, GlLoadingIcon } from '@gitlab/ui';
+import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import DropdownTitle from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_title.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
@@ -42,7 +42,7 @@ describe('DropdownTitle', () => {
});
it('renders edit link', () => {
- const editBtnEl = wrapper.find(GlDeprecatedButton);
+ const editBtnEl = wrapper.find(GlButton);
expect(editBtnEl.exists()).toBe(true);
expect(editBtnEl.text()).toBe('Edit');
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
index 6e97b046be2..a1e0db4d29e 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/labels_select_root_spec.js
@@ -9,9 +9,14 @@ import DropdownButton from '~/vue_shared/components/sidebar/labels_select_vue/dr
import DropdownContents from '~/vue_shared/components/sidebar/labels_select_vue/dropdown_contents.vue';
import labelsSelectModule from '~/vue_shared/components/sidebar/labels_select_vue/store';
+import { isInViewport } from '~/lib/utils/common_utils';
import { mockConfig } from './mock_data';
+jest.mock('~/lib/utils/common_utils', () => ({
+ isInViewport: jest.fn().mockReturnValue(true),
+}));
+
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -21,6 +26,9 @@ const createComponent = (config = mockConfig, slots = {}) =>
slots,
store: new Vuex.Store(labelsSelectModule()),
propsData: config,
+ stubs: {
+ 'dropdown-contents': DropdownContents,
+ },
});
describe('LabelsSelectRoot', () => {
@@ -144,5 +152,42 @@ describe('LabelsSelectRoot', () => {
expect(wrapper.find(DropdownContents).exists()).toBe(true);
});
});
+
+ describe('sets content direction based on viewport', () => {
+ it('does not set direction when `state.variant` is not "embedded"', () => {
+ wrapper.vm.$store.dispatch('toggleDropdownContents');
+
+ wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
+ });
+ });
+
+ describe('when `state.variant` is "embedded"', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ ...mockConfig, variant: 'embedded' });
+ wrapper.vm.$store.dispatch('toggleDropdownContents');
+ });
+
+ it('set direction when out of viewport', () => {
+ isInViewport.mockImplementation(() => false);
+ wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(true);
+ });
+ });
+
+ it('does not set direction when inside of viewport', () => {
+ isInViewport.mockImplementation(() => true);
+ wrapper.vm.setContentIsOnViewport(wrapper.vm.$store.state);
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.find(DropdownContents).props('renderOnTop')).toBe(false);
+ });
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
index 072d8fe2fe2..c742220ba8a 100644
--- a/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/vue_shared/components/sidebar/labels_select_vue/store/actions_spec.js
@@ -1,10 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
import defaultState from '~/vue_shared/components/sidebar/labels_select_vue/store/state';
import * as types from '~/vue_shared/components/sidebar/labels_select_vue/store/mutation_types';
import * as actions from '~/vue_shared/components/sidebar/labels_select_vue/store/actions';
-import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
describe('LabelsSelect Actions', () => {
diff --git a/spec/frontend/vue_shared/components/split_button_spec.js b/spec/frontend/vue_shared/components/split_button_spec.js
index e09bc073042..f3bd4c14717 100644
--- a/spec/frontend/vue_shared/components/split_button_spec.js
+++ b/spec/frontend/vue_shared/components/split_button_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDeprecatedDropdown, GlDeprecatedDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import SplitButton from '~/vue_shared/components/split_button.vue';
@@ -25,10 +25,10 @@ describe('SplitButton', () => {
});
};
- const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdown = () => wrapper.find(GlDeprecatedDropdown);
const findDropdownItem = (index = 0) =>
findDropdown()
- .findAll(GlDropdownItem)
+ .findAll(GlDeprecatedDropdownItem)
.at(index);
const selectItem = index => {
findDropdownItem(index).vm.$emit('click');
diff --git a/spec/frontend/vue_shared/components/table_pagination_spec.js b/spec/frontend/vue_shared/components/table_pagination_spec.js
index 56ffffc7f0f..ef3ae088eec 100644
--- a/spec/frontend/vue_shared/components/table_pagination_spec.js
+++ b/spec/frontend/vue_shared/components/table_pagination_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { GlPagination } from '@gitlab/ui';
+import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
describe('Pagination component', () => {
let wrapper;
diff --git a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
index 46fcb92455b..691e19473c1 100644
--- a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
+++ b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
@@ -1,16 +1,19 @@
import { shallowMount } from '@vue/test-utils';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
+import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
describe('Time ago with tooltip component', () => {
let vm;
- const buildVm = (propsData = {}) => {
+ const buildVm = (propsData = {}, scopedSlots = {}) => {
vm = shallowMount(TimeAgoTooltip, {
propsData,
+ scopedSlots,
});
};
const timestamp = '2017-05-08T14:57:39.781Z';
+ const timeAgoTimestamp = getTimeago().format(timestamp);
afterEach(() => {
vm.destroy();
@@ -20,10 +23,9 @@ describe('Time ago with tooltip component', () => {
buildVm({
time: timestamp,
});
- const timeago = getTimeago();
expect(vm.attributes('title')).toEqual(formatDate(timestamp));
- expect(vm.text()).toEqual(timeago.format(timestamp));
+ expect(vm.text()).toEqual(timeAgoTimestamp);
});
it('should render provided html class', () => {
@@ -34,4 +36,16 @@ describe('Time ago with tooltip component', () => {
expect(vm.classes()).toContain('foo');
});
+
+ it('should render with the datetime attribute', () => {
+ buildVm({ time: timestamp });
+
+ expect(vm.attributes('datetime')).toEqual(timestamp);
+ });
+
+ it('should render provided scope content with the correct timeAgo string', () => {
+ buildVm({ time: timestamp }, { default: `<span>The time is {{ props.timeAgo }}</span>` });
+
+ expect(vm.text()).toEqual(`The time is ${timeAgoTimestamp}`);
+ });
});
diff --git a/spec/frontend/vue_shared/components/toggle_button_spec.js b/spec/frontend/vue_shared/components/toggle_button_spec.js
index 83bbb37a89a..f58647ff12b 100644
--- a/spec/frontend/vue_shared/components/toggle_button_spec.js
+++ b/spec/frontend/vue_shared/components/toggle_button_spec.js
@@ -32,7 +32,7 @@ describe('Toggle Button', () => {
it('renders input status icon', () => {
expect(vm.$el.querySelectorAll('span.toggle-icon').length).toEqual(1);
- expect(vm.$el.querySelectorAll('svg.s16.toggle-icon-svg').length).toEqual(1);
+ expect(vm.$el.querySelectorAll('svg.s18').length).toEqual(1);
});
});
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 1db1114f9ba..6f66d1cafb9 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
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlDeprecatedButton } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -37,7 +37,7 @@ describe('UserAvatarList', () => {
};
const clickButton = () => {
- const button = wrapper.find(GlDeprecatedButton);
+ const button = wrapper.find(GlButton);
button.vm.$emit('click');
};
@@ -112,7 +112,7 @@ describe('UserAvatarList', () => {
it('does not show button', () => {
factory();
- expect(wrapper.find(GlDeprecatedButton).exists()).toBe(false);
+ expect(wrapper.find(GlButton).exists()).toBe(false);
});
});
diff --git a/spec/frontend/vue_shared/directives/autofocusonshow_spec.js b/spec/frontend/vue_shared/directives/autofocusonshow_spec.js
index 90530b7d5c2..1c9e89f99e9 100644
--- a/spec/frontend/vue_shared/directives/autofocusonshow_spec.js
+++ b/spec/frontend/vue_shared/directives/autofocusonshow_spec.js
@@ -1,3 +1,4 @@
+import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
/**
@@ -6,20 +7,14 @@ import autofocusonshow from '~/vue_shared/directives/autofocusonshow';
* on underlying DOM methods.
*/
describe('AutofocusOnShow directive', () => {
+ useMockIntersectionObserver();
+
describe('with input invisible on component render', () => {
let el;
beforeEach(() => {
setFixtures('<div id="container" style="display: none;"><input id="inputel"/></div>');
el = document.querySelector('#inputel');
-
- window.IntersectionObserver = class {
- observe = jest.fn();
- };
- });
-
- afterEach(() => {
- delete window.IntersectionObserver;
});
it('should bind IntersectionObserver on input element', () => {
diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js
new file mode 100644
index 00000000000..a349aad9f1c
--- /dev/null
+++ b/spec/frontend/whats_new/components/app_spec.js
@@ -0,0 +1,57 @@
+import { createLocalVue, mount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { GlDrawer } from '@gitlab/ui';
+import App from '~/whats_new/components/app.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('App', () => {
+ let wrapper;
+ let store;
+ let actions;
+ let state;
+
+ beforeEach(() => {
+ actions = {
+ closeDrawer: jest.fn(),
+ };
+
+ state = {
+ open: true,
+ };
+
+ store = new Vuex.Store({
+ actions,
+ state,
+ });
+
+ wrapper = mount(App, {
+ localVue,
+ store,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const getDrawer = () => wrapper.find(GlDrawer);
+
+ it('contains a drawer', () => {
+ expect(getDrawer().exists()).toBe(true);
+ });
+
+ it('dispatches closeDrawer when clicking close', () => {
+ getDrawer().vm.$emit('close');
+ expect(actions.closeDrawer).toHaveBeenCalled();
+ });
+
+ it.each([true, false])('passes open property', async openState => {
+ wrapper.vm.$store.state.open = openState;
+
+ await wrapper.vm.$nextTick();
+
+ expect(getDrawer().props('open')).toBe(openState);
+ });
+});
diff --git a/spec/frontend/whats_new/components/trigger_spec.js b/spec/frontend/whats_new/components/trigger_spec.js
new file mode 100644
index 00000000000..7961957e077
--- /dev/null
+++ b/spec/frontend/whats_new/components/trigger_spec.js
@@ -0,0 +1,43 @@
+import { createLocalVue, mount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import { GlButton } from '@gitlab/ui';
+import Trigger from '~/whats_new/components/trigger.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Trigger', () => {
+ let wrapper;
+ let store;
+ let actions;
+ let state;
+
+ beforeEach(() => {
+ actions = {
+ openDrawer: jest.fn(),
+ };
+
+ state = {
+ open: true,
+ };
+
+ store = new Vuex.Store({
+ actions,
+ state,
+ });
+
+ wrapper = mount(Trigger, {
+ localVue,
+ store,
+ });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('dispatches openDrawer when clicking close', () => {
+ wrapper.find(GlButton).vm.$emit('click');
+ expect(actions.openDrawer).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/whats_new/store/actions_spec.js b/spec/frontend/whats_new/store/actions_spec.js
new file mode 100644
index 00000000000..d95453c9175
--- /dev/null
+++ b/spec/frontend/whats_new/store/actions_spec.js
@@ -0,0 +1,17 @@
+import testAction from 'helpers/vuex_action_helper';
+import actions from '~/whats_new/store/actions';
+import * as types from '~/whats_new/store/mutation_types';
+
+describe('whats new actions', () => {
+ describe('openDrawer', () => {
+ it('should commit openDrawer', () => {
+ testAction(actions.openDrawer, {}, {}, [{ type: types.OPEN_DRAWER }]);
+ });
+ });
+
+ describe('closeDrawer', () => {
+ it('should commit closeDrawer', () => {
+ testAction(actions.closeDrawer, {}, {}, [{ type: types.CLOSE_DRAWER }]);
+ });
+ });
+});
diff --git a/spec/frontend/whats_new/store/mutations_spec.js b/spec/frontend/whats_new/store/mutations_spec.js
new file mode 100644
index 00000000000..3c33364fed3
--- /dev/null
+++ b/spec/frontend/whats_new/store/mutations_spec.js
@@ -0,0 +1,25 @@
+import mutations from '~/whats_new/store/mutations';
+import createState from '~/whats_new/store/state';
+import * as types from '~/whats_new/store/mutation_types';
+
+describe('whats new mutations', () => {
+ let state;
+
+ beforeEach(() => {
+ state = createState;
+ });
+
+ describe('openDrawer', () => {
+ it('sets open to true', () => {
+ mutations[types.OPEN_DRAWER](state);
+ expect(state.open).toBe(true);
+ });
+ });
+
+ describe('closeDrawer', () => {
+ it('sets open to false', () => {
+ mutations[types.CLOSE_DRAWER](state);
+ expect(state.open).toBe(false);
+ });
+ });
+});