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

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/graphql_helpers.js14
-rw-r--r--spec/frontend/__helpers__/graphql_helpers_spec.js23
-rw-r--r--spec/frontend/__helpers__/stub_component.js25
-rw-r--r--spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js2
-rw-r--r--spec/frontend/admin/users/components/user_actions_spec.js138
-rw-r--r--spec/frontend/admin/users/components/user_avatar_spec.js64
-rw-r--r--spec/frontend/admin/users/components/user_date_spec.js34
-rw-r--r--spec/frontend/admin/users/components/users_table_spec.js26
-rw-r--r--spec/frontend/admin/users/index_spec.js4
-rw-r--r--spec/frontend/admin/users/mock_data.js1
-rw-r--r--spec/frontend/alert_management/components/alert_management_table_spec.js2
-rw-r--r--spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap98
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap406
-rw-r--r--spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js (renamed from spec/frontend/alerts_settings/alert_mapping_builder_spec.js)49
-rw-r--r--spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js (renamed from spec/frontend/alerts_settings/alerts_integrations_list_spec.js)0
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_form_spec.js (renamed from spec/frontend/alerts_settings/alerts_settings_form_spec.js)240
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js (renamed from spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js)2
-rw-r--r--spec/frontend/alerts_settings/components/mocks/apollo_mock.js (renamed from spec/frontend/alerts_settings/mocks/apollo_mock.js)0
-rw-r--r--spec/frontend/alerts_settings/components/mocks/integrations.json (renamed from spec/frontend/alerts_settings/mocks/integrations.json)0
-rw-r--r--spec/frontend/alerts_settings/components/util.js (renamed from spec/frontend/alerts_settings/util.js)0
-rw-r--r--spec/frontend/alerts_settings/mocks/alertFields.json123
-rw-r--r--spec/frontend/alerts_settings/utils/mapping_transformations_spec.js81
-rw-r--r--spec/frontend/api/api_utils_spec.js4
-rw-r--r--spec/frontend/api_spec.js22
-rw-r--r--spec/frontend/behaviors/autosize_spec.js32
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_spec.js.snap2
-rw-r--r--spec/frontend/blob/components/blob_content_spec.js2
-rw-r--r--spec/frontend/blob/components/blob_header_filepath_spec.js2
-rw-r--r--spec/frontend/blob/suggest_gitlab_ci_yml/components/popover_spec.js2
-rw-r--r--spec/frontend/boards/board_list_deprecated_spec.js2
-rw-r--r--spec/frontend/boards/board_list_helper.js2
-rw-r--r--spec/frontend/boards/board_list_spec.js4
-rw-r--r--spec/frontend/boards/boards_store_spec.js2
-rw-r--r--spec/frontend/boards/boards_util_spec.js17
-rw-r--r--spec/frontend/boards/components/board_assignee_dropdown_spec.js17
-rw-r--r--spec/frontend/boards/components/board_card_layout_deprecated_spec.js158
-rw-r--r--spec/frontend/boards/components/board_card_layout_spec.js65
-rw-r--r--spec/frontend/boards/components/board_configuration_options_spec.js15
-rw-r--r--spec/frontend/boards/components/board_content_spec.js2
-rw-r--r--spec/frontend/boards/components/board_form_spec.js2
-rw-r--r--spec/frontend/boards/components/board_list_header_deprecated_spec.js8
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js10
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js14
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js2
-rw-r--r--spec/frontend/boards/issue_card_deprecated_spec.js2
-rw-r--r--spec/frontend/boards/issue_card_inner_spec.js2
-rw-r--r--spec/frontend/boards/issue_spec.js2
-rw-r--r--spec/frontend/boards/mock_data.js4
-rw-r--r--spec/frontend/boards/stores/actions_spec.js81
-rw-r--r--spec/frontend/boards/stores/getters_spec.js18
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js42
-rw-r--r--spec/frontend/captcha/init_recaptcha_script_spec.js49
-rw-r--r--spec/frontend/ci_variable_list/store/actions_spec.js2
-rw-r--r--spec/frontend/clusters/components/__snapshots__/remove_cluster_confirmation_spec.js.snap8
-rw-r--r--spec/frontend/clusters/components/applications_spec.js58
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js2
-rw-r--r--spec/frontend/clusters_list/store/mutations_spec.js2
-rw-r--r--spec/frontend/create_cluster/eks_cluster/store/actions_spec.js2
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js2
-rw-r--r--spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js2
-rw-r--r--spec/frontend/deploy_keys/components/key_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_notes/design_discussion_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_sidebar_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_todo_button_spec.js2
-rw-r--r--spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap10
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js50
-rw-r--r--spec/frontend/design_management/pages/design/index_spec.js8
-rw-r--r--spec/frontend/design_management/pages/index_spec.js10
-rw-r--r--spec/frontend/design_management/utils/cache_update_spec.js2
-rw-r--r--spec/frontend/diffs/components/app_spec.js6
-rw-r--r--spec/frontend/diffs/components/compare_versions_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_content_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js296
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js63
-rw-r--r--spec/frontend/diffs/components/diff_row_spec.js2
-rw-r--r--spec/frontend/diffs/components/diff_row_utils_spec.js11
-rw-r--r--spec/frontend/diffs/components/diff_view_spec.js12
-rw-r--r--spec/frontend/diffs/components/inline_diff_table_row_spec.js2
-rw-r--r--spec/frontend/diffs/components/parallel_diff_table_row_spec.js2
-rw-r--r--spec/frontend/diffs/store/actions_spec.js2
-rw-r--r--spec/frontend/diffs/store/getters_spec.js22
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js2
-rw-r--r--spec/frontend/diffs/utils/diff_file_spec.js13
-rw-r--r--spec/frontend/diffs/utils/file_reviews_spec.js48
-rw-r--r--spec/frontend/editor/editor_lite_spec.js298
-rw-r--r--spec/frontend/environments/environment_actions_spec.js2
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js2
-rw-r--r--spec/frontend/feature_flags/components/edit_feature_flag_spec.js9
-rw-r--r--spec/frontend/feature_flags/components/form_spec.js43
-rw-r--r--spec/frontend/feature_flags/components/new_feature_flag_spec.js12
-rw-r--r--spec/frontend/filtered_search/recent_searches_root_spec.js49
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js2
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_search_input_spec.js34
-rw-r--r--spec/frontend/frequent_items/store/actions_spec.js2
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js13
-rw-r--r--spec/frontend/grafana_integration/components/__snapshots__/grafana_integration_spec.js.snap6
-rw-r--r--spec/frontend/groups/components/app_spec.js4
-rw-r--r--spec/frontend/groups/components/group_folder_spec.js2
-rw-r--r--spec/frontend/groups/components/group_item_spec.js2
-rw-r--r--spec/frontend/groups/components/groups_spec.js4
-rw-r--r--spec/frontend/groups/components/item_actions_spec.js16
-rw-r--r--spec/frontend/groups/members/mock_data.js33
-rw-r--r--spec/frontend/groups/members/utils_spec.js45
-rw-r--r--spec/frontend/ide/components/activity_bar_spec.js17
-rw-r--r--spec/frontend/ide/components/commit_sidebar/actions_spec.js25
-rw-r--r--spec/frontend/ide/components/commit_sidebar/form_spec.js391
-rw-r--r--spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js9
-rw-r--r--spec/frontend/ide/components/ide_sidebar_nav_spec.js3
-rw-r--r--spec/frontend/ide/components/ide_spec.js56
-rw-r--r--spec/frontend/ide/components/preview/navigator_spec.js2
-rw-r--r--spec/frontend/ide/lib/decorations/controller_spec.js2
-rw-r--r--spec/frontend/ide/lib/editor_spec.js3
-rw-r--r--spec/frontend/ide/stores/actions_spec.js2
-rw-r--r--spec/frontend/ide/stores/getters_spec.js5
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js17
-rw-r--r--spec/frontend/ide/stores/modules/commit/getters_spec.js9
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js156
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js103
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js88
-rw-r--r--spec/frontend/incidents_settings/components/__snapshots__/incidents_settings_tabs_spec.js.snap4
-rw-r--r--spec/frontend/incidents_settings/components/pagerduty_form_spec.js2
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js157
-rw-r--r--spec/frontend/integrations/edit/components/jira_issues_fields_spec.js69
-rw-r--r--spec/frontend/integrations/edit/store/actions_spec.js33
-rw-r--r--spec/frontend/integrations/edit/store/mutations_spec.js26
-rw-r--r--spec/frontend/integrations/edit/store/state_spec.js3
-rw-r--r--spec/frontend/integrations/integration_settings_form_spec.js79
-rw-r--r--spec/frontend/issuable_list/components/issuable_item_spec.js17
-rw-r--r--spec/frontend/issue_show/components/app_spec.js8
-rw-r--r--spec/frontend/issue_show/components/incidents/incident_tabs_spec.js2
-rw-r--r--spec/frontend/issue_show/issue_spec.js2
-rw-r--r--spec/frontend/issues_list/components/issuable_spec.js2
-rw-r--r--spec/frontend/jira_connect/api_spec.js2
-rw-r--r--spec/frontend/jira_connect/components/app_spec.js36
-rw-r--r--spec/frontend/jira_connect/components/groups_list_item_spec.js109
-rw-r--r--spec/frontend/jira_connect/components/groups_list_spec.js30
-rw-r--r--spec/frontend/jira_connect/index_spec.js56
-rw-r--r--spec/frontend/jira_connect/mock_data.js2
-rw-r--r--spec/frontend/jira_import/components/jira_import_form_spec.js79
-rw-r--r--spec/frontend/jobs/components/erased_block_spec.js4
-rw-r--r--spec/frontend/jobs/components/job_app_spec.js1
-rw-r--r--spec/frontend/jobs/components/job_sidebar_details_container_spec.js9
-rw-r--r--spec/frontend/jobs/components/job_sidebar_retry_button_spec.js2
-rw-r--r--spec/frontend/jobs/components/trigger_block_spec.js116
-rw-r--r--spec/frontend/lib/utils/array_utility_spec.js32
-rw-r--r--spec/frontend/lib/utils/color_utils_spec.js17
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js8
-rw-r--r--spec/frontend/lib/utils/datetime_utility_spec.js343
-rw-r--r--spec/frontend/lib/utils/unit_format/formatter_factory_spec.js21
-rw-r--r--spec/frontend/logs/components/log_advanced_filters_spec.js3
-rw-r--r--spec/frontend/logs/components/log_simple_filters_spec.js3
-rw-r--r--spec/frontend/logs/stores/actions_spec.js2
-rw-r--r--spec/frontend/members/components/app_spec.js (renamed from spec/frontend/groups/members/components/app_spec.js)8
-rw-r--r--spec/frontend/members/components/avatars/group_avatar_spec.js2
-rw-r--r--spec/frontend/members/components/avatars/invite_avatar_spec.js2
-rw-r--r--spec/frontend/members/components/avatars/user_avatar_spec.js2
-rw-r--r--spec/frontend/members/components/table/member_action_buttons_spec.js2
-rw-r--r--spec/frontend/members/components/table/member_avatar_spec.js2
-rw-r--r--spec/frontend/members/components/table/members_table_cell_spec.js22
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js9
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js3
-rw-r--r--spec/frontend/members/index_spec.js (renamed from spec/frontend/groups/members/index_spec.js)20
-rw-r--r--spec/frontend/members/mock_data.js6
-rw-r--r--spec/frontend/members/store/actions_spec.js12
-rw-r--r--spec/frontend/members/store/mutations_spec.js64
-rw-r--r--spec/frontend/members/utils_spec.js98
-rw-r--r--spec/frontend/merge_request/components/status_box_spec.js6
-rw-r--r--spec/frontend/milestones/milestone_combobox_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/anomaly_spec.js2
-rw-r--r--spec/frontend/monitoring/components/charts/single_stat_spec.js11
-rw-r--r--spec/frontend/monitoring/components/charts/time_series_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_actions_menu_spec.js4
-rw-r--r--spec/frontend/monitoring/components/dashboard_header_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_builder_spec.js5
-rw-r--r--spec/frontend/monitoring/components/dashboard_panel_spec.js32
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js2
-rw-r--r--spec/frontend/monitoring/components/dashboard_url_time_spec.js4
-rw-r--r--spec/frontend/monitoring/csv_export_spec.js2
-rw-r--r--spec/frontend/monitoring/mock_data.js2
-rw-r--r--spec/frontend/monitoring/requests/index_spec.js2
-rw-r--r--spec/frontend/monitoring/store/utils_spec.js2
-rw-r--r--spec/frontend/monitoring/store/variable_mapping_spec.js2
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js9
-rw-r--r--spec/frontend/notes/components/discussion_actions_spec.js2
-rw-r--r--spec/frontend/notes/components/discussion_counter_spec.js2
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js7
-rw-r--r--spec/frontend/notes/components/note_form_spec.js28
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js2
-rw-r--r--spec/frontend/notes/stores/actions_spec.js63
-rw-r--r--spec/frontend/notes/stores/mutation_spec.js13
-rw-r--r--spec/frontend/notifications/components/notifications_dropdown_spec.js240
-rw-r--r--spec/frontend/onboarding_issues/index_spec.js137
-rw-r--r--spec/frontend/packages/details/store/getters_spec.js2
-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.snap555
-rw-r--r--spec/frontend/packages/list/components/packages_filter_spec.js50
-rw-r--r--spec/frontend/packages/list/components/packages_list_app_spec.js49
-rw-r--r--spec/frontend/packages/list/components/packages_search_spec.js145
-rw-r--r--spec/frontend/packages/list/components/packages_sort_spec.js90
-rw-r--r--spec/frontend/packages/list/components/tokens/package_type_token_spec.js48
-rw-r--r--spec/frontend/packages/list/stores/actions_spec.js12
-rw-r--r--spec/frontend/packages/list/stores/mutations_spec.js9
-rw-r--r--spec/frontend/packages/shared/components/__snapshots__/package_list_row_spec.js.snap10
-rw-r--r--spec/frontend/packages/shared/components/packages_list_loader_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js99
-rw-r--r--spec/frontend/packages_and_registries/settings/group/mock_data.js12
-rw-r--r--spec/frontend/pages/projects/edit/mount_search_settings_spec.js25
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js2
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js8
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_form_spec.js6
-rw-r--r--spec/frontend/pipeline_editor/components/commit/commit_section_spec.js223
-rw-r--r--spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js34
-rw-r--r--spec/frontend/pipeline_editor/components/header/validation_segment_spec.js (renamed from spec/frontend/pipeline_editor/components/info/validation_segment_spec.js)9
-rw-r--r--spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js129
-rw-r--r--spec/frontend/pipeline_editor/components/text_editor_spec.js93
-rw-r--r--spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js42
-rw-r--r--spec/frontend/pipeline_editor/graphql/resolvers_spec.js6
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_app_spec.js424
-rw-r--r--spec/frontend/pipeline_editor/pipeline_editor_home_spec.js50
-rw-r--r--spec/frontend/pipeline_new/components/pipeline_new_form_spec.js113
-rw-r--r--spec/frontend/pipelines/blank_state_spec.js21
-rw-r--r--spec/frontend/pipelines/graph/graph_component_legacy_spec.js2
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js12
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js7
-rw-r--r--spec/frontend/pipelines/graph/stage_column_component_spec.js4
-rw-r--r--spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap23
-rw-r--r--spec/frontend/pipelines/graph_shared/links_inner_spec.js197
-rw-r--r--spec/frontend/pipelines/graph_shared/links_layer_spec.js (renamed from spec/frontend/pipelines/shared/links_layer_spec.js)0
-rw-r--r--spec/frontend/pipelines/header_component_spec.js8
-rw-r--r--spec/frontend/pipelines/legacy_header_component_spec.js116
-rw-r--r--spec/frontend/pipelines/pipeline_graph/mock_data.js135
-rw-r--r--spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js2
-rw-r--r--spec/frontend/pipelines/pipeline_url_spec.js28
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js64
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js6
-rw-r--r--spec/frontend/pipelines/pipelines_table_row_spec.js4
-rw-r--r--spec/frontend/pipelines/stage_spec.js278
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js5
-rw-r--r--spec/frontend/pipelines/test_reports/test_case_details_spec.js35
-rw-r--r--spec/frontend/pipelines/test_reports/test_suite_table_spec.js2
-rw-r--r--spec/frontend/pipelines/tokens/pipeline_trigger_author_token_spec.js2
-rw-r--r--spec/frontend/profile/account/components/delete_account_modal_spec.js2
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js2
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js5
-rw-r--r--spec/frontend/projects/commit/store/actions_spec.js2
-rw-r--r--spec/frontend/projects/commit_box/info/load_branches_spec.js2
-rw-r--r--spec/frontend/projects/compare/components/app_spec.js116
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_spec.js92
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap4
-rw-r--r--spec/frontend/projects/members/utils_spec.js14
-rw-r--r--spec/frontend/projects/pipelines/charts/components/app_spec.js118
-rw-r--r--spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js51
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js236
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js190
-rw-r--r--spec/frontend/projects/settings_service_desk/services/service_desk_service_spec.js111
-rw-r--r--spec/frontend/registry/explorer/components/delete_image_spec.js153
-rw-r--r--spec/frontend/registry/explorer/components/details_page/empty_state_spec.js54
-rw-r--r--spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js43
-rw-r--r--spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js2
-rw-r--r--spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js2
-rw-r--r--spec/frontend/registry/explorer/mock_data.js6
-rw-r--r--spec/frontend/registry/explorer/pages/details_spec.js58
-rw-r--r--spec/frontend/registry/explorer/pages/list_spec.js49
-rw-r--r--spec/frontend/releases/components/app_index_spec.js2
-rw-r--r--spec/frontend/releases/components/release_block_footer_spec.js2
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js2
-rw-r--r--spec/frontend/releases/stores/modules/list/actions_spec.js2
-rw-r--r--spec/frontend/releases/stores/modules/list/mutations_spec.js2
-rw-r--r--spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js5
-rw-r--r--spec/frontend/reports/codequality_report/mock_data.js50
-rw-r--r--spec/frontend/reports/codequality_report/store/actions_spec.js169
-rw-r--r--spec/frontend/reports/codequality_report/store/mutations_spec.js14
-rw-r--r--spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js18
-rw-r--r--spec/frontend/reports/components/grouped_test_reports_app_spec.js3
-rw-r--r--spec/frontend/reports/components/summary_row_spec.js4
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js96
-rw-r--r--spec/frontend/search/highlight_blob_search_result_spec.js3
-rw-r--r--spec/frontend/search/index_spec.js3
-rw-r--r--spec/frontend/search/mock_data.js22
-rw-r--r--spec/frontend/search/sort/components/app_spec.js168
-rw-r--r--spec/frontend/search/topbar/components/app_spec.js113
-rw-r--r--spec/frontend/search/topbar/components/project_filter_spec.js2
-rw-r--r--spec/frontend/search_settings/index_spec.js37
-rw-r--r--spec/frontend/search_settings/mount_spec.js36
-rw-r--r--spec/frontend/search_spec.js23
-rw-r--r--spec/frontend/serverless/components/environment_row_spec.js2
-rw-r--r--spec/frontend/serverless/store/actions_spec.js2
-rw-r--r--spec/frontend/sidebar/__snapshots__/todo_spec.js.snap2
-rw-r--r--spec/frontend/sidebar/assignees_realtime_spec.js2
-rw-r--r--spec/frontend/sidebar/assignees_spec.js2
-rw-r--r--spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js2
-rw-r--r--spec/frontend/sidebar/reviewers_spec.js9
-rw-r--r--spec/frontend/sidebar/subscriptions_spec.js25
-rw-r--r--spec/frontend/sidebar/todo_spec.js4
-rw-r--r--spec/frontend/snippets/components/show_spec.js2
-rw-r--r--spec/frontend/static_site_editor/components/edit_meta_modal_spec.js2
-rw-r--r--spec/frontend/static_site_editor/pages/success_spec.js2
-rw-r--r--spec/frontend/static_site_editor/services/front_matterify_spec.js3
-rw-r--r--spec/frontend/static_site_editor/services/parse_source_file_spec.js3
-rw-r--r--spec/frontend/terraform/components/states_table_actions_spec.js89
-rw-r--r--spec/frontend/terraform/components/states_table_spec.js19
-rw-r--r--spec/frontend/terraform/components/terraform_list_spec.js25
-rw-r--r--spec/frontend/user_lists/components/edit_user_list_spec.js2
-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_status_icon_spec.js46
-rw-r--r--spec/frontend/vue_mr_widget/components/mr_widget_suggest_pipeline_spec.js4
-rw-r--r--spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap24
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js6
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js10
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js16
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js26
-rw-r--r--spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js39
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/mr_widget_terraform_container_spec.js2
-rw-r--r--spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js8
-rw-r--r--spec/frontend/vue_mr_widget/mr_widget_options_spec.js75
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_details_spec.js (renamed from spec/frontend/alert_management/components/alert_details_spec.js)49
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js (renamed from spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js)8
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_metrics_spec.js (renamed from spec/frontend/alert_management/components/alert_metrics_spec.js)4
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_status_spec.js (renamed from spec/frontend/alert_management/components/alert_status_spec.js)27
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js (renamed from spec/frontend/alert_management/components/alert_summary_row_spec.js)2
-rw-r--r--spec/frontend/vue_shared/alert_details/mocks/alerts.json (renamed from spec/frontend/alert_management/mocks/alerts.json)0
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_managment_sidebar_assignees_spec.js (renamed from spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js)8
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js (renamed from spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js)6
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js (renamed from spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js)33
-rw-r--r--spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js (renamed from spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/color_picker/color_picker_spec.js39
-rw-r--r--spec/frontend/vue_shared/components/editor_lite_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/components/gl_countdown_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/gl_modal_vuex_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/help_popover_spec.js65
-rw-r--r--spec/frontend/vue_shared/components/issue/issue_milestone_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/modal_copy_button_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/mock_data.js107
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js113
-rw-r--r--spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap43
-rw-r--r--spec/frontend/vue_shared/components/settings/settings_block_spec.js86
-rw-r--r--spec/frontend/vue_shared/components/todo_button_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js2
-rw-r--r--spec/frontend/vue_shared/directives/track_event_spec.js2
-rw-r--r--spec/frontend/vue_shared/security_reports/mock_data.js17
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js368
-rw-r--r--spec/frontend/whats_new/store/actions_spec.js2
347 files changed, 8580 insertions, 4354 deletions
diff --git a/spec/frontend/__helpers__/graphql_helpers.js b/spec/frontend/__helpers__/graphql_helpers.js
new file mode 100644
index 00000000000..63123aa046f
--- /dev/null
+++ b/spec/frontend/__helpers__/graphql_helpers.js
@@ -0,0 +1,14 @@
+/**
+ * Returns a clone of the given object with all __typename keys omitted,
+ * including deeply nested ones.
+ *
+ * Only works with JSON-serializable objects.
+ *
+ * @param {object} An object with __typename keys (e.g., a GraphQL response)
+ * @returns {object} A new object with no __typename keys
+ */
+export const stripTypenames = (object) => {
+ return JSON.parse(
+ JSON.stringify(object, (key, value) => (key === '__typename' ? undefined : value)),
+ );
+};
diff --git a/spec/frontend/__helpers__/graphql_helpers_spec.js b/spec/frontend/__helpers__/graphql_helpers_spec.js
new file mode 100644
index 00000000000..dd23fbbf4e9
--- /dev/null
+++ b/spec/frontend/__helpers__/graphql_helpers_spec.js
@@ -0,0 +1,23 @@
+import { stripTypenames } from './graphql_helpers';
+
+describe('stripTypenames', () => {
+ it.each`
+ input | expected
+ ${{}} | ${{}}
+ ${{ __typename: 'Foo' }} | ${{}}
+ ${{ bar: 'bar', __typename: 'Foo' }} | ${{ bar: 'bar' }}
+ ${{ bar: { __typename: 'Bar' }, __typename: 'Foo' }} | ${{ bar: {} }}
+ ${{ bar: [{ __typename: 'Bar' }], __typename: 'Foo' }} | ${{ bar: [{}] }}
+ ${[]} | ${[]}
+ ${[{ __typename: 'Foo' }]} | ${[{}]}
+ ${[{ bar: [{ a: 1, __typename: 'Bar' }] }]} | ${[{ bar: [{ a: 1 }] }]}
+ `('given $input returns $expected, with all __typename keys removed', ({ input, expected }) => {
+ const actual = stripTypenames(input);
+ expect(actual).toEqual(expected);
+ expect(input).not.toBe(actual);
+ });
+
+ it('given null returns null', () => {
+ expect(stripTypenames(null)).toEqual(null);
+ });
+});
diff --git a/spec/frontend/__helpers__/stub_component.js b/spec/frontend/__helpers__/stub_component.js
index 45550450517..96fe3a8bc45 100644
--- a/spec/frontend/__helpers__/stub_component.js
+++ b/spec/frontend/__helpers__/stub_component.js
@@ -1,7 +1,32 @@
+/**
+ * Returns a new object with keys pointing to stubbed methods
+ *
+ * This is helpful for stubbing components like GlModal where it's supported
+ * in the API to call `.show()` and `.hide()` ([Bootstrap Vue docs][1]).
+ *
+ * [1]: https://bootstrap-vue.org/docs/components/modal#using-show-hide-and-toggle-component-methods
+ *
+ * @param {Object} methods - Object whose keys will be in the returned object.
+ */
+const createStubbedMethods = (methods = {}) => {
+ if (!methods) {
+ return {};
+ }
+
+ return Object.keys(methods).reduce(
+ (acc, key) =>
+ Object.assign(acc, {
+ [key]: () => {},
+ }),
+ {},
+ );
+};
+
export function stubComponent(Component, options = {}) {
return {
props: Component.props,
model: Component.model,
+ methods: createStubbedMethods(Component.methods),
// Do not render any slots/scoped slots except default
// This differs from VTU behavior which renders all slots
template: '<div><slot></slot></div>',
diff --git a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
index 1a3b151afa0..0faf0db4135 100644
--- a/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
+++ b/spec/frontend/add_context_commits_modal/components/add_context_commits_modal_spec.js
@@ -2,11 +2,11 @@ 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';
+import getDiffWithCommit from '../../diffs/mock_data/diff_with_commit';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/admin/users/components/user_actions_spec.js b/spec/frontend/admin/users/components/user_actions_spec.js
new file mode 100644
index 00000000000..78bc37233c2
--- /dev/null
+++ b/spec/frontend/admin/users/components/user_actions_spec.js
@@ -0,0 +1,138 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlDropdownDivider } from '@gitlab/ui';
+import AdminUserActions from '~/admin/users/components/user_actions.vue';
+import { generateUserPaths } from '~/admin/users/utils';
+
+import { users, paths } from '../mock_data';
+
+const BLOCK = 'block';
+const EDIT = 'edit';
+const LDAP = 'ldapBlocked';
+const DELETE = 'delete';
+const DELETE_WITH_CONTRIBUTIONS = 'deleteWithContributions';
+
+describe('AdminUserActions component', () => {
+ let wrapper;
+ const user = users[0];
+ const userPaths = generateUserPaths(paths, user.username);
+
+ const findEditButton = () => wrapper.find('[data-testid="edit"]');
+ const findActionsDropdown = () => wrapper.find('[data-testid="actions"');
+ const findDropdownDivider = () => wrapper.find(GlDropdownDivider);
+
+ const initComponent = ({ actions = [] } = {}) => {
+ wrapper = shallowMount(AdminUserActions, {
+ propsData: {
+ user: {
+ ...user,
+ actions,
+ },
+ paths,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('edit button', () => {
+ describe('when the user has an edit action attached', () => {
+ beforeEach(() => {
+ initComponent({ actions: [EDIT] });
+ });
+
+ it('renders the edit button linking to the user edit path', () => {
+ expect(findEditButton().exists()).toBe(true);
+ expect(findEditButton().attributes('href')).toBe(userPaths.edit);
+ });
+ });
+
+ describe('when there is no edit action attached to the user', () => {
+ beforeEach(() => {
+ initComponent({ actions: [] });
+ });
+
+ it('does not render the edit button linking to the user edit path', () => {
+ expect(findEditButton().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('actions dropdown', () => {
+ describe('when there are actions', () => {
+ const actions = [EDIT, BLOCK];
+
+ beforeEach(() => {
+ initComponent({ actions });
+ });
+
+ it('renders the actions dropdown', () => {
+ expect(findActionsDropdown().exists()).toBe(true);
+ });
+
+ it.each(actions)('renders a dropdown item for %s', (action) => {
+ const dropdownAction = wrapper.find(`[data-testid="${action}"]`);
+ expect(dropdownAction.exists()).toBe(true);
+ expect(dropdownAction.attributes('href')).toBe(userPaths[action]);
+ });
+
+ describe('when there is a LDAP action', () => {
+ beforeEach(() => {
+ initComponent({ actions: [LDAP] });
+ });
+
+ it('renders the LDAP dropdown item without a link', () => {
+ const dropdownAction = wrapper.find(`[data-testid="${LDAP}"]`);
+ expect(dropdownAction.exists()).toBe(true);
+ expect(dropdownAction.attributes('href')).toBe(undefined);
+ });
+ });
+
+ describe('when there is a delete action', () => {
+ const deleteActions = [DELETE, DELETE_WITH_CONTRIBUTIONS];
+
+ beforeEach(() => {
+ initComponent({ actions: [BLOCK, ...deleteActions] });
+ });
+
+ it('renders a dropdown divider', () => {
+ expect(findDropdownDivider().exists()).toBe(true);
+ });
+
+ it('only renders delete dropdown items for actions containing the word "delete"', () => {
+ const { length } = wrapper.findAll(`[data-testid*="delete-"]`);
+ expect(length).toBe(deleteActions.length);
+ });
+
+ it.each(deleteActions)('renders a delete dropdown item for %s', (action) => {
+ const deleteAction = wrapper.find(`[data-testid="delete-${action}"]`);
+ expect(deleteAction.exists()).toBe(true);
+ expect(deleteAction.attributes('href')).toBe(userPaths[action]);
+ });
+ });
+
+ describe('when there are no delete actions', () => {
+ it('does not render a dropdown divider', () => {
+ expect(findDropdownDivider().exists()).toBe(false);
+ });
+
+ it('does not render a delete dropdown item', () => {
+ const anyDeleteAction = wrapper.find(`[data-testid*="delete-"]`);
+ expect(anyDeleteAction.exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when there are no actions', () => {
+ beforeEach(() => {
+ initComponent({ actions: [] });
+ });
+
+ it('does not render the actions dropdown', () => {
+ expect(findActionsDropdown().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/admin/users/components/user_avatar_spec.js b/spec/frontend/admin/users/components/user_avatar_spec.js
index ba4e83690d0..0c048a06a34 100644
--- a/spec/frontend/admin/users/components/user_avatar_spec.js
+++ b/spec/frontend/admin/users/components/user_avatar_spec.js
@@ -1,7 +1,10 @@
-import { GlAvatarLink, GlAvatarLabeled, GlBadge } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { GlAvatarLink, GlAvatarLabeled, GlBadge, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
+import { LENGTH_OF_USER_NOTE_TOOLTIP } from '~/admin/users/constants';
+import { truncate } from '~/lib/utils/text_utility';
import { users, paths } from '../mock_data';
describe('AdminUserAvatar component', () => {
@@ -9,17 +12,25 @@ describe('AdminUserAvatar component', () => {
const user = users[0];
const adminUserPath = paths.adminUser;
+ const findNote = () => wrapper.find(GlIcon);
const findAvatar = () => wrapper.find(GlAvatarLabeled);
const findAvatarLink = () => wrapper.find(GlAvatarLink);
const findAllBadges = () => wrapper.findAll(GlBadge);
+ const findTooltip = () => getBinding(findNote().element, 'gl-tooltip');
const initComponent = (props = {}) => {
- wrapper = mount(AdminUserAvatar, {
+ wrapper = shallowMount(AdminUserAvatar, {
propsData: {
user,
adminUserPath,
...props,
},
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ stubs: {
+ GlAvatarLabeled,
+ },
});
};
@@ -53,11 +64,58 @@ describe('AdminUserAvatar component', () => {
expect(findAvatar().attributes('src')).toBe(user.avatarUrl);
});
+ it('renders a user note icon', () => {
+ expect(findNote().exists()).toBe(true);
+ expect(findNote().props('name')).toBe('document');
+ });
+
+ it("renders the user's note tooltip", () => {
+ const tooltip = findTooltip();
+
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe(user.note);
+ });
+
it("renders the user's badges", () => {
findAllBadges().wrappers.forEach((badge, idx) => {
expect(badge.text()).toBe(user.badges[idx].text);
expect(badge.props('variant')).toBe(user.badges[idx].variant);
});
});
+
+ describe('and the user note is very long', () => {
+ const noteText = new Array(LENGTH_OF_USER_NOTE_TOOLTIP + 1).join('a');
+
+ beforeEach(() => {
+ initComponent({
+ user: {
+ ...user,
+ note: noteText,
+ },
+ });
+ });
+
+ it("renders a truncated user's note tooltip", () => {
+ const tooltip = findTooltip();
+
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe(truncate(noteText, LENGTH_OF_USER_NOTE_TOOLTIP));
+ });
+ });
+
+ describe('and the user does not have a note', () => {
+ beforeEach(() => {
+ initComponent({
+ user: {
+ ...user,
+ note: null,
+ },
+ });
+ });
+
+ it('does not render a user note', () => {
+ expect(findNote().exists()).toBe(false);
+ });
+ });
});
});
diff --git a/spec/frontend/admin/users/components/user_date_spec.js b/spec/frontend/admin/users/components/user_date_spec.js
new file mode 100644
index 00000000000..6428b10059b
--- /dev/null
+++ b/spec/frontend/admin/users/components/user_date_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+
+import UserDate from '~/admin/users/components/user_date.vue';
+import { users } from '../mock_data';
+
+const mockDate = users[0].createdAt;
+
+describe('FormatDate component', () => {
+ let wrapper;
+
+ const initComponent = (props = {}) => {
+ wrapper = shallowMount(UserDate, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it.each`
+ date | output
+ ${mockDate} | ${'13 Nov, 2020'}
+ ${null} | ${'Never'}
+ ${undefined} | ${'Never'}
+ `('renders $date as $output', ({ date, output }) => {
+ initComponent({ date });
+
+ expect(wrapper.text()).toBe(output);
+ });
+});
diff --git a/spec/frontend/admin/users/components/users_table_spec.js b/spec/frontend/admin/users/components/users_table_spec.js
index b79d2d4d39d..ac48542cec5 100644
--- a/spec/frontend/admin/users/components/users_table_spec.js
+++ b/spec/frontend/admin/users/components/users_table_spec.js
@@ -3,6 +3,9 @@ import { mount } from '@vue/test-utils';
import AdminUsersTable from '~/admin/users/components/users_table.vue';
import AdminUserAvatar from '~/admin/users/components/user_avatar.vue';
+import AdminUserDate from '~/admin/users/components/user_date.vue';
+import AdminUserActions from '~/admin/users/components/user_actions.vue';
+
import { users, paths } from '../mock_data';
describe('AdminUsersTable component', () => {
@@ -39,18 +42,21 @@ describe('AdminUsersTable component', () => {
initComponent();
});
- it.each`
- key | label
- ${'name'} | ${'Name'}
- ${'projectsCount'} | ${'Projects'}
- ${'createdAt'} | ${'Created on'}
- ${'lastActivityOn'} | ${'Last activity'}
- `('renders users.$key in column $label', ({ key, label }) => {
- expect(getCellByLabel(0, label).text()).toContain(`${user[key]}`);
+ it('renders the projects count', () => {
+ expect(getCellByLabel(0, 'Projects').text()).toContain(`${user.projectsCount}`);
});
- it('renders an AdminUserAvatar component', () => {
- expect(getCellByLabel(0, 'Name').find(AdminUserAvatar).exists()).toBe(true);
+ it('renders the user actions', () => {
+ expect(wrapper.find(AdminUserActions).exists()).toBe(true);
+ });
+
+ it.each`
+ component | label
+ ${AdminUserAvatar} | ${'Name'}
+ ${AdminUserDate} | ${'Created on'}
+ ${AdminUserDate} | ${'Last activity'}
+ `('renders the component for column $label', ({ component, label }) => {
+ expect(getCellByLabel(0, label).find(component).exists()).toBe(true);
});
});
diff --git a/spec/frontend/admin/users/index_spec.js b/spec/frontend/admin/users/index_spec.js
index 171d54c8f4f..20b60bd8640 100644
--- a/spec/frontend/admin/users/index_spec.js
+++ b/spec/frontend/admin/users/index_spec.js
@@ -1,5 +1,5 @@
import { createWrapper } from '@vue/test-utils';
-import initAdminUsers from '~/admin/users';
+import { initAdminUsersApp } from '~/admin/users';
import AdminUsersApp from '~/admin/users/components/app.vue';
import { users, paths } from './mock_data';
@@ -16,7 +16,7 @@ describe('initAdminUsersApp', () => {
document.body.appendChild(el);
- wrapper = createWrapper(initAdminUsers(el));
+ wrapper = createWrapper(initAdminUsersApp(el));
});
afterEach(() => {
diff --git a/spec/frontend/admin/users/mock_data.js b/spec/frontend/admin/users/mock_data.js
index 860994a9152..c3918ef5173 100644
--- a/spec/frontend/admin/users/mock_data.js
+++ b/spec/frontend/admin/users/mock_data.js
@@ -14,6 +14,7 @@ export const users = [
],
projectsCount: 0,
actions: [],
+ note: 'Create per issue #999',
},
];
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 0cc3d565e10..21217e6c608 100644
--- a/spec/frontend/alert_management/components/alert_management_table_spec.js
+++ b/spec/frontend/alert_management/components/alert_management_table_spec.js
@@ -2,11 +2,11 @@ import { mount } from '@vue/test-utils';
import { GlTable, GlAlert, GlLoadingIcon, GlDropdown, GlIcon, GlAvatar } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import mockAlerts from 'jest/vue_shared/alert_details/mocks/alerts.json';
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';
import FilteredSearchBar from '~/vue_shared/components/filtered_search_bar/filtered_search_bar_root.vue';
-import mockAlerts from '../mocks/alerts.json';
import defaultProvideValues from '../mocks/alerts_provide_config.json';
jest.mock('~/lib/utils/url_utility', () => ({
diff --git a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap b/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap
deleted file mode 100644
index ef68a6a2c32..00000000000
--- a/spec/frontend/alerts_settings/__snapshots__/alerts_settings_form_spec.js.snap
+++ /dev/null
@@ -1,98 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`AlertsSettingsFormNew with default values renders the initial template 1`] = `
-"<form class=\\"gl-mt-6\\">
- <h5 class=\\"gl-font-lg gl-my-5\\">Add new integrations</h5>
- <div id=\\"integration-type\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"integration-type__BV_label_\\" for=\\"integration-type\\" class=\\"d-block col-form-label\\">1. Select integration type</label>
- <div class=\\"bv-no-focus-ring\\"><select class=\\"gl-form-select mw-100 custom-select\\" id=\\"__BVID__8\\">
- <option value=\\"\\">Select integration type</option>
- <option value=\\"HTTP\\">HTTP Endpoint</option>
- <option value=\\"PROMETHEUS\\">External Prometheus</option>
- </select>
- <!---->
- <!---->
- <!---->
- <!---->
- </div>
- </div>
- <transition-stub css=\\"true\\" enterclass=\\"\\" leaveclass=\\"collapse show\\" entertoclass=\\"collapse show\\" leavetoclass=\\"collapse\\" enteractiveclass=\\"collapsing\\" leaveactiveclass=\\"collapsing\\" class=\\"gl-mt-3\\">
- <div class=\\"collapse\\" style=\\"display: none;\\" id=\\"__BVID__10\\">
- <div>
- <div id=\\"name-integration\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"name-integration__BV_label_\\" for=\\"name-integration\\" class=\\"d-block col-form-label\\">2. Name integration</label>
- <div class=\\"bv-no-focus-ring\\"><input type=\\"text\\" placeholder=\\"Enter integration name\\" class=\\"gl-form-input form-control\\" id=\\"__BVID__15\\">
- <!---->
- <!---->
- <!---->
- </div>
- </div>
- <div id=\\"integration-webhook\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"integration-webhook__BV_label_\\" for=\\"integration-webhook\\" class=\\"d-block col-form-label\\">3. Set up webhook</label>
- <div class=\\"bv-no-focus-ring\\"><span>Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the <a rel=\\"noopener noreferrer\\" target=\\"_blank\\" href=\\"https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html\\" class=\\"gl-link gl-display-inline-block\\">GitLab documentation</a> to learn more about configuring your endpoint.</span> <label class=\\"gl-display-flex gl-flex-direction-column gl-mb-0 gl-w-max-content gl-my-4 gl-font-weight-normal\\">
- <div class=\\"gl-toggle-wrapper\\"><span class=\\"gl-toggle-label\\">Active</span>
- <!----> <button aria-label=\\"Active\\" type=\\"button\\" class=\\"gl-toggle\\"><span class=\\"toggle-icon\\"><svg data-testid=\\"close-icon\\" aria-hidden=\\"true\\" class=\\"gl-icon s16\\"><use href=\\"#close\\"></use></svg></span></button></div>
- <!---->
- </label>
- <!---->
- <div class=\\"gl-my-4\\"><span class=\\"gl-font-weight-bold\\">
- Webhook URL
- </span>
- <div id=\\"url\\" readonly=\\"readonly\\">
- <div role=\\"group\\" class=\\"input-group\\">
- <!---->
- <!----> <input id=\\"url\\" type=\\"text\\" readonly=\\"readonly\\" class=\\"gl-form-input form-control\\">
- <div class=\\"input-group-append\\"><button title=\\"Copy\\" data-clipboard-text=\\"\\" aria-label=\\"Copy this value\\" type=\\"button\\" class=\\"btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon\\">
- <!----> <svg data-testid=\\"copy-to-clipboard-icon\\" aria-hidden=\\"true\\" class=\\"gl-button-icon gl-icon s16\\">
- <use href=\\"#copy-to-clipboard\\"></use>
- </svg>
- <!----></button></div>
- <!---->
- </div>
- </div>
- </div>
- <div class=\\"gl-my-4\\"><span class=\\"gl-font-weight-bold\\">
- Authorization key
- </span>
- <div id=\\"authorization-key\\" readonly=\\"readonly\\" class=\\"gl-mb-3\\">
- <div role=\\"group\\" class=\\"input-group\\">
- <!---->
- <!----> <input id=\\"authorization-key\\" type=\\"text\\" readonly=\\"readonly\\" class=\\"gl-form-input form-control\\">
- <div class=\\"input-group-append\\"><button title=\\"Copy\\" data-clipboard-text=\\"\\" aria-label=\\"Copy this value\\" type=\\"button\\" class=\\"btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon\\">
- <!----> <svg data-testid=\\"copy-to-clipboard-icon\\" aria-hidden=\\"true\\" class=\\"gl-button-icon gl-icon s16\\">
- <use href=\\"#copy-to-clipboard\\"></use>
- </svg>
- <!----></button></div>
- <!---->
- </div>
- </div> <button type=\\"button\\" disabled=\\"disabled\\" class=\\"btn btn-default btn-md disabled gl-button\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">
- Reset Key
- </span></button>
- <!---->
- </div>
- <!---->
- <!---->
- <!---->
- </div>
- </div>
- <div id=\\"test-integration\\" role=\\"group\\" class=\\"form-group gl-form-group\\"><label id=\\"test-integration__BV_label_\\" for=\\"test-integration\\" class=\\"d-block col-form-label\\">4. Sample alert payload (optional)</label>
- <div class=\\"bv-no-focus-ring\\"><span>Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to test the integration (optional).</span> <textarea id=\\"test-payload\\" disabled=\\"disabled\\" placeholder=\\"{ &quot;events&quot;: [{ &quot;application&quot;: &quot;Name of application&quot; }] }\\" wrap=\\"soft\\" class=\\"gl-form-input gl-form-textarea gl-my-3 form-control is-valid\\" style=\\"resize: none; overflow-y: scroll;\\"></textarea>
- <!---->
- <!---->
- <!---->
- </div>
- </div>
- <!---->
- <!---->
- </div>
- <div class=\\"gl-display-flex gl-justify-content-start gl-py-3\\"><button data-testid=\\"integration-form-submit\\" type=\\"submit\\" class=\\"btn js-no-auto-disable btn-success btn-md gl-button\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">Save integration
- </span></button> <button data-testid=\\"integration-test-and-submit\\" type=\\"button\\" disabled=\\"disabled\\" class=\\"btn gl-mx-3 js-no-auto-disable btn-success btn-md disabled gl-button btn-success-secondary\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">Save and test payload</span></button> <button type=\\"reset\\" class=\\"btn js-no-auto-disable btn-default btn-md gl-button\\">
- <!---->
- <!----> <span class=\\"gl-button-text\\">Cancel</span></button></div>
- </div>
- </transition-stub>
-</form>"
-`;
diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap
new file mode 100644
index 00000000000..eb2b82a0211
--- /dev/null
+++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_settings_form_spec.js.snap
@@ -0,0 +1,406 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`AlertsSettingsForm with default values renders the initial template 1`] = `
+<form
+ class="gl-mt-6"
+>
+ <h5
+ class="gl-font-lg gl-my-5"
+ >
+ Add new integrations
+ </h5>
+
+ <div
+ class="form-group gl-form-group"
+ id="integration-type"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="integration-type"
+ id="integration-type__BV_label_"
+ >
+ 1. Select integration type
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <select
+ class="gl-form-select mw-100 custom-select"
+ id="__BVID__8"
+ >
+ <option
+ value=""
+ >
+ Select integration type
+ </option>
+ <option
+ value="HTTP"
+ >
+ HTTP Endpoint
+ </option>
+ <option
+ value="PROMETHEUS"
+ >
+ External Prometheus
+ </option>
+ </select>
+
+ <!---->
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <transition-stub
+ class="gl-mt-3"
+ css="true"
+ enteractiveclass="collapsing"
+ enterclass=""
+ entertoclass="collapse show"
+ leaveactiveclass="collapsing"
+ leaveclass="collapse show"
+ leavetoclass="collapse"
+ >
+ <div
+ class="collapse"
+ id="__BVID__10"
+ style="display: none;"
+ >
+ <div>
+ <div
+ class="form-group gl-form-group"
+ id="name-integration"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="name-integration"
+ id="name-integration__BV_label_"
+ >
+ 2. Name integration
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <input
+ class="gl-form-input form-control"
+ id="__BVID__15"
+ placeholder="Enter integration name"
+ type="text"
+ />
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <div
+ class="form-group gl-form-group"
+ id="integration-webhook"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="integration-webhook"
+ id="integration-webhook__BV_label_"
+ >
+ 3. Set up webhook
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <span>
+ Utilize the URL and authorization key below to authorize an external service to send alerts to GitLab. Review your external service's documentation to learn where to add these details, and the
+ <a
+ class="gl-link gl-display-inline-block"
+ href="https://docs.gitlab.com/ee/operations/incident_management/alert_integrations.html"
+ rel="noopener noreferrer"
+ target="_blank"
+ >
+ GitLab documentation
+ </a>
+ to learn more about configuring your endpoint.
+ </span>
+
+ <label
+ class="gl-display-flex gl-flex-direction-column gl-mb-0 gl-w-max-content gl-my-4 gl-font-weight-normal"
+ >
+ <span
+ class="gl-toggle-wrapper"
+ >
+ <span
+ class="gl-toggle-label"
+ data-testid="toggle-label"
+ >
+ Active
+ </span>
+
+ <!---->
+
+ <button
+ aria-label="Active"
+ class="gl-toggle"
+ role="switch"
+ type="button"
+ >
+ <span
+ class="toggle-icon"
+ >
+ <svg
+ aria-hidden="true"
+ class="gl-icon s16"
+ data-testid="close-icon"
+ >
+ <use
+ href="#close"
+ />
+ </svg>
+ </span>
+ </button>
+ </span>
+
+ <!---->
+ </label>
+
+ <!---->
+
+ <div
+ class="gl-my-4"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+
+ Webhook URL
+
+ </span>
+
+ <div
+ id="url"
+ readonly="readonly"
+ >
+ <div
+ class="input-group"
+ role="group"
+ >
+ <!---->
+ <!---->
+
+ <input
+ class="gl-form-input form-control"
+ id="url"
+ readonly="readonly"
+ type="text"
+ />
+
+ <div
+ class="input-group-append"
+ >
+ <button
+ aria-label="Copy this value"
+ class="btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon"
+ data-clipboard-text=""
+ title="Copy"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="copy-to-clipboard-icon"
+ >
+ <use
+ href="#copy-to-clipboard"
+ />
+ </svg>
+
+ <!---->
+ </button>
+ </div>
+ <!---->
+ </div>
+ </div>
+ </div>
+
+ <div
+ class="gl-my-4"
+ >
+ <span
+ class="gl-font-weight-bold"
+ >
+
+ Authorization key
+
+ </span>
+
+ <div
+ class="gl-mb-3"
+ id="authorization-key"
+ readonly="readonly"
+ >
+ <div
+ class="input-group"
+ role="group"
+ >
+ <!---->
+ <!---->
+
+ <input
+ class="gl-form-input form-control"
+ id="authorization-key"
+ readonly="readonly"
+ type="text"
+ />
+
+ <div
+ class="input-group-append"
+ >
+ <button
+ aria-label="Copy this value"
+ class="btn gl-m-0! btn-default btn-md gl-button btn-default-secondary btn-icon"
+ data-clipboard-text=""
+ title="Copy"
+ type="button"
+ >
+ <!---->
+
+ <svg
+ aria-hidden="true"
+ class="gl-button-icon gl-icon s16"
+ data-testid="copy-to-clipboard-icon"
+ >
+ <use
+ href="#copy-to-clipboard"
+ />
+ </svg>
+
+ <!---->
+ </button>
+ </div>
+ <!---->
+ </div>
+ </div>
+
+ <button
+ class="btn btn-default btn-md disabled gl-button"
+ disabled="disabled"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+
+ Reset Key
+
+ </span>
+ </button>
+
+ <!---->
+ </div>
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <div
+ class="form-group gl-form-group"
+ id="test-integration"
+ role="group"
+ >
+ <label
+ class="d-block col-form-label"
+ for="test-integration"
+ id="test-integration__BV_label_"
+ >
+ 4. Sample alert payload (optional)
+ </label>
+ <div
+ class="bv-no-focus-ring"
+ >
+ <span>
+ Provide an example payload from the monitoring tool you intend to integrate with. This payload can be used to test the integration (optional).
+ </span>
+
+ <textarea
+ class="gl-form-input gl-form-textarea gl-my-3 form-control is-valid"
+ disabled="disabled"
+ id="test-payload"
+ placeholder="{ \\"events\\": [{ \\"application\\": \\"Name of application\\" }] }"
+ style="resize: none; overflow-y: scroll;"
+ wrap="soft"
+ />
+ <!---->
+ <!---->
+ <!---->
+ </div>
+ </div>
+
+ <!---->
+
+ <!---->
+ </div>
+
+ <div
+ class="gl-display-flex gl-justify-content-start gl-py-3"
+ >
+ <button
+ class="btn js-no-auto-disable btn-success btn-md gl-button"
+ data-testid="integration-form-submit"
+ type="submit"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Save integration
+
+ </span>
+ </button>
+
+ <button
+ class="btn gl-mx-3 js-no-auto-disable btn-success btn-md disabled gl-button btn-success-secondary"
+ data-testid="integration-test-and-submit"
+ disabled="disabled"
+ type="button"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Save and test payload
+ </span>
+ </button>
+
+ <button
+ class="btn js-no-auto-disable btn-default btn-md gl-button"
+ type="reset"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ Cancel
+ </span>
+ </button>
+ </div>
+ </div>
+ </transition-stub>
+</form>
+`;
diff --git a/spec/frontend/alerts_settings/alert_mapping_builder_spec.js b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
index 5d48ff02e35..1ec7b5c18f6 100644
--- a/spec/frontend/alerts_settings/alert_mapping_builder_spec.js
+++ b/spec/frontend/alerts_settings/components/alert_mapping_builder_spec.js
@@ -1,8 +1,10 @@
import { GlIcon, GlFormInput, GlDropdown, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import AlertMappingBuilder, { i18n } from '~/alerts_settings/components/alert_mapping_builder.vue';
-import gitlabFields from '~/alerts_settings/components/mocks/gitlabFields.json';
import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
+import { capitalizeFirstCharacter } from '~/lib/utils/text_utility';
+import * as transformationUtils from '~/alerts_settings/utils/mapping_transformations';
+import alertFields from '../mocks/alertFields.json';
describe('AlertMappingBuilder', () => {
let wrapper;
@@ -10,8 +12,9 @@ describe('AlertMappingBuilder', () => {
function mountComponent() {
wrapper = shallowMount(AlertMappingBuilder, {
propsData: {
- payloadFields: parsedMapping.samplePayload.payloadAlerFields.nodes,
- mapping: parsedMapping.storedMapping.nodes,
+ parsedPayload: parsedMapping.samplePayload.payloadAlerFields.nodes,
+ savedMapping: parsedMapping.storedMapping.nodes,
+ alertFields,
},
});
}
@@ -42,52 +45,58 @@ describe('AlertMappingBuilder', () => {
});
it('renders disabled form input for each mapped field', () => {
- gitlabFields.forEach((field, index) => {
+ alertFields.forEach((field, index) => {
const input = findColumnInRow(index + 1, 0).find(GlFormInput);
- expect(input.attributes('value')).toBe(`${field.label} (${field.type.join(' or ')})`);
+ const types = field.types.map((t) => capitalizeFirstCharacter(t.toLowerCase())).join(' or ');
+ expect(input.attributes('value')).toBe(`${field.label} (${types})`);
expect(input.attributes('disabled')).toBe('');
});
});
it('renders right arrow next to each input', () => {
- gitlabFields.forEach((field, index) => {
+ alertFields.forEach((field, index) => {
const arrow = findColumnInRow(index + 1, 1).find('.right-arrow');
expect(arrow.exists()).toBe(true);
});
});
it('renders mapping dropdown for each field', () => {
- gitlabFields.forEach(({ compatibleTypes }, index) => {
+ alertFields.forEach(({ types }, index) => {
const dropdown = findColumnInRow(index + 1, 2).find(GlDropdown);
- const searchBox = dropdown.find(GlSearchBoxByType);
- const dropdownItems = dropdown.findAll(GlDropdownItem);
+ const searchBox = dropdown.findComponent(GlSearchBoxByType);
+ const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
- const numberOfMappingOptions = nodes.filter(({ type }) =>
- type.some((t) => compatibleTypes.includes(t)),
- );
+ const mappingOptions = nodes.filter(({ type }) => types.includes(type));
expect(dropdown.exists()).toBe(true);
expect(searchBox.exists()).toBe(true);
- expect(dropdownItems).toHaveLength(numberOfMappingOptions.length);
+ expect(dropdownItems).toHaveLength(mappingOptions.length);
});
});
it('renders fallback dropdown only for the fields that have fallback', () => {
- gitlabFields.forEach(({ compatibleTypes, numberOfFallbacks }, index) => {
+ alertFields.forEach(({ types, numberOfFallbacks }, index) => {
const dropdown = findColumnInRow(index + 1, 3).find(GlDropdown);
expect(dropdown.exists()).toBe(Boolean(numberOfFallbacks));
if (numberOfFallbacks) {
- const searchBox = dropdown.find(GlSearchBoxByType);
- const dropdownItems = dropdown.findAll(GlDropdownItem);
+ const searchBox = dropdown.findComponent(GlSearchBoxByType);
+ const dropdownItems = dropdown.findAllComponents(GlDropdownItem);
const { nodes } = parsedMapping.samplePayload.payloadAlerFields;
- const numberOfMappingOptions = nodes.filter(({ type }) =>
- type.some((t) => compatibleTypes.includes(t)),
- );
+ const mappingOptions = nodes.filter(({ type }) => types.includes(type));
expect(searchBox.exists()).toBe(Boolean(numberOfFallbacks));
- expect(dropdownItems).toHaveLength(numberOfMappingOptions.length);
+ expect(dropdownItems).toHaveLength(mappingOptions.length);
}
});
});
+
+ it('emits event with selected mapping', () => {
+ const mappingToSave = { fieldName: 'TITLE', mapping: 'PARSED_TITLE' };
+ jest.spyOn(transformationUtils, 'transformForSave').mockReturnValue(mappingToSave);
+ const dropdown = findColumnInRow(1, 2).find(GlDropdown);
+ const option = dropdown.find(GlDropdownItem);
+ option.vm.$emit('click');
+ expect(wrapper.emitted('onMappingUpdate')[0]).toEqual([mappingToSave]);
+ });
});
diff --git a/spec/frontend/alerts_settings/alerts_integrations_list_spec.js b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
index 5a3874d055b..5a3874d055b 100644
--- a/spec/frontend/alerts_settings/alerts_integrations_list_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_integrations_list_spec.js
diff --git a/spec/frontend/alerts_settings/alerts_settings_form_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
index 21cdec6f94c..b43c1318a86 100644
--- a/spec/frontend/alerts_settings/alerts_settings_form_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_form_spec.js
@@ -9,10 +9,12 @@ import {
} from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
-import { defaultAlertSettingsConfig } from './util';
+import MappingBuilder from '~/alerts_settings/components/alert_mapping_builder.vue';
import { typeSet } from '~/alerts_settings/constants';
+import alertFields from '../mocks/alertFields.json';
+import { defaultAlertSettingsConfig } from './util';
-describe('AlertsSettingsFormNew', () => {
+describe('AlertsSettingsForm', () => {
let wrapper;
const mockToastShow = jest.fn();
@@ -20,6 +22,7 @@ describe('AlertsSettingsFormNew', () => {
data = {},
props = {},
multipleHttpIntegrationsCustomMapping = false,
+ multiIntegrations = true,
} = {}) => {
wrapper = mount(AlertsSettingsForm, {
data() {
@@ -31,8 +34,9 @@ describe('AlertsSettingsFormNew', () => {
...props,
},
provide: {
- glFeatures: { multipleHttpIntegrationsCustomMapping },
...defaultAlertSettingsConfig,
+ glFeatures: { multipleHttpIntegrationsCustomMapping },
+ multiIntegrations,
},
mocks: {
$toast: {
@@ -49,6 +53,7 @@ describe('AlertsSettingsFormNew', () => {
const findFormToggle = () => wrapper.find(GlToggle);
const findTestPayloadSection = () => wrapper.find(`[id = "test-integration"]`);
const findMappingBuilderSection = () => wrapper.find(`[id = "mapping-builder"]`);
+ const findMappingBuilder = () => wrapper.findComponent(MappingBuilder);
const findSubmitButton = () => wrapper.find(`[type = "submit"]`);
const findMultiSupportText = () =>
wrapper.find(`[data-testid="multi-integrations-not-supported"]`);
@@ -63,13 +68,23 @@ describe('AlertsSettingsFormNew', () => {
}
});
+ const selectOptionAtIndex = async (index) => {
+ const options = findSelect().findAll('option');
+ await options.at(index).setSelected();
+ };
+
+ const enableIntegration = (index, value) => {
+ findFormFields().at(index).setValue(value);
+ findFormToggle().trigger('click');
+ };
+
describe('with default values', () => {
beforeEach(() => {
createComponent();
});
it('renders the initial template', () => {
- expect(wrapper.html()).toMatchSnapshot();
+ expect(wrapper.element).toMatchSnapshot();
});
it('render the initial form with only an integration type dropdown', () => {
@@ -80,10 +95,7 @@ describe('AlertsSettingsFormNew', () => {
});
it('shows the rest of the form when the dropdown is used', async () => {
- const options = findSelect().findAll('option');
- await options.at(1).setSelected();
-
- await wrapper.vm.$nextTick();
+ await selectOptionAtIndex(1);
expect(findFormFields().at(0).isVisible()).toBe(true);
});
@@ -96,120 +108,132 @@ describe('AlertsSettingsFormNew', () => {
it('disabled the name input when the selected value is prometheus', async () => {
createComponent();
- const options = findSelect().findAll('option');
- await options.at(2).setSelected();
+ await selectOptionAtIndex(2);
expect(findFormFields().at(0).attributes('disabled')).toBe('disabled');
});
});
describe('submitting integration form', () => {
- it('allows for create-new-integration with the correct form values for HTTP', async () => {
- createComponent();
+ describe('HTTP', () => {
+ it('create', async () => {
+ createComponent();
- const options = findSelect().findAll('option');
- await options.at(1).setSelected();
+ const integrationName = 'Test integration';
+ await selectOptionAtIndex(1);
+ enableIntegration(0, integrationName);
- await findFormFields().at(0).setValue('Test integration');
- await findFormToggle().trigger('click');
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
- await wrapper.vm.$nextTick();
+ findForm().trigger('submit');
- expect(findSubmitButton().exists()).toBe(true);
- expect(findSubmitButton().text()).toBe('Save integration');
-
- findForm().trigger('submit');
-
- await wrapper.vm.$nextTick();
-
- expect(wrapper.emitted('create-new-integration')).toBeTruthy();
- expect(wrapper.emitted('create-new-integration')[0]).toEqual([
- { type: typeSet.http, variables: { name: 'Test integration', active: true } },
- ]);
- });
+ expect(wrapper.emitted('create-new-integration')[0]).toEqual([
+ { type: typeSet.http, variables: { name: integrationName, active: true } },
+ ]);
+ });
- it('allows for create-new-integration with the correct form values for PROMETHEUS', async () => {
- createComponent();
+ it('create with custom mapping', async () => {
+ createComponent({
+ multipleHttpIntegrationsCustomMapping: true,
+ multiIntegrations: true,
+ props: { alertFields },
+ });
- const options = findSelect().findAll('option');
- await options.at(2).setSelected();
+ const integrationName = 'Test integration';
+ await selectOptionAtIndex(1);
- await findFormFields().at(0).setValue('Test integration');
- await findFormFields().at(1).setValue('https://test.com');
- await findFormToggle().trigger('click');
+ enableIntegration(0, integrationName);
- await wrapper.vm.$nextTick();
+ const sampleMapping = { field: 'test' };
+ findMappingBuilder().vm.$emit('onMappingUpdate', sampleMapping);
+ findForm().trigger('submit');
- expect(findSubmitButton().exists()).toBe(true);
- expect(findSubmitButton().text()).toBe('Save integration');
+ expect(wrapper.emitted('create-new-integration')[0]).toEqual([
+ {
+ type: typeSet.http,
+ variables: {
+ name: integrationName,
+ active: true,
+ payloadAttributeMappings: sampleMapping,
+ payloadExample: null,
+ },
+ },
+ ]);
+ });
- findForm().trigger('submit');
+ it('update', () => {
+ createComponent({
+ data: {
+ selectedIntegration: typeSet.http,
+ currentIntegration: { id: '1', name: 'Test integration pre' },
+ },
+ props: {
+ loading: false,
+ },
+ });
+ const updatedIntegrationName = 'Test integration post';
+ enableIntegration(0, updatedIntegrationName);
- await wrapper.vm.$nextTick();
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
- expect(wrapper.emitted('create-new-integration')).toBeTruthy();
- expect(wrapper.emitted('create-new-integration')[0]).toEqual([
- { type: typeSet.prometheus, variables: { apiUrl: 'https://test.com', active: true } },
- ]);
- });
+ findForm().trigger('submit');
- it('allows for update-integration with the correct form values for HTTP', async () => {
- createComponent({
- data: {
- selectedIntegration: typeSet.http,
- currentIntegration: { id: '1', name: 'Test integration pre' },
- },
- props: {
- loading: false,
- },
+ expect(wrapper.emitted('update-integration')[0]).toEqual([
+ { type: typeSet.http, variables: { name: updatedIntegrationName, active: true } },
+ ]);
});
+ });
- await findFormFields().at(0).setValue('Test integration post');
- await findFormToggle().trigger('click');
+ describe('PROMETHEUS', () => {
+ it('create', async () => {
+ createComponent();
- await wrapper.vm.$nextTick();
+ await selectOptionAtIndex(2);
- expect(findSubmitButton().exists()).toBe(true);
- expect(findSubmitButton().text()).toBe('Save integration');
+ const apiUrl = 'https://test.com';
+ enableIntegration(1, apiUrl);
- findForm().trigger('submit');
+ findFormToggle().trigger('click');
- await wrapper.vm.$nextTick();
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
- expect(wrapper.emitted('update-integration')).toBeTruthy();
- expect(wrapper.emitted('update-integration')[0]).toEqual([
- { type: typeSet.http, variables: { name: 'Test integration post', active: true } },
- ]);
- });
+ findForm().trigger('submit');
- it('allows for update-integration with the correct form values for PROMETHEUS', async () => {
- createComponent({
- data: {
- selectedIntegration: typeSet.prometheus,
- currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' },
- },
- props: {
- loading: false,
- },
+ expect(wrapper.emitted('create-new-integration')[0]).toEqual([
+ { type: typeSet.prometheus, variables: { apiUrl, active: true } },
+ ]);
});
- await findFormFields().at(0).setValue('Test integration');
- await findFormFields().at(1).setValue('https://test-post.com');
- await findFormToggle().trigger('click');
-
- await wrapper.vm.$nextTick();
+ it('update', () => {
+ createComponent({
+ data: {
+ selectedIntegration: typeSet.prometheus,
+ currentIntegration: { id: '1', apiUrl: 'https://test-pre.com' },
+ },
+ props: {
+ loading: false,
+ },
+ });
- expect(findSubmitButton().exists()).toBe(true);
- expect(findSubmitButton().text()).toBe('Save integration');
+ const apiUrl = 'https://test-post.com';
+ enableIntegration(1, apiUrl);
- findForm().trigger('submit');
+ const submitBtn = findSubmitButton();
+ expect(submitBtn.exists()).toBe(true);
+ expect(submitBtn.text()).toBe('Save integration');
- await wrapper.vm.$nextTick();
+ findForm().trigger('submit');
- expect(wrapper.emitted('update-integration')).toBeTruthy();
- expect(wrapper.emitted('update-integration')[0]).toEqual([
- { type: typeSet.prometheus, variables: { apiUrl: 'https://test-post.com', active: true } },
- ]);
+ expect(wrapper.emitted('update-integration')[0]).toEqual([
+ { type: typeSet.prometheus, variables: { apiUrl, active: true } },
+ ]);
+ });
});
});
@@ -234,9 +258,10 @@ describe('AlertsSettingsFormNew', () => {
jest.runAllTimers();
await wrapper.vm.$nextTick();
- expect(findJsonTestSubmit().exists()).toBe(true);
- expect(findJsonTestSubmit().text()).toBe('Save and test payload');
- expect(findJsonTestSubmit().props('disabled')).toBe(true);
+ const jsonTestSubmit = findJsonTestSubmit();
+ expect(jsonTestSubmit.exists()).toBe(true);
+ expect(jsonTestSubmit.text()).toBe('Save and test payload');
+ expect(jsonTestSubmit.props('disabled')).toBe(true);
});
it('should allow for the form to be automatically saved if the test payload is successfully submitted', async () => {
@@ -257,6 +282,7 @@ describe('AlertsSettingsFormNew', () => {
currentIntegration: {
type: typeSet.http,
},
+ alertFields,
},
});
});
@@ -329,21 +355,29 @@ describe('AlertsSettingsFormNew', () => {
describe('Mapping builder section', () => {
describe.each`
- featureFlag | integrationOption | visible
- ${true} | ${1} | ${true}
- ${true} | ${2} | ${false}
- ${false} | ${1} | ${false}
- ${false} | ${2} | ${false}
- `('', ({ featureFlag, integrationOption, visible }) => {
+ alertFieldsProvided | multiIntegrations | featureFlag | integrationOption | visible
+ ${true} | ${true} | ${true} | ${1} | ${true}
+ ${true} | ${true} | ${true} | ${2} | ${false}
+ ${true} | ${true} | ${false} | ${1} | ${false}
+ ${true} | ${true} | ${false} | ${2} | ${false}
+ ${true} | ${false} | ${true} | ${1} | ${false}
+ ${false} | ${true} | ${true} | ${1} | ${false}
+ `('', ({ alertFieldsProvided, multiIntegrations, featureFlag, integrationOption, visible }) => {
const visibleMsg = visible ? 'is rendered' : 'is not rendered';
const featureFlagMsg = featureFlag ? 'is enabled' : 'is disabled';
+ const alertFieldsMsg = alertFieldsProvided ? 'are provided' : 'are not provided';
const integrationType = integrationOption === 1 ? typeSet.http : typeSet.prometheus;
- it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType}`, async () => {
- createComponent({ multipleHttpIntegrationsCustomMapping: featureFlag });
- const options = findSelect().findAll('option');
- options.at(integrationOption).setSelected();
- await wrapper.vm.$nextTick();
+ it(`${visibleMsg} when multipleHttpIntegrationsCustomMapping feature flag ${featureFlagMsg} and integration type is ${integrationType} and alert fields ${alertFieldsMsg}`, async () => {
+ createComponent({
+ multipleHttpIntegrationsCustomMapping: featureFlag,
+ multiIntegrations,
+ props: {
+ alertFields: alertFieldsProvided ? alertFields : [],
+ },
+ });
+ await selectOptionAtIndex(integrationOption);
+
expect(findMappingBuilderSection().exists()).toBe(visible);
});
});
diff --git a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
index 4d0732ca76c..79a7cd93f01 100644
--- a/spec/frontend/alerts_settings/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -1,10 +1,10 @@
import VueApollo from 'vue-apollo';
import { mount, createLocalVue } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
+import { GlLoadingIcon } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { useMockIntersectionObserver } from 'helpers/mock_dom_observer';
-import { GlLoadingIcon } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
import AlertsSettingsWrapper from '~/alerts_settings/components/alerts_settings_wrapper.vue';
import AlertsSettingsForm from '~/alerts_settings/components/alerts_settings_form.vue';
diff --git a/spec/frontend/alerts_settings/mocks/apollo_mock.js b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
index e0eba1e8421..e0eba1e8421 100644
--- a/spec/frontend/alerts_settings/mocks/apollo_mock.js
+++ b/spec/frontend/alerts_settings/components/mocks/apollo_mock.js
diff --git a/spec/frontend/alerts_settings/mocks/integrations.json b/spec/frontend/alerts_settings/components/mocks/integrations.json
index b1284fc55a2..b1284fc55a2 100644
--- a/spec/frontend/alerts_settings/mocks/integrations.json
+++ b/spec/frontend/alerts_settings/components/mocks/integrations.json
diff --git a/spec/frontend/alerts_settings/util.js b/spec/frontend/alerts_settings/components/util.js
index 5c07f22f1c9..5c07f22f1c9 100644
--- a/spec/frontend/alerts_settings/util.js
+++ b/spec/frontend/alerts_settings/components/util.js
diff --git a/spec/frontend/alerts_settings/mocks/alertFields.json b/spec/frontend/alerts_settings/mocks/alertFields.json
new file mode 100644
index 00000000000..ffe59dd0c05
--- /dev/null
+++ b/spec/frontend/alerts_settings/mocks/alertFields.json
@@ -0,0 +1,123 @@
+[
+ {
+ "name": "title",
+ "label": "Title",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ],
+ "numberOfFallbacks": 1
+ },
+ {
+ "name": "description",
+ "label": "Description",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "start_time",
+ "label": "Start time",
+ "type": [
+ "datetime"
+ ],
+ "types": [
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "end_time",
+ "label": "End time",
+ "type": [
+ "datetime"
+ ],
+ "types": [
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "service",
+ "label": "Service",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "monitoring_tool",
+ "label": "Monitoring tool",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "hosts",
+ "label": "Hosts",
+ "type": [
+ "string",
+ "ARRAY"
+ ],
+ "types": [
+ "string",
+ "ARRAY",
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "severity",
+ "label": "Severity",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "fingerprint",
+ "label": "Fingerprint",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ]
+ },
+ {
+ "name": "gitlab_environment_name",
+ "label": "Environment",
+ "type": [
+ "string"
+ ],
+ "types": [
+ "string",
+ "number",
+ "datetime"
+ ]
+ }
+]
diff --git a/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js
new file mode 100644
index 00000000000..f8d2a7aa23b
--- /dev/null
+++ b/spec/frontend/alerts_settings/utils/mapping_transformations_spec.js
@@ -0,0 +1,81 @@
+import {
+ getMappingData,
+ getPayloadFields,
+ transformForSave,
+} from '~/alerts_settings/utils/mapping_transformations';
+import parsedMapping from '~/alerts_settings/components/mocks/parsedMapping.json';
+import alertFields from '../mocks/alertFields.json';
+
+describe('Mapping Transformation Utilities', () => {
+ const nameField = {
+ label: 'Name',
+ path: ['alert', 'name'],
+ type: 'string',
+ };
+ const dashboardField = {
+ label: 'Dashboard Id',
+ path: ['alert', 'dashboardId'],
+ type: 'string',
+ };
+
+ describe('getMappingData', () => {
+ it('should return mapping data', () => {
+ const result = getMappingData(
+ alertFields,
+ getPayloadFields(parsedMapping.samplePayload.payloadAlerFields.nodes.slice(0, 3)),
+ parsedMapping.storedMapping.nodes.slice(0, 3),
+ );
+
+ result.forEach((data, index) => {
+ expect(data).toEqual(
+ expect.objectContaining({
+ ...alertFields[index],
+ searchTerm: '',
+ fallbackSearchTerm: '',
+ }),
+ );
+ });
+ });
+ });
+
+ describe('transformForSave', () => {
+ it('should transform mapped data for save', () => {
+ const fieldName = 'title';
+ const mockMappingData = [
+ {
+ name: fieldName,
+ mapping: 'alert_name',
+ mappingFields: getPayloadFields([dashboardField, nameField]),
+ },
+ ];
+ const result = transformForSave(mockMappingData);
+ const { path, type, label } = nameField;
+ expect(result).toEqual([
+ { fieldName: fieldName.toUpperCase(), path, type: type.toUpperCase(), label },
+ ]);
+ });
+
+ it('should return empty array if no mapping provided', () => {
+ const fieldName = 'title';
+ const mockMappingData = [
+ {
+ name: fieldName,
+ mapping: null,
+ mappingFields: getPayloadFields([nameField, dashboardField]),
+ },
+ ];
+ const result = transformForSave(mockMappingData);
+ expect(result).toEqual([]);
+ });
+ });
+
+ describe('getPayloadFields', () => {
+ it('should add name field to each payload field', () => {
+ const result = getPayloadFields([nameField, dashboardField]);
+ expect(result).toEqual([
+ { ...nameField, name: 'alert_name' },
+ { ...dashboardField, name: 'alert_dashboardId' },
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/api/api_utils_spec.js b/spec/frontend/api/api_utils_spec.js
index 3fec26f0149..04be442ef71 100644
--- a/spec/frontend/api/api_utils_spec.js
+++ b/spec/frontend/api/api_utils_spec.js
@@ -20,6 +20,10 @@ describe('~/api/api_utils.js', () => {
);
});
+ it('ensures the URL is prefixed with a /', () => {
+ expect(apiUtils.buildApiUrl('api/:version/projects/:id')).toEqual('/api/v7/projects/:id');
+ });
+
describe('when gon includes a relative_url_root property', () => {
beforeEach(() => {
window.gon.relative_url_root = '/relative/root';
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 76d67195499..6bfc4a7b7c8 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -260,6 +260,28 @@ describe('Api', () => {
});
});
+ describe('groupLabels', () => {
+ it('fetches group labels', (done) => {
+ const options = { params: { search: 'foo' } };
+ const expectedGroup = 'gitlab-org';
+ const expectedUrl = `${dummyUrlRoot}/groups/${expectedGroup}/-/labels`;
+ mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ {
+ id: 1,
+ title: 'Foo Label',
+ },
+ ]);
+
+ Api.groupLabels(expectedGroup, options)
+ .then((res) => {
+ expect(res.length).toBe(1);
+ expect(res[0].title).toBe('Foo Label');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
describe('namespaces', () => {
it('fetches namespaces', (done) => {
const query = 'dummy query';
diff --git a/spec/frontend/behaviors/autosize_spec.js b/spec/frontend/behaviors/autosize_spec.js
index 352bd8a0ed0..a9dbee7fd08 100644
--- a/spec/frontend/behaviors/autosize_spec.js
+++ b/spec/frontend/behaviors/autosize_spec.js
@@ -1,12 +1,20 @@
import '~/behaviors/autosize';
-function load() {
- document.dispatchEvent(new Event('DOMContentLoaded'));
-}
-
jest.mock('~/helpers/startup_css_helper', () => {
return {
- waitForCSSLoaded: jest.fn().mockImplementation((cb) => cb.apply()),
+ waitForCSSLoaded: jest.fn().mockImplementation((cb) => {
+ // This is a hack:
+ // autosize.js will execute and modify the DOM
+ // whenever waitForCSSLoaded calls its callback function.
+ // This setTimeout is here because everything within setTimeout will be queued
+ // as async code until the current call stack is executed.
+ // If we would not do this, the mock for waitForCSSLoaded would call its callback
+ // before the fixture in the beforeEach is set and the Test would fail.
+ // more on this here: https://johnresig.com/blog/how-javascript-timers-work/
+ setTimeout(() => {
+ cb.apply();
+ }, 0);
+ }),
};
});
@@ -16,9 +24,15 @@ describe('Autosize behavior', () => {
});
it('is applied to the textarea', () => {
- load();
-
- const textarea = document.querySelector('textarea');
- expect(textarea.classList).toContain('js-autosize-initialized');
+ // This is the second part of the Hack:
+ // Because we are forcing the mock for WaitForCSSLoaded and the very end of our callstack
+ // to call its callback. This querySelector needs to go to the very end of our callstack
+ // as well, if we would not have this setTimeout Function here, the querySelector
+ // would run before the mockImplementation called its callBack Function
+ // the DOM Manipulation didn't happen yet and the test would fail.
+ setTimeout(() => {
+ const textarea = document.querySelector('textarea');
+ expect(textarea.classList).toContain('js-autosize-initialized');
+ }, 0);
});
});
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 b54efb93bc9..31fb6addcac 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="gl-display-none gl-display-sm-flex"
+ class="gl-display-none gl-sm-display-flex"
>
<viewer-switcher-stub
value="simple"
diff --git a/spec/frontend/blob/components/blob_content_spec.js b/spec/frontend/blob/components/blob_content_spec.js
index 3db95e5ad3f..44e2742745f 100644
--- a/spec/frontend/blob/components/blob_content_spec.js
+++ b/spec/frontend/blob/components/blob_content_spec.js
@@ -7,6 +7,7 @@ import {
BLOB_RENDER_EVENT_SHOW_SOURCE,
BLOB_RENDER_ERRORS,
} from '~/blob/components/constants';
+import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
import {
Blob,
RichViewerMock,
@@ -14,7 +15,6 @@ import {
RichBlobContentMock,
SimpleBlobContentMock,
} from './mock_data';
-import { RichViewer, SimpleViewer } from '~/vue_shared/components/blob_viewers';
describe('Blob Content component', () => {
let wrapper;
diff --git a/spec/frontend/blob/components/blob_header_filepath_spec.js b/spec/frontend/blob/components/blob_header_filepath_spec.js
index 7b8b5050486..ed2d214c307 100644
--- a/spec/frontend/blob/components/blob_header_filepath_spec.js
+++ b/spec/frontend/blob/components/blob_header_filepath_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import BlobHeaderFilepath from '~/blob/components/blob_header_filepath.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import { Blob as MockBlob } from './mock_data';
import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { Blob as MockBlob } from './mock_data';
jest.mock('~/lib/utils/number_utils', () => ({
numberToHumanSize: jest.fn(() => 'a lot'),
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 e55b8e4af24..c3e32f24d7f 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,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import { GlButton } from '@gitlab/ui';
+import { mockTracking, unmockTracking, triggerEvent } from 'helpers/tracking_helper';
import Popover from '~/blob/suggest_gitlab_ci_yml/components/popover.vue';
import * as utils from '~/lib/utils/common_utils';
diff --git a/spec/frontend/boards/board_list_deprecated_spec.js b/spec/frontend/boards/board_list_deprecated_spec.js
index 393d7f954b1..d1c9d648c96 100644
--- a/spec/frontend/boards/board_list_deprecated_spec.js
+++ b/spec/frontend/boards/board_list_deprecated_spec.js
@@ -9,9 +9,9 @@ import eventHub from '~/boards/eventhub';
import BoardList from '~/boards/components/board_list_deprecated.vue';
import '~/boards/models/issue';
import '~/boards/models/list';
-import { listObj, boardsMockInterceptor } from './mock_data';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
+import { listObj, boardsMockInterceptor } from './mock_data';
const createComponent = ({ done, listIssueProps = {}, componentProps = {}, listProps = {} }) => {
const el = document.createElement('div');
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index f82b1f7ed5c..7f0bad030fe 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -9,9 +9,9 @@ import BoardList from '~/boards/components/board_list_deprecated.vue';
import '~/boards/models/issue';
import '~/boards/models/list';
-import { listObj, boardsMockInterceptor } from './mock_data';
import store from '~/boards/stores';
import boardsStore from '~/boards/stores/boards_store';
+import { listObj, boardsMockInterceptor } from './mock_data';
window.Sortable = Sortable;
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 1b62f25044e..05f4479959e 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -1,11 +1,11 @@
import Vuex from 'vuex';
-import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import { createLocalVue, mount } from '@vue/test-utils';
+import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import eventHub from '~/boards/eventhub';
import BoardList from '~/boards/components/board_list.vue';
import BoardCard from '~/boards/components/board_card.vue';
-import { mockList, mockIssuesByListId, issues, mockIssues } from './mock_data';
import defaultState from '~/boards/stores/state';
+import { mockList, mockIssuesByListId, issues, mockIssues } from './mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/boards/boards_store_spec.js b/spec/frontend/boards/boards_store_spec.js
index f1d249ff069..b7a18c6c0ce 100644
--- a/spec/frontend/boards/boards_store_spec.js
+++ b/spec/frontend/boards/boards_store_spec.js
@@ -3,10 +3,10 @@ import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import boardsStore from '~/boards/stores/boards_store';
import eventHub from '~/boards/eventhub';
-import { listObj, listObjDuplicate } from './mock_data';
import ListIssue from '~/boards/models/issue';
import List from '~/boards/models/list';
+import { listObj, listObjDuplicate } from './mock_data';
jest.mock('js-cookie');
diff --git a/spec/frontend/boards/boards_util_spec.js b/spec/frontend/boards/boards_util_spec.js
new file mode 100644
index 00000000000..0feb1411003
--- /dev/null
+++ b/spec/frontend/boards/boards_util_spec.js
@@ -0,0 +1,17 @@
+import { transformNotFilters } from '~/boards/boards_util';
+
+describe('transformNotFilters', () => {
+ const filters = {
+ 'not[labelName]': ['label'],
+ 'not[assigneeUsername]': 'assignee',
+ };
+
+ it('formats not filters, transforms epicId to fullEpicId', () => {
+ const result = transformNotFilters(filters);
+
+ expect(result).toEqual({
+ labelName: ['label'],
+ assigneeUsername: 'assignee',
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/board_assignee_dropdown_spec.js b/spec/frontend/boards/components/board_assignee_dropdown_spec.js
index e52c14f9783..77d48c42d4b 100644
--- a/spec/frontend/boards/components/board_assignee_dropdown_spec.js
+++ b/spec/frontend/boards/components/board_assignee_dropdown_spec.js
@@ -6,8 +6,8 @@ import {
GlSearchBoxByType,
GlLoadingIcon,
} from '@gitlab/ui';
-import createMockApollo from 'helpers/mock_apollo_helper';
import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import BoardAssigneeDropdown from '~/boards/components/board_assignee_dropdown.vue';
import IssuableAssignees from '~/sidebar/components/assignees/issuable_assignees.vue';
import MultiSelectDropdown from '~/vue_shared/components/sidebar/multiselect_dropdown.vue';
@@ -38,7 +38,7 @@ describe('BoardCardAssigneeDropdown', () => {
return {
search,
selected: [],
- participants,
+ issueParticipants: participants,
};
},
store,
@@ -49,7 +49,7 @@ describe('BoardCardAssigneeDropdown', () => {
mocks: {
$apollo: {
queries: {
- participants: {
+ searchUsers: {
loading,
},
},
@@ -70,7 +70,6 @@ describe('BoardCardAssigneeDropdown', () => {
return {
search,
selected: [],
- participants,
};
},
store,
@@ -256,17 +255,15 @@ describe('BoardCardAssigneeDropdown', () => {
},
);
- describe('when participants is loading', () => {
- beforeEach(() => {
- createComponent('', true);
- });
-
+ describe('when searching users is loading', () => {
it('finds a loading icon in the dropdown', () => {
+ createComponent('test', true);
+
expect(findLoadingIcon().exists()).toBe(true);
});
});
- describe('when participants is loading is false', () => {
+ describe('when participants loading is false', () => {
beforeEach(() => {
createComponent();
});
diff --git a/spec/frontend/boards/components/board_card_layout_deprecated_spec.js b/spec/frontend/boards/components/board_card_layout_deprecated_spec.js
new file mode 100644
index 00000000000..80f9c1a0545
--- /dev/null
+++ b/spec/frontend/boards/components/board_card_layout_deprecated_spec.js
@@ -0,0 +1,158 @@
+/* global List */
+/* global ListLabel */
+
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } 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 '~/boards/models/label';
+import '~/boards/models/assignee';
+import '~/boards/models/list';
+import boardsVuexStore from '~/boards/stores';
+import boardsStore from '~/boards/stores/boards_store';
+import BoardCardLayout from '~/boards/components/board_card_layout_deprecated.vue';
+import issueCardInner from '~/boards/components/issue_card_inner.vue';
+import { ISSUABLE } from '~/boards/constants';
+import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
+
+describe('Board card layout', () => {
+ let wrapper;
+ let mock;
+ let list;
+ let store;
+
+ const localVue = createLocalVue();
+ localVue.use(Vuex);
+
+ const createStore = ({ getters = {}, actions = {} } = {}) => {
+ store = new Vuex.Store({
+ ...boardsVuexStore,
+ actions,
+ getters,
+ });
+ };
+
+ // this particular mount component needs to be used after the root beforeEach because it depends on list being initialized
+ const mountComponent = ({ propsData = {}, provide = {} } = {}) => {
+ wrapper = shallowMount(BoardCardLayout, {
+ localVue,
+ stubs: {
+ issueCardInner,
+ },
+ store,
+ propsData: {
+ list,
+ issue: list.issues[0],
+ disabled: false,
+ index: 0,
+ ...propsData,
+ },
+ provide: {
+ groupId: null,
+ rootPath: '/',
+ scopedLabelsAvailable: false,
+ ...provide,
+ },
+ });
+ };
+
+ const setupData = () => {
+ list = new List(listObj);
+ boardsStore.create();
+ boardsStore.detail.issue = {};
+ const label1 = new ListLabel({
+ id: 3,
+ title: 'testing 123',
+ color: '#000cff',
+ text_color: 'white',
+ description: 'test',
+ });
+ return waitForPromises().then(() => {
+ list.issues[0].labels.push(label1);
+ });
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ mock.onAny().reply(boardsMockInterceptor);
+ setMockEndpoints();
+ return setupData();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ list = null;
+ mock.restore();
+ });
+
+ describe('mouse events', () => {
+ it('sets showDetail to true on mousedown', async () => {
+ createStore();
+ mountComponent();
+
+ wrapper.trigger('mousedown');
+ await wrapper.vm.$nextTick();
+
+ expect(wrapper.vm.showDetail).toBe(true);
+ });
+
+ it('sets showDetail to false on mousemove', async () => {
+ createStore();
+ mountComponent();
+ wrapper.trigger('mousedown');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.showDetail).toBe(true);
+ wrapper.trigger('mousemove');
+ await wrapper.vm.$nextTick();
+ expect(wrapper.vm.showDetail).toBe(false);
+ });
+
+ it("calls 'setActiveId' when 'graphqlBoardLists' feature flag is turned on", async () => {
+ const setActiveId = jest.fn();
+ createStore({
+ actions: {
+ setActiveId,
+ },
+ });
+ mountComponent({
+ provide: {
+ glFeatures: { graphqlBoardLists: true },
+ },
+ });
+
+ wrapper.trigger('mouseup');
+ await wrapper.vm.$nextTick();
+
+ expect(setActiveId).toHaveBeenCalledTimes(1);
+ expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
+ id: list.issues[0].id,
+ sidebarType: ISSUABLE,
+ });
+ });
+
+ it("calls 'setActiveId' when epic swimlanes is active", async () => {
+ const setActiveId = jest.fn();
+ const isSwimlanesOn = () => true;
+ createStore({
+ getters: { isSwimlanesOn },
+ actions: {
+ setActiveId,
+ },
+ });
+ mountComponent();
+
+ wrapper.trigger('mouseup');
+ await wrapper.vm.$nextTick();
+
+ expect(setActiveId).toHaveBeenCalledTimes(1);
+ expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
+ id: list.issues[0].id,
+ sidebarType: ISSUABLE,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/boards/components/board_card_layout_spec.js b/spec/frontend/boards/components/board_card_layout_spec.js
index d8633871e8d..3ce3d0100f8 100644
--- a/spec/frontend/boards/components/board_card_layout_spec.js
+++ b/spec/frontend/boards/components/board_card_layout_spec.js
@@ -1,28 +1,14 @@
-/* global List */
-/* global ListLabel */
-
import Vuex from 'vuex';
import { createLocalVue, shallowMount } 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 '~/boards/models/label';
-import '~/boards/models/assignee';
-import '~/boards/models/list';
-import boardsVuexStore from '~/boards/stores';
-import boardsStore from '~/boards/stores/boards_store';
+import defaultState from '~/boards/stores/state';
import BoardCardLayout from '~/boards/components/board_card_layout.vue';
-import issueCardInner from '~/boards/components/issue_card_inner.vue';
-import { listObj, boardsMockInterceptor, setMockEndpoints } from '../mock_data';
-
+import IssueCardInner from '~/boards/components/issue_card_inner.vue';
import { ISSUABLE } from '~/boards/constants';
+import { mockLabelList, mockIssue } from '../mock_data';
describe('Board card layout', () => {
let wrapper;
- let mock;
- let list;
let store;
const localVue = createLocalVue();
@@ -30,7 +16,7 @@ describe('Board card layout', () => {
const createStore = ({ getters = {}, actions = {} } = {}) => {
store = new Vuex.Store({
- ...boardsVuexStore,
+ state: defaultState,
actions,
getters,
});
@@ -41,12 +27,12 @@ describe('Board card layout', () => {
wrapper = shallowMount(BoardCardLayout, {
localVue,
stubs: {
- issueCardInner,
+ IssueCardInner,
},
store,
propsData: {
- list,
- issue: list.issues[0],
+ list: mockLabelList,
+ issue: mockIssue,
disabled: false,
index: 0,
...propsData,
@@ -60,34 +46,9 @@ describe('Board card layout', () => {
});
};
- const setupData = () => {
- list = new List(listObj);
- boardsStore.create();
- boardsStore.detail.issue = {};
- const label1 = new ListLabel({
- id: 3,
- title: 'testing 123',
- color: '#000cff',
- text_color: 'white',
- description: 'test',
- });
- return waitForPromises().then(() => {
- list.issues[0].labels.push(label1);
- });
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onAny().reply(boardsMockInterceptor);
- setMockEndpoints();
- return setupData();
- });
-
afterEach(() => {
wrapper.destroy();
wrapper = null;
- list = null;
- mock.restore();
});
describe('mouse events', () => {
@@ -112,25 +73,21 @@ describe('Board card layout', () => {
expect(wrapper.vm.showDetail).toBe(false);
});
- it("calls 'setActiveId' when 'graphqlBoardLists' feature flag is turned on", async () => {
+ it("calls 'setActiveId'", async () => {
const setActiveId = jest.fn();
createStore({
actions: {
setActiveId,
},
});
- mountComponent({
- provide: {
- glFeatures: { graphqlBoardLists: true },
- },
- });
+ mountComponent();
wrapper.trigger('mouseup');
await wrapper.vm.$nextTick();
expect(setActiveId).toHaveBeenCalledTimes(1);
expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
- id: list.issues[0].id,
+ id: mockIssue.id,
sidebarType: ISSUABLE,
});
});
@@ -151,7 +108,7 @@ describe('Board card layout', () => {
expect(setActiveId).toHaveBeenCalledTimes(1);
expect(setActiveId).toHaveBeenCalledWith(expect.any(Object), {
- id: list.issues[0].id,
+ id: mockIssue.id,
sidebarType: ISSUABLE,
});
});
diff --git a/spec/frontend/boards/components/board_configuration_options_spec.js b/spec/frontend/boards/components/board_configuration_options_spec.js
index d9614c254e2..6f0971a9458 100644
--- a/spec/frontend/boards/components/board_configuration_options_spec.js
+++ b/spec/frontend/boards/components/board_configuration_options_spec.js
@@ -7,6 +7,7 @@ describe('BoardConfigurationOptions', () => {
const defaultProps = {
hideBacklogList: false,
hideClosedList: false,
+ readonly: false,
};
const createComponent = (props = {}) => {
@@ -61,4 +62,18 @@ describe('BoardConfigurationOptions', () => {
expect(wrapper.emitted('update:hideClosedList')).toEqual([[true]]);
});
+
+ it('renders checkboxes disabled when user does not have edit rights', () => {
+ createComponent({ readonly: true });
+
+ expect(closedListCheckbox().attributes('disabled')).toBe('true');
+ expect(backlogListCheckbox().attributes('disabled')).toBe('true');
+ });
+
+ it('renders checkboxes enabled when user has edit rights', () => {
+ createComponent();
+
+ expect(closedListCheckbox().attributes('disabled')).toBeUndefined();
+ expect(backlogListCheckbox().attributes('disabled')).toBeUndefined();
+ });
});
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 98be02d7dbf..ef2795030a0 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -5,8 +5,8 @@ import Draggable from 'vuedraggable';
import EpicsSwimlanes from 'ee_component/boards/components/epics_swimlanes.vue';
import getters from 'ee_else_ce/boards/stores/getters';
import BoardColumnDeprecated from '~/boards/components/board_column_deprecated.vue';
-import { mockLists, mockListsWithModel } from '../mock_data';
import BoardContent from '~/boards/components/board_content.vue';
+import { mockLists, mockListsWithModel } from '../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index c34987a55de..acff37296ce 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import { TEST_HOST } from 'helpers/test_constants';
import { GlModal } from '@gitlab/ui';
+import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { deprecatedCreateFlash as createFlash } from '~/flash';
diff --git a/spec/frontend/boards/components/board_list_header_deprecated_spec.js b/spec/frontend/boards/components/board_list_header_deprecated_spec.js
index 6207724e6a9..15e767e9d91 100644
--- a/spec/frontend/boards/components/board_list_header_deprecated_spec.js
+++ b/spec/frontend/boards/components/board_list_header_deprecated_spec.js
@@ -74,7 +74,13 @@ describe('Board List Header Component', () => {
describe('Add issue button', () => {
const hasNoAddButton = [ListType.closed];
- const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
+ const hasAddButton = [
+ ListType.backlog,
+ ListType.label,
+ ListType.milestone,
+ ListType.iteration,
+ ListType.assignee,
+ ];
it.each(hasNoAddButton)('does not render when List Type is `%s`', (listType) => {
createComponent({ listType });
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 357d05ced02..6d8c9e51d3f 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -78,7 +78,13 @@ describe('Board List Header Component', () => {
describe('Add issue button', () => {
const hasNoAddButton = [ListType.closed];
- const hasAddButton = [ListType.backlog, ListType.label, ListType.milestone, ListType.assignee];
+ const hasAddButton = [
+ ListType.backlog,
+ ListType.label,
+ ListType.milestone,
+ ListType.iteration,
+ ListType.assignee,
+ ];
it.each(hasNoAddButton)('does not render when List Type is `%s`', (listType) => {
createComponent({ listType });
@@ -167,7 +173,7 @@ describe('Board List Header Component', () => {
describe('user can drag', () => {
const cannotDragList = [ListType.backlog, ListType.closed];
- const canDragList = [ListType.label, ListType.milestone, ListType.assignee];
+ const canDragList = [ListType.label, ListType.milestone, ListType.iteration, ListType.assignee];
it.each(cannotDragList)(
'does not have user-can-drag-class so user cannot drag list',
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js
index 74d88d9f34c..e6e2befedd0 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_milestone_select_spec.js
@@ -20,7 +20,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () =>
wrapper = null;
});
- const createWrapper = ({ milestone = null } = {}) => {
+ const createWrapper = ({ milestone = null, loading = false } = {}) => {
store = createStore();
store.state.issues = { [TEST_ISSUE.id]: { ...TEST_ISSUE, milestone } };
store.state.activeId = TEST_ISSUE.id;
@@ -38,7 +38,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () =>
},
mocks: {
$apollo: {
- loading: false,
+ loading,
},
},
});
@@ -63,12 +63,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () =>
});
it('shows loader while Apollo is loading', async () => {
- createWrapper({ milestone: TEST_MILESTONE });
-
- expect(findLoader().exists()).toBe(false);
-
- wrapper.vm.$apollo.loading = true;
- await wrapper.vm.$nextTick();
+ createWrapper({ milestone: TEST_MILESTONE, loading: true });
expect(findLoader().exists()).toBe(true);
});
@@ -76,8 +71,7 @@ describe('~/boards/components/sidebar/board_sidebar_milestone_select.vue', () =>
it('shows message when error or no milestones found', async () => {
createWrapper();
- wrapper.setData({ milestones: [] });
- await wrapper.vm.$nextTick();
+ await wrapper.setData({ milestones: [] });
expect(findNoMilestonesFoundItem().text()).toBe('No milestones found');
});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
index b1df0f2d771..8e1285dcd07 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_subscription_spec.js
@@ -4,8 +4,8 @@ import { GlToggle, GlLoadingIcon } from '@gitlab/ui';
import BoardSidebarSubscription from '~/boards/components/sidebar/board_sidebar_subscription.vue';
import * as types from '~/boards/stores/mutation_types';
import { createStore } from '~/boards/stores';
-import { mockActiveIssue } from '../../mock_data';
import createFlash from '~/flash';
+import { mockActiveIssue } from '../../mock_data';
jest.mock('~/flash.js');
diff --git a/spec/frontend/boards/issue_card_deprecated_spec.js b/spec/frontend/boards/issue_card_deprecated_spec.js
index fd7b0edb97e..2fefda5158b 100644
--- a/spec/frontend/boards/issue_card_deprecated_spec.js
+++ b/spec/frontend/boards/issue_card_deprecated_spec.js
@@ -7,8 +7,8 @@ import '~/boards/models/issue';
import '~/boards/models/list';
import { GlLabel } from '@gitlab/ui';
import IssueCardInner from '~/boards/components/issue_card_inner_deprecated.vue';
-import { listObj } from './mock_data';
import store from '~/boards/stores';
+import { listObj } from './mock_data';
describe('Issue card component', () => {
const user = new ListAssignee({
diff --git a/spec/frontend/boards/issue_card_inner_spec.js b/spec/frontend/boards/issue_card_inner_spec.js
index f9ad78494af..aa8f6b826b6 100644
--- a/spec/frontend/boards/issue_card_inner_spec.js
+++ b/spec/frontend/boards/issue_card_inner_spec.js
@@ -2,10 +2,10 @@ import { mount } from '@vue/test-utils';
import { range } from 'lodash';
import { GlLabel } from '@gitlab/ui';
import IssueCardInner from '~/boards/components/issue_card_inner.vue';
-import { mockLabelList } from './mock_data';
import defaultStore from '~/boards/stores';
import eventHub from '~/boards/eventhub';
import { updateHistory } from '~/lib/utils/url_utility';
+import { mockLabelList } from './mock_data';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/boards/eventhub');
diff --git a/spec/frontend/boards/issue_spec.js b/spec/frontend/boards/issue_spec.js
index d68e17c06a7..1f354fb04db 100644
--- a/spec/frontend/boards/issue_spec.js
+++ b/spec/frontend/boards/issue_spec.js
@@ -41,7 +41,7 @@ describe('Issue model', () => {
});
expect(issue.labels.length).toBe(1);
- expect(issue.labels[0].color).toBe('red');
+ expect(issue.labels[0].color).toBe('#F0AD4E');
});
it('adds other label with same title', () => {
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index d5cfb9b7d07..6d60937f850 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -137,7 +137,7 @@ export const rawIssue = {
{
id: 1,
title: 'test',
- color: 'red',
+ color: '#F0AD4E',
description: 'testing',
},
],
@@ -165,7 +165,7 @@ export const mockIssue = {
{
id: 1,
title: 'test',
- color: 'red',
+ color: '#F0AD4E',
description: 'testing',
},
],
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index e4209cd5e55..60bcbd1ec92 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -1,16 +1,4 @@
import testAction from 'helpers/vuex_action_helper';
-import {
- mockLists,
- mockListsById,
- mockIssue,
- mockIssue2,
- rawIssue,
- mockIssues,
- mockMilestone,
- labels,
- mockActiveIssue,
- mockGroupProjects,
-} from '../mock_data';
import actions, { gqlClient } from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import { inactiveId } from '~/boards/constants';
@@ -25,6 +13,18 @@ import {
formatIssueInput,
} from '~/boards/boards_util';
import createFlash from '~/flash';
+import {
+ mockLists,
+ mockListsById,
+ mockIssue,
+ mockIssue2,
+ rawIssue,
+ mockIssues,
+ mockMilestone,
+ labels,
+ mockActiveIssue,
+ mockGroupProjects,
+} from '../mock_data';
jest.mock('~/flash');
@@ -71,7 +71,7 @@ describe('setFilters', () => {
actions.setFilters,
filters,
state,
- [{ type: types.SET_FILTERS, payload: filters }],
+ [{ type: types.SET_FILTERS, payload: { ...filters, not: {} } }],
[],
done,
);
@@ -254,6 +254,27 @@ describe('createList', () => {
});
});
+describe('fetchLabels', () => {
+ it('should commit mutation RECEIVE_LABELS_SUCCESS on success', async () => {
+ const queryResponse = {
+ data: {
+ group: {
+ labels: {
+ nodes: labels,
+ },
+ },
+ },
+ };
+ jest.spyOn(gqlClient, 'query').mockResolvedValue(queryResponse);
+
+ await testAction({
+ action: actions.fetchLabels,
+ state: { boardType: 'group' },
+ expectedMutations: [{ type: types.RECEIVE_LABELS_SUCCESS, payload: labels }],
+ });
+ });
+});
+
describe('moveList', () => {
it('should commit MOVE_LIST mutation and dispatch updateList action', (done) => {
const initialBoardListsState = {
@@ -1201,6 +1222,40 @@ describe('setSelectedProject', () => {
});
});
+describe('toggleBoardItemMultiSelection', () => {
+ const boardItem = mockIssue;
+
+ it('should commit mutation ADD_BOARD_ITEM_TO_SELECTION if item is not on selection state', () => {
+ testAction(
+ actions.toggleBoardItemMultiSelection,
+ boardItem,
+ { selectedBoardItems: [] },
+ [
+ {
+ type: types.ADD_BOARD_ITEM_TO_SELECTION,
+ payload: boardItem,
+ },
+ ],
+ [],
+ );
+ });
+
+ it('should commit mutation REMOVE_BOARD_ITEM_FROM_SELECTION if item is on selection state', () => {
+ testAction(
+ actions.toggleBoardItemMultiSelection,
+ boardItem,
+ { selectedBoardItems: [mockIssue] },
+ [
+ {
+ type: types.REMOVE_BOARD_ITEM_FROM_SELECTION,
+ payload: boardItem,
+ },
+ ],
+ [],
+ );
+ });
+});
+
describe('fetchBacklog', () => {
expectNotImplemented(actions.fetchBacklog);
});
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
index 44b41b5667d..74d597b9046 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -62,6 +62,22 @@ describe('Boards - Getters', () => {
});
});
+ describe('groupPathByIssueId', () => {
+ it('returns group path for the active issue', () => {
+ const mockActiveIssue = {
+ referencePath: 'gitlab-org/gitlab-test#1',
+ };
+ expect(getters.groupPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual(
+ 'gitlab-org',
+ );
+ });
+
+ it('returns empty string as group path when active issue is an empty object', () => {
+ const mockActiveIssue = {};
+ expect(getters.groupPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual('');
+ });
+ });
+
describe('projectPathByIssueId', () => {
it('returns project path for the active issue', () => {
const mockActiveIssue = {
@@ -72,7 +88,7 @@ describe('Boards - Getters', () => {
);
});
- it('returns empty string as project when active issue is an empty object', () => {
+ it('returns empty string as project path when active issue is an empty object', () => {
const mockActiveIssue = {};
expect(getters.projectPathForActiveIssue({}, { activeIssue: mockActiveIssue })).toEqual('');
});
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index c5fe0e22c3c..2b6548534c3 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -1,7 +1,14 @@
import mutations from '~/boards/stores/mutations';
import * as types from '~/boards/stores/mutation_types';
import defaultState from '~/boards/stores/state';
-import { mockLists, rawIssue, mockIssue, mockIssue2, mockGroupProjects } from '../mock_data';
+import {
+ mockLists,
+ rawIssue,
+ mockIssue,
+ mockIssue2,
+ mockGroupProjects,
+ labels,
+} from '../mock_data';
const expectNotImplemented = (action) => {
it('is not implemented', () => {
@@ -99,13 +106,11 @@ describe('Board Store Mutations', () => {
});
});
- describe('RECEIVE_LABELS_FAILURE', () => {
- it('sets error message', () => {
- mutations.RECEIVE_LABELS_FAILURE(state);
+ describe('RECEIVE_LABELS_SUCCESS', () => {
+ it('sets labels on state', () => {
+ mutations.RECEIVE_LABELS_SUCCESS(state, labels);
- expect(state.error).toEqual(
- 'An error occurred while fetching labels. Please reload the page.',
- );
+ expect(state.labels).toEqual(labels);
});
});
@@ -589,4 +594,27 @@ describe('Board Store Mutations', () => {
expect(state.selectedProject).toEqual(mockGroupProjects[0]);
});
});
+
+ describe('ADD_BOARD_ITEM_TO_SELECTION', () => {
+ it('Should add boardItem to selectedBoardItems state', () => {
+ expect(state.selectedBoardItems).toEqual([]);
+
+ mutations[types.ADD_BOARD_ITEM_TO_SELECTION](state, mockIssue);
+
+ expect(state.selectedBoardItems).toEqual([mockIssue]);
+ });
+ });
+
+ describe('REMOVE_BOARD_ITEM_FROM_SELECTION', () => {
+ it('Should remove boardItem to selectedBoardItems state', () => {
+ state = {
+ ...state,
+ selectedBoardItems: [mockIssue],
+ };
+
+ mutations[types.REMOVE_BOARD_ITEM_FROM_SELECTION](state, mockIssue);
+
+ expect(state.selectedBoardItems).toEqual([]);
+ });
+ });
});
diff --git a/spec/frontend/captcha/init_recaptcha_script_spec.js b/spec/frontend/captcha/init_recaptcha_script_spec.js
new file mode 100644
index 00000000000..df114821651
--- /dev/null
+++ b/spec/frontend/captcha/init_recaptcha_script_spec.js
@@ -0,0 +1,49 @@
+import {
+ RECAPTCHA_API_URL_PREFIX,
+ RECAPTCHA_ONLOAD_CALLBACK_NAME,
+ clearMemoizeCache,
+ initRecaptchaScript,
+} from '~/captcha/init_recaptcha_script';
+
+describe('initRecaptchaScript', () => {
+ afterEach(() => {
+ document.head.innerHTML = '';
+ clearMemoizeCache();
+ });
+
+ const getScriptOnload = () => window[RECAPTCHA_ONLOAD_CALLBACK_NAME];
+ const triggerScriptOnload = (...args) => window[RECAPTCHA_ONLOAD_CALLBACK_NAME](...args);
+
+ describe('when called', () => {
+ let result;
+
+ beforeEach(() => {
+ result = initRecaptchaScript();
+ });
+
+ it('adds script to head', () => {
+ expect(document.head).toMatchInlineSnapshot(`
+ <head>
+ <script
+ class="js-recaptcha-script"
+ src="${RECAPTCHA_API_URL_PREFIX}?onload=${RECAPTCHA_ONLOAD_CALLBACK_NAME}&render=explicit"
+ />
+ </head>
+ `);
+ });
+
+ it('is memoized', () => {
+ expect(initRecaptchaScript()).toBe(result);
+ expect(document.head.querySelectorAll('script').length).toBe(1);
+ });
+
+ it('when onload is triggered, resolves promise', async () => {
+ const instance = {};
+
+ triggerScriptOnload(instance);
+
+ await expect(result).resolves.toBe(instance);
+ expect(getScriptOnload()).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/frontend/ci_variable_list/store/actions_spec.js b/spec/frontend/ci_variable_list/store/actions_spec.js
index 075e5829305..03a2e37ed55 100644
--- a/spec/frontend/ci_variable_list/store/actions_spec.js
+++ b/spec/frontend/ci_variable_list/store/actions_spec.js
@@ -6,8 +6,8 @@ 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';
-import mockData from '../services/mock_data';
import { prepareDataForDisplay, prepareEnvironments } from '~/ci_variable_list/store/utils';
+import mockData from '../services/mock_data';
jest.mock('~/api.js');
jest.mock('~/flash.js');
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 ee4ec4636ea..6047b404197 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
@@ -60,8 +60,8 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
>
<svg
aria-hidden="true"
- class="gl-icon s16 gl-new-dropdown-item-check-icon"
- data-testid="mobile-issue-close-icon"
+ class="gl-icon s16 gl-new-dropdown-item-check-icon gl-mt-3 gl-align-self-start"
+ data-testid="dropdown-item-checkbox"
>
<use
href="#mobile-issue-close"
@@ -115,8 +115,8 @@ exports[`Remove cluster confirmation modal renders splitbutton with modal includ
>
<svg
aria-hidden="true"
- class="gl-icon s16 gl-new-dropdown-item-check-icon gl-visibility-hidden"
- data-testid="mobile-issue-close-icon"
+ class="gl-icon s16 gl-new-dropdown-item-check-icon gl-visibility-hidden gl-mt-3 gl-align-self-start"
+ data-testid="dropdown-item-checkbox"
>
<use
href="#mobile-issue-close"
diff --git a/spec/frontend/clusters/components/applications_spec.js b/spec/frontend/clusters/components/applications_spec.js
index cf89246c1a5..78f08430554 100644
--- a/spec/frontend/clusters/components/applications_spec.js
+++ b/spec/frontend/clusters/components/applications_spec.js
@@ -1,13 +1,13 @@
import { shallowMount, mount } from '@vue/test-utils';
import Applications from '~/clusters/components/applications.vue';
import { CLUSTER_TYPE, PROVIDER_TYPE } from '~/clusters/constants';
-import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
import eventHub from '~/clusters/event_hub';
import ApplicationRow from '~/clusters/components/application_row.vue';
import KnativeDomainEditor from '~/clusters/components/knative_domain_editor.vue';
import CrossplaneProviderStack from '~/clusters/components/crossplane_provider_stack.vue';
import IngressModsecuritySettings from '~/clusters/components/ingress_modsecurity_settings.vue';
import FluentdOutputSettings from '~/clusters/components/fluentd_output_settings.vue';
+import { APPLICATIONS_MOCK_STATE } from '../services/mock_data';
describe('Applications', () => {
let wrapper;
@@ -16,7 +16,7 @@ describe('Applications', () => {
gon.features = gon.features || {};
});
- const createApp = ({ applications, type, propsData } = {}, isShallow) => {
+ const createComponent = ({ applications, type, propsData } = {}, isShallow) => {
const mountMethod = isShallow ? shallowMount : mount;
wrapper = mountMethod(Applications, {
@@ -29,7 +29,7 @@ describe('Applications', () => {
});
};
- const createShallowApp = (options) => createApp(options, true);
+ const createShallowComponent = (options) => createComponent(options, true);
const findByTestId = (id) => wrapper.find(`[data-testid="${id}"]`);
afterEach(() => {
wrapper.destroy();
@@ -37,7 +37,7 @@ describe('Applications', () => {
describe('Project cluster applications', () => {
beforeEach(() => {
- createApp({ type: CLUSTER_TYPE.PROJECT });
+ createComponent({ type: CLUSTER_TYPE.PROJECT });
});
it('renders a row for Ingress', () => {
@@ -82,7 +82,7 @@ describe('Applications', () => {
describe('Group cluster applications', () => {
beforeEach(() => {
- createApp({ type: CLUSTER_TYPE.GROUP });
+ createComponent({ type: CLUSTER_TYPE.GROUP });
});
it('renders a row for Ingress', () => {
@@ -128,7 +128,7 @@ describe('Applications', () => {
describe('Instance cluster applications', () => {
beforeEach(() => {
- createApp({ type: CLUSTER_TYPE.INSTANCE });
+ createComponent({ type: CLUSTER_TYPE.INSTANCE });
});
it('renders a row for Ingress', () => {
@@ -174,14 +174,14 @@ describe('Applications', () => {
describe('Helm application', () => {
it('does not render a row for Helm Tiller', () => {
- createApp();
+ createComponent();
expect(wrapper.find('.js-cluster-application-row-helm').exists()).toBe(false);
});
});
describe('Ingress application', () => {
it('shows the correct warning message', () => {
- createApp();
+ createComponent();
expect(findByTestId('ingressCostWarning').element).toMatchSnapshot();
});
@@ -195,7 +195,7 @@ describe('Applications', () => {
},
};
- beforeEach(() => createShallowApp(propsData));
+ beforeEach(() => createShallowComponent(propsData));
it('renders IngressModsecuritySettings', () => {
const modsecuritySettings = wrapper.find(IngressModsecuritySettings);
@@ -206,7 +206,7 @@ describe('Applications', () => {
describe('when installed', () => {
describe('with ip address', () => {
it('renders ip address with a clipboard button', () => {
- createApp({
+ createComponent({
applications: {
ingress: {
title: 'Ingress',
@@ -225,7 +225,7 @@ describe('Applications', () => {
describe('with hostname', () => {
it('renders hostname with a clipboard button', () => {
- createApp({
+ createComponent({
applications: {
ingress: {
title: 'Ingress',
@@ -255,7 +255,7 @@ describe('Applications', () => {
describe('without ip address', () => {
it('renders an input text with a loading icon and an alert text', () => {
- createApp({
+ createComponent({
applications: {
ingress: {
title: 'Ingress',
@@ -272,7 +272,7 @@ describe('Applications', () => {
describe('before installing', () => {
it('does not render the IP address', () => {
- createApp();
+ createComponent();
expect(wrapper.text()).not.toContain('Ingress IP Address');
expect(wrapper.find('.js-endpoint').exists()).toBe(false);
@@ -282,13 +282,13 @@ describe('Applications', () => {
describe('Cert-Manager application', () => {
it('shows the correct description', () => {
- createApp();
+ createComponent();
expect(findByTestId('certManagerDescription').element).toMatchSnapshot();
});
describe('when not installed', () => {
it('renders email & allows editing', () => {
- createApp({
+ createComponent({
applications: {
cert_manager: {
title: 'Cert-Manager',
@@ -305,7 +305,7 @@ describe('Applications', () => {
describe('when installed', () => {
it('renders email in readonly', () => {
- createApp({
+ createComponent({
applications: {
cert_manager: {
title: 'Cert-Manager',
@@ -324,7 +324,7 @@ describe('Applications', () => {
describe('Jupyter application', () => {
describe('with ingress installed with ip & jupyter installable', () => {
it('renders hostname active input', () => {
- createApp({
+ createComponent({
applications: {
ingress: {
title: 'Ingress',
@@ -342,7 +342,7 @@ describe('Applications', () => {
describe('with ingress installed without external ip', () => {
it('does not render hostname input', () => {
- createApp({
+ createComponent({
applications: {
ingress: { title: 'Ingress', status: 'installed' },
},
@@ -356,7 +356,7 @@ describe('Applications', () => {
describe('with ingress & jupyter installed', () => {
it('renders readonly input', () => {
- createApp({
+ createComponent({
applications: {
ingress: {
title: 'Ingress',
@@ -375,7 +375,7 @@ describe('Applications', () => {
describe('without ingress installed', () => {
beforeEach(() => {
- createApp();
+ createComponent();
});
it('does not render input', () => {
@@ -388,7 +388,7 @@ describe('Applications', () => {
describe('Prometheus application', () => {
it('shows the correct description', () => {
- createApp();
+ createComponent();
expect(findByTestId('prometheusDescription').element).toMatchSnapshot();
});
});
@@ -414,14 +414,14 @@ describe('Applications', () => {
let knativeDomainEditor;
beforeEach(() => {
- createShallowApp(propsData);
+ createShallowComponent(propsData);
jest.spyOn(eventHub, '$emit');
knativeDomainEditor = wrapper.find(KnativeDomainEditor);
});
it('shows the correct description', async () => {
- createApp();
+ createComponent();
wrapper.setProps({
providerType: PROVIDER_TYPE.GCP,
preInstalledKnative: true,
@@ -487,7 +487,7 @@ describe('Applications', () => {
},
};
- beforeEach(() => createShallowApp(propsData));
+ beforeEach(() => createShallowComponent(propsData));
it('renders the correct Component', () => {
const crossplane = wrapper.find(CrossplaneProviderStack);
@@ -495,7 +495,7 @@ describe('Applications', () => {
});
it('shows the correct description', () => {
- createApp();
+ createComponent();
expect(findByTestId('crossplaneDescription').element).toMatchSnapshot();
});
});
@@ -503,7 +503,7 @@ describe('Applications', () => {
describe('Elastic Stack application', () => {
describe('with elastic stack installable', () => {
it('renders the install button enabled', () => {
- createApp();
+ createComponent();
expect(
wrapper
@@ -517,7 +517,7 @@ describe('Applications', () => {
describe('elastic stack installed', () => {
it('renders uninstall button', () => {
- createApp({
+ createComponent({
applications: {
elastic_stack: { title: 'Elastic Stack', status: 'installed' },
},
@@ -535,7 +535,7 @@ describe('Applications', () => {
});
describe('Fluentd application', () => {
- beforeEach(() => createShallowApp());
+ beforeEach(() => createShallowComponent());
it('renders the correct Component', () => {
expect(wrapper.find(FluentdOutputSettings).exists()).toBe(true);
@@ -544,7 +544,7 @@ describe('Applications', () => {
describe('Cilium application', () => {
it('shows the correct description', () => {
- createApp({ propsData: { ciliumHelpPath: 'cilium-help-path' } });
+ createComponent({ propsData: { ciliumHelpPath: 'cilium-help-path' } });
expect(findByTestId('ciliumDescription').element).toMatchSnapshot();
});
});
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index 6214cb50e13..60d6bb46e3c 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -5,10 +5,10 @@ import * as Sentry from '~/sentry/wrapper';
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 { apiData } from '../mock_data';
jest.mock('~/flash.js');
diff --git a/spec/frontend/clusters_list/store/mutations_spec.js b/spec/frontend/clusters_list/store/mutations_spec.js
index df0dfe587b6..2ca1c20ad56 100644
--- a/spec/frontend/clusters_list/store/mutations_spec.js
+++ b/spec/frontend/clusters_list/store/mutations_spec.js
@@ -1,7 +1,7 @@
import * as types from '~/clusters_list/store/mutation_types';
-import { apiData } from '../mock_data';
import getInitialState from '~/clusters_list/store/state';
import mutations from '~/clusters_list/store/mutations';
+import { apiData } from '../mock_data';
describe('Admin statistics panel mutations', () => {
let state;
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 35348d3a03b..fd5db1c6a29 100644
--- a/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
+++ b/spec/frontend/create_cluster/eks_cluster/store/actions_spec.js
@@ -1,6 +1,6 @@
+import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
-import MockAdapter from 'axios-mock-adapter';
import createState from '~/create_cluster/eks_cluster/store/state';
import * as actions from '~/create_cluster/eks_cluster/store/actions';
import {
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
index c09eaa63d4d..e97e0a6686d 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_machine_type_dropdown_spec.js
@@ -1,10 +1,10 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
-import { selectedMachineTypeMock, gapiMachineTypesResponseMock } from '../mock_data';
import createState from '~/create_cluster/gke_cluster/store/state';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
import GkeMachineTypeDropdown from '~/create_cluster/gke_cluster/components/gke_machine_type_dropdown.vue';
+import { selectedMachineTypeMock, gapiMachineTypesResponseMock } from '../mock_data';
const componentConfig = {
fieldId: 'cluster_provider_gcp_attributes_gcp_machine_type',
diff --git a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
index eb58108bf3c..90f96c98427 100644
--- a/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
+++ b/spec/frontend/create_cluster/gke_cluster/components/gke_project_id_dropdown_spec.js
@@ -1,10 +1,10 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import createState from '~/create_cluster/gke_cluster/store/state';
-import { selectedProjectMock, gapiProjectsResponseMock } from '../mock_data';
import GkeProjectIdDropdown from '~/create_cluster/gke_cluster/components/gke_project_id_dropdown.vue';
import DropdownButton from '~/vue_shared/components/dropdown/dropdown_button.vue';
import DropdownHiddenInput from '~/vue_shared/components/dropdown/dropdown_hidden_input.vue';
+import { selectedProjectMock, gapiProjectsResponseMock } from '../mock_data';
const componentConfig = {
docsUrl: 'https://console.cloud.google.com/home/dashboard',
diff --git a/spec/frontend/deploy_keys/components/key_spec.js b/spec/frontend/deploy_keys/components/key_spec.js
index fcb4e31dec8..f783fdd6476 100644
--- a/spec/frontend/deploy_keys/components/key_spec.js
+++ b/spec/frontend/deploy_keys/components/key_spec.js
@@ -76,7 +76,7 @@ describe('Deploy keys key', () => {
createComponent({ deployKey: { ...deployKey, deploy_keys_projects: deployKeysProjects } });
expect(wrapper.find('.deploy-project-label').attributes('title')).toBe(
- 'Write access allowed',
+ 'Grant write permissions to this key',
);
});
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 77fc70e08d1..a414eb04ab4 100644
--- a/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_discussion_spec.js
@@ -1,6 +1,5 @@
import { mount } from '@vue/test-utils';
import { GlLoadingIcon } from '@gitlab/ui';
-import notes from '../../mock_data/notes';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
import DesignNote from '~/design_management/components/design_notes/design_note.vue';
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
@@ -8,6 +7,7 @@ import createNoteMutation from '~/design_management/graphql/mutations/create_not
import toggleResolveDiscussionMutation from '~/design_management/graphql/mutations/toggle_resolve_discussion.mutation.graphql';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ToggleRepliesWidget from '~/design_management/components/design_notes/toggle_replies_widget.vue';
+import notes from '../../mock_data/notes';
import mockDiscussion from '../../mock_data/discussion';
const defaultMockDiscussion = {
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index a026cc39c84..ecb1fdbf534 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
import DesignOverlay from '~/design_management/components/design_overlay.vue';
import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
-import notes from '../mock_data/notes';
import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '~/design_management/constants';
+import notes from '../mock_data/notes';
const mutate = jest.fn(() => Promise.resolve());
diff --git a/spec/frontend/design_management/components/design_sidebar_spec.js b/spec/frontend/design_management/components/design_sidebar_spec.js
index 60266883fcd..3d520ea721a 100644
--- a/spec/frontend/design_management/components/design_sidebar_spec.js
+++ b/spec/frontend/design_management/components/design_sidebar_spec.js
@@ -4,9 +4,9 @@ import Cookies from 'js-cookie';
import DesignSidebar from '~/design_management/components/design_sidebar.vue';
import Participants from '~/sidebar/components/participants/participants.vue';
import DesignDiscussion from '~/design_management/components/design_notes/design_discussion.vue';
-import design from '../mock_data/design';
import updateActiveDiscussionMutation from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
import DesignTodoButton from '~/design_management/components/design_todo_button.vue';
+import design from '../mock_data/design';
const scrollIntoViewMock = jest.fn();
HTMLElement.prototype.scrollIntoView = scrollIntoViewMock;
diff --git a/spec/frontend/design_management/components/design_todo_button_spec.js b/spec/frontend/design_management/components/design_todo_button_spec.js
index 9ebc6ca26a2..0c7516c55df 100644
--- a/spec/frontend/design_management/components/design_todo_button_spec.js
+++ b/spec/frontend/design_management/components/design_todo_button_spec.js
@@ -111,7 +111,7 @@ describe('Design management design todo button', () => {
});
it('renders correct button text', () => {
- expect(wrapper.text()).toBe('Add a To Do');
+ expect(wrapper.text()).toBe('Add a to do');
});
describe('when clicked', () => {
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 36a2ffd19c3..8fe3e92360a 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
@@ -26,9 +26,10 @@ exports[`Design management list item component with notes renders item with mult
<img
alt="test"
- class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full design-img"
data-qa-filename="test"
data-qa-selector="design_image"
+ data-testid="design-img-1"
src=""
/>
</gl-intersection-observer-stub>
@@ -43,6 +44,8 @@ exports[`Design management list item component with notes renders item with mult
<span
class="gl-font-weight-bold str-truncated-100"
data-qa-selector="design_file_name"
+ data-testid="design-img-filename-1"
+ title="test"
>
test
</span>
@@ -100,9 +103,10 @@ exports[`Design management list item component with notes renders item with sing
<img
alt="test"
- class="gl-display-block gl-mx-auto gl-max-w-full mh-100 design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full design-img"
data-qa-filename="test"
data-qa-selector="design_image"
+ data-testid="design-img-1"
src=""
/>
</gl-intersection-observer-stub>
@@ -117,6 +121,8 @@ exports[`Design management list item component with notes renders item with sing
<span
class="gl-font-weight-bold str-truncated-100"
data-qa-selector="design_file_name"
+ data-testid="design-img-filename-1"
+ title="test"
>
test
</span>
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
index 55c6ecbc26b..a89d05127c7 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 { extendedWrapper } from 'helpers/vue_test_utils_helper';
import Item from '~/design_management/components/list/item.vue';
const localVue = createLocalVue();
@@ -17,8 +18,11 @@ const DESIGN_VERSION_EVENT = {
describe('Design management list item component', () => {
let wrapper;
+ const imgId = 1;
+ const imgFilename = 'test';
- const findDesignEvent = () => wrapper.find('[data-testid="designEvent"]');
+ const findDesignEvent = () => wrapper.findByTestId('design-event');
+ const findImgFilename = (id = imgId) => wrapper.findByTestId(`design-img-filename-${id}`);
const findEventIcon = () => findDesignEvent().find(GlIcon);
const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
@@ -28,25 +32,27 @@ describe('Design management list item component', () => {
isUploading = false,
imageLoading = false,
} = {}) {
- wrapper = shallowMount(Item, {
- localVue,
- router,
- propsData: {
- id: 1,
- filename: 'test',
- image: 'http://via.placeholder.com/300',
- isUploading,
- event,
- notesCount,
- updatedAt: '01-01-2019',
- },
- data() {
- return {
- imageLoading,
- };
- },
- stubs: ['router-link'],
- });
+ wrapper = extendedWrapper(
+ shallowMount(Item, {
+ localVue,
+ router,
+ propsData: {
+ id: imgId,
+ filename: imgFilename,
+ image: 'http://via.placeholder.com/300',
+ isUploading,
+ event,
+ notesCount,
+ updatedAt: '01-01-2019',
+ },
+ data() {
+ return {
+ imageLoading,
+ };
+ },
+ stubs: ['router-link'],
+ }),
+ );
}
afterEach(() => {
@@ -75,6 +81,10 @@ describe('Design management list item component', () => {
return wrapper.vm.$nextTick();
});
+ it('renders a tooltip', () => {
+ expect(findImgFilename().attributes('title')).toEqual(imgFilename);
+ });
+
describe('before image is loaded', () => {
it('renders loading spinner', () => {
expect(wrapper.find(GlLoadingIcon)).toExist();
diff --git a/spec/frontend/design_management/pages/design/index_spec.js b/spec/frontend/design_management/pages/design/index_spec.js
index 9c11af28cf0..63e1c712d6b 100644
--- a/spec/frontend/design_management/pages/design/index_spec.js
+++ b/spec/frontend/design_management/pages/design/index_spec.js
@@ -18,15 +18,15 @@ import { DESIGNS_ROUTE_NAME, DESIGN_ROUTE_NAME } from '~/design_management/route
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';
-import design from '../../mock_data/design';
-import mockResponseWithDesigns from '../../mock_data/designs';
-import mockResponseNoDesigns from '../../mock_data/no_designs';
-import mockAllVersions from '../../mock_data/all_versions';
import {
DESIGN_TRACKING_PAGE_NAME,
DESIGN_SNOWPLOW_EVENT_TYPES,
DESIGN_USAGE_PING_EVENT_TYPES,
} from '~/design_management/utils/tracking';
+import design from '../../mock_data/design';
+import mockResponseWithDesigns from '../../mock_data/designs';
+import mockResponseNoDesigns from '../../mock_data/no_designs';
+import mockAllVersions from '../../mock_data/all_versions';
jest.mock('~/flash');
jest.mock('~/api.js');
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 7d28d6f6d11..94d75f018f9 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -22,6 +22,11 @@ import {
import createFlash from '~/flash';
import createRouter from '~/design_management/router';
import * as utils from '~/design_management/utils/design_management_utils';
+import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql';
+import {
+ DESIGN_TRACKING_PAGE_NAME,
+ DESIGN_SNOWPLOW_EVENT_TYPES,
+} from '~/design_management/utils/tracking';
import {
designListQueryResponse,
designUploadMutationCreatedResponse,
@@ -31,11 +36,6 @@ import {
reorderedDesigns,
moveDesignMutationResponseWithErrors,
} from '../mock_data/apollo_mock';
-import moveDesignMutation from '~/design_management/graphql/mutations/move_design.mutation.graphql';
-import {
- DESIGN_TRACKING_PAGE_NAME,
- DESIGN_SNOWPLOW_EVENT_TYPES,
-} from '~/design_management/utils/tracking';
jest.mock('~/flash.js');
const mockPageEl = {
diff --git a/spec/frontend/design_management/utils/cache_update_spec.js b/spec/frontend/design_management/utils/cache_update_spec.js
index 2fb08c3ef05..7327cf00abd 100644
--- a/spec/frontend/design_management/utils/cache_update_spec.js
+++ b/spec/frontend/design_management/utils/cache_update_spec.js
@@ -10,8 +10,8 @@ import {
ADD_IMAGE_DIFF_NOTE_ERROR,
UPDATE_IMAGE_DIFF_NOTE_ERROR,
} from '~/design_management/utils/error_messages';
-import design from '../mock_data/design';
import createFlash from '~/flash';
+import design from '../mock_data/design';
jest.mock('~/flash.js');
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 7fbeb33dd93..e84fa6fb0ae 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -3,8 +3,8 @@ import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import { GlLoadingIcon, GlPagination } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'spec/test_constants';
import Mousetrap from 'mousetrap';
+import { TEST_HOST } from 'spec/test_constants';
import App from '~/diffs/components/app.vue';
import NoChanges from '~/diffs/components/no_changes.vue';
import DiffFile from '~/diffs/components/diff_file.vue';
@@ -13,14 +13,14 @@ import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
import CommitWidget from '~/diffs/components/commit_widget.vue';
import TreeList from '~/diffs/components/tree_list.vue';
-import createDiffsStore from '../create_diffs_store';
import axios from '~/lib/utils/axios_utils';
import * as urlUtils from '~/lib/utils/url_utility';
-import diffsMockData from '../mock_data/merge_request_diffs';
import { EVT_VIEW_FILE_BY_FILE } from '~/diffs/constants';
import eventHub from '~/diffs/event_hub';
+import diffsMockData from '../mock_data/merge_request_diffs';
+import createDiffsStore from '../create_diffs_store';
const mergeRequestDiff = { version_index: 1 };
const TEST_ENDPOINT = `${TEST_HOST}/diff/endpoint`;
diff --git a/spec/frontend/diffs/components/compare_versions_spec.js b/spec/frontend/diffs/components/compare_versions_spec.js
index 949cc855200..adb695b8a64 100644
--- a/spec/frontend/diffs/components/compare_versions_spec.js
+++ b/spec/frontend/diffs/components/compare_versions_spec.js
@@ -1,6 +1,6 @@
-import { trimText } from 'helpers/text_helper';
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
+import { trimText } from 'helpers/text_helper';
import CompareVersionsComponent from '~/diffs/components/compare_versions.vue';
import { createStore } from '~/mr_notes/stores';
import diffsMockData from '../mock_data/merge_request_diffs';
diff --git a/spec/frontend/diffs/components/diff_content_spec.js b/spec/frontend/diffs/components/diff_content_spec.js
index c1cf4793c88..7b2b8996808 100644
--- a/spec/frontend/diffs/components/diff_content_spec.js
+++ b/spec/frontend/diffs/components/diff_content_spec.js
@@ -9,9 +9,9 @@ import ParallelDiffView from '~/diffs/components/parallel_diff_view.vue';
import NoteForm from '~/notes/components/note_form.vue';
import DiffDiscussions from '~/diffs/components/diff_discussions.vue';
import { IMAGE_DIFF_POSITION_TYPE } from '~/diffs/constants';
-import diffFileMockData from '../mock_data/diff_file';
import { diffViewerModes } from '~/ide/constants';
import DiffView from '~/diffs/components/diff_view.vue';
+import diffFileMockData from '../mock_data/diff_file';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index e9a63e861ed..5bd59055afc 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -7,16 +7,23 @@ import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import FileIcon from '~/vue_shared/components/file_icon.vue';
-import diffDiscussionsMockData from '../mock_data/diff_discussions';
import { truncateSha } from '~/lib/utils/text_utility';
import { diffViewerModes } from '~/ide/constants';
import { __, sprintf } from '~/locale';
import { scrollToElement } from '~/lib/utils/common_utils';
+import { SET_MR_FILE_REVIEWS } from '~/diffs/store/mutation_types';
+import { reviewFile } from '~/diffs/store/actions';
+import { DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE } from '~/diffs/constants';
+import testAction from '../../__helpers__/vuex_action_helper';
+import diffDiscussionsMockData from '../mock_data/diff_discussions';
+
jest.mock('~/lib/utils/common_utils');
const diffFile = Object.freeze(
Object.assign(diffDiscussionsMockData.diff_file, {
+ id: '123',
+ file_identifier_hash: 'abc',
edit_path: 'link:/to/edit/path',
blob: {
id: '848ed9407c6730ff16edb3dd24485a0eea24292a',
@@ -52,6 +59,8 @@ describe('DiffFileHeader component', () => {
toggleFileDiscussionWrappers: jest.fn(),
toggleFullDiff: jest.fn(),
toggleActiveFileByHash: jest.fn(),
+ setFileCollapsedByUser: jest.fn(),
+ reviewFile: jest.fn(),
},
},
},
@@ -79,10 +88,11 @@ describe('DiffFileHeader component', () => {
const findViewFileButton = () => wrapper.find({ ref: 'viewButton' });
const findCollapseIcon = () => wrapper.find({ ref: 'collapseIcon' });
const findEditButton = () => wrapper.find({ ref: 'editButton' });
+ const findReviewFileCheckbox = () => wrapper.find("[data-testid='fileReviewCheckbox']");
- const createComponent = (props) => {
+ const createComponent = ({ props, options = {} } = {}) => {
mockStoreConfig = cloneDeep(defaultMockStoreConfig);
- const store = new Vuex.Store(mockStoreConfig);
+ const store = new Vuex.Store({ ...mockStoreConfig, ...(options.store || {}) });
wrapper = shallowMount(DiffFileHeader, {
propsData: {
@@ -91,6 +101,7 @@ describe('DiffFileHeader component', () => {
viewDiffsFileByFile: false,
...props,
},
+ ...options,
localVue,
store,
});
@@ -101,7 +112,7 @@ describe('DiffFileHeader component', () => {
${'visible'} | ${true}
${'hidden'} | ${false}
`('collapse toggle is $visibility if collapsible is $collapsible', ({ collapsible }) => {
- createComponent({ collapsible });
+ createComponent({ props: { collapsible } });
expect(findCollapseIcon().exists()).toBe(collapsible);
});
@@ -110,7 +121,7 @@ describe('DiffFileHeader component', () => {
${true} | ${'chevron-down'}
${false} | ${'chevron-right'}
`('collapse icon is $icon if expanded is $expanded', ({ icon, expanded }) => {
- createComponent({ expanded, collapsible: true });
+ createComponent({ props: { expanded, collapsible: true } });
expect(findCollapseIcon().props('name')).toBe(icon);
});
@@ -124,7 +135,7 @@ describe('DiffFileHeader component', () => {
});
it('when collapseIcon is clicked emits toggleFile', () => {
- createComponent({ collapsible: true });
+ createComponent({ props: { collapsible: true } });
findCollapseIcon().vm.$emit('click', new Event('click'));
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().toggleFile).toBeDefined();
@@ -132,7 +143,7 @@ describe('DiffFileHeader component', () => {
});
it('when other element in header is clicked does not emits toggleFile', () => {
- createComponent({ collapsible: true });
+ createComponent({ props: { collapsible: true } });
findTitleLink().trigger('click');
return wrapper.vm.$nextTick().then(() => {
@@ -171,10 +182,12 @@ describe('DiffFileHeader component', () => {
it('prefers submodule_tree_url over submodule_link for href', () => {
const submoduleTreeUrl = 'some://tree/url';
createComponent({
- discussionLink: 'discussionLink',
- diffFile: {
- ...submoduleDiffFile,
- submodule_tree_url: 'some://tree/url',
+ props: {
+ discussionLink: 'discussionLink',
+ diffFile: {
+ ...submoduleDiffFile,
+ submodule_tree_url: 'some://tree/url',
+ },
},
});
@@ -184,8 +197,10 @@ describe('DiffFileHeader component', () => {
it('uses submodule_link for href if submodule_tree_url does not exists', () => {
const submoduleLink = 'link://to/submodule';
createComponent({
- discussionLink: 'discussionLink',
- diffFile: submoduleDiffFile,
+ props: {
+ discussionLink: 'discussionLink',
+ diffFile: submoduleDiffFile,
+ },
});
expect(findTitleLink().attributes('href')).toBe(submoduleLink);
@@ -193,7 +208,9 @@ describe('DiffFileHeader component', () => {
it('uses file_path + SHA as link text', () => {
createComponent({
- diffFile: submoduleDiffFile,
+ props: {
+ diffFile: submoduleDiffFile,
+ },
});
expect(findTitleLink().text()).toContain(
@@ -203,15 +220,19 @@ describe('DiffFileHeader component', () => {
it('does not render file actions', () => {
createComponent({
- diffFile: submoduleDiffFile,
- addMergeRequestButtons: true,
+ props: {
+ diffFile: submoduleDiffFile,
+ addMergeRequestButtons: true,
+ },
});
expect(findFileActions().exists()).toBe(false);
});
it('renders submodule icon', () => {
createComponent({
- diffFile: submoduleDiffFile,
+ props: {
+ diffFile: submoduleDiffFile,
+ },
});
expect(wrapper.find(FileIcon).props('submodule')).toBe(true);
@@ -223,13 +244,15 @@ describe('DiffFileHeader component', () => {
it('for mode_changed file mode displays mode changes', () => {
createComponent({
- diffFile: {
- ...diffFile,
- a_mode: 'old-mode',
- b_mode: 'new-mode',
- viewer: {
- ...diffFile.viewer,
- name: diffViewerModes.mode_changed,
+ props: {
+ diffFile: {
+ ...diffFile,
+ a_mode: 'old-mode',
+ b_mode: 'new-mode',
+ viewer: {
+ ...diffFile.viewer,
+ name: diffViewerModes.mode_changed,
+ },
},
},
});
@@ -240,13 +263,15 @@ describe('DiffFileHeader component', () => {
'for %s file mode does not display mode changes',
(mode) => {
createComponent({
- diffFile: {
- ...diffFile,
- a_mode: 'old-mode',
- b_mode: 'new-mode',
- viewer: {
- ...diffFile.viewer,
- name: diffViewerModes[mode],
+ props: {
+ diffFile: {
+ ...diffFile,
+ a_mode: 'old-mode',
+ b_mode: 'new-mode',
+ viewer: {
+ ...diffFile.viewer,
+ name: diffViewerModes[mode],
+ },
},
},
});
@@ -256,32 +281,38 @@ describe('DiffFileHeader component', () => {
it('displays the LFS label for files stored in LFS', () => {
createComponent({
- diffFile: { ...diffFile, stored_externally: true, external_storage: 'lfs' },
+ props: {
+ diffFile: { ...diffFile, stored_externally: true, external_storage: 'lfs' },
+ },
});
expect(findLfsLabel().exists()).toBe(true);
});
it('does not display the LFS label for files stored in repository', () => {
createComponent({
- diffFile: { ...diffFile, stored_externally: false },
+ props: {
+ diffFile: { ...diffFile, stored_externally: false },
+ },
});
expect(findLfsLabel().exists()).toBe(false);
});
it('does not render view replaced file button if no replaced view path is present', () => {
createComponent({
- diffFile: { ...diffFile, replaced_view_path: null },
+ props: {
+ diffFile: { ...diffFile, replaced_view_path: null },
+ },
});
expect(findReplacedFileButton().exists()).toBe(false);
});
describe('when addMergeRequestButtons is false', () => {
it('does not render file actions', () => {
- createComponent({ addMergeRequestButtons: false });
+ createComponent({ props: { addMergeRequestButtons: false } });
expect(findFileActions().exists()).toBe(false);
});
it('should not render edit button', () => {
- createComponent({ addMergeRequestButtons: false });
+ createComponent({ props: { addMergeRequestButtons: false } });
expect(findEditButton().exists()).toBe(false);
});
});
@@ -290,7 +321,7 @@ describe('DiffFileHeader component', () => {
describe('without discussions', () => {
it('does not render a toggle discussions button', () => {
diffHasDiscussionsResultMock.mockReturnValue(false);
- createComponent({ addMergeRequestButtons: true });
+ createComponent({ props: { addMergeRequestButtons: true } });
expect(findToggleDiscussionsButton().exists()).toBe(false);
});
});
@@ -298,7 +329,7 @@ describe('DiffFileHeader component', () => {
describe('with discussions', () => {
it('dispatches toggleFileDiscussionWrappers when user clicks on toggle discussions button', () => {
diffHasDiscussionsResultMock.mockReturnValue(true);
- createComponent({ addMergeRequestButtons: true });
+ createComponent({ props: { addMergeRequestButtons: true } });
expect(findToggleDiscussionsButton().exists()).toBe(true);
findToggleDiscussionsButton().vm.$emit('click');
expect(
@@ -309,7 +340,9 @@ describe('DiffFileHeader component', () => {
it('should show edit button', () => {
createComponent({
- addMergeRequestButtons: true,
+ props: {
+ addMergeRequestButtons: true,
+ },
});
expect(findEditButton().exists()).toBe(true);
});
@@ -319,25 +352,27 @@ describe('DiffFileHeader component', () => {
const externalUrl = 'link://to/external';
const formattedExternalUrl = 'link://formatted';
createComponent({
- diffFile: {
- ...diffFile,
- external_url: externalUrl,
- formatted_external_url: formattedExternalUrl,
+ props: {
+ diffFile: {
+ ...diffFile,
+ external_url: externalUrl,
+ formatted_external_url: formattedExternalUrl,
+ },
+ addMergeRequestButtons: true,
},
- addMergeRequestButtons: true,
});
expect(findExternalLink().exists()).toBe(true);
});
it('is hidden by default', () => {
- createComponent({ addMergeRequestButtons: true });
+ createComponent({ props: { addMergeRequestButtons: true } });
expect(findExternalLink().exists()).toBe(false);
});
});
describe('without file blob', () => {
beforeEach(() => {
- createComponent({ diffFile: { ...diffFile, blob: false } });
+ createComponent({ props: { diffFile: { ...diffFile, blob: false } } });
});
it('should not render toggle discussions button', () => {
@@ -352,8 +387,10 @@ describe('DiffFileHeader component', () => {
it('should render correct file view button', () => {
const viewPath = 'link://view-path';
createComponent({
- diffFile: { ...diffFile, view_path: viewPath },
- addMergeRequestButtons: true,
+ props: {
+ diffFile: { ...diffFile, view_path: viewPath },
+ addMergeRequestButtons: true,
+ },
});
expect(findViewFileButton().attributes('href')).toBe(viewPath);
expect(findViewFileButton().text()).toEqual(
@@ -367,9 +404,11 @@ describe('DiffFileHeader component', () => {
describe('when diff is fully expanded', () => {
it('is not rendered', () => {
createComponent({
- diffFile: {
- ...diffFile,
- is_fully_expanded: true,
+ props: {
+ diffFile: {
+ ...diffFile,
+ is_fully_expanded: true,
+ },
},
});
expect(findExpandButton().exists()).toBe(false);
@@ -387,17 +426,17 @@ describe('DiffFileHeader component', () => {
};
it('renders expand to full file button if not showing full file already', () => {
- createComponent(fullyNotExpandedFileProps);
+ createComponent({ props: fullyNotExpandedFileProps });
expect(findExpandButton().exists()).toBe(true);
});
it('renders loading icon when loading full file', () => {
- createComponent(fullyNotExpandedFileProps);
+ createComponent({ props: fullyNotExpandedFileProps });
expect(findExpandButton().exists()).toBe(true);
});
it('toggles full diff on click', () => {
- createComponent(fullyNotExpandedFileProps);
+ createComponent({ props: fullyNotExpandedFileProps });
findExpandButton().vm.$emit('click');
expect(mockStoreConfig.modules.diffs.actions.toggleFullDiff).toHaveBeenCalled();
});
@@ -407,7 +446,9 @@ describe('DiffFileHeader component', () => {
it('uses discussionPath for link if it is defined', () => {
const discussionPath = 'link://to/discussion';
createComponent({
- discussionPath,
+ props: {
+ discussionPath,
+ },
});
expect(findTitleLink().attributes('href')).toBe(discussionPath);
});
@@ -436,21 +477,21 @@ describe('DiffFileHeader component', () => {
describe('for new file', () => {
it('displays the path', () => {
- createComponent({ diffFile: { ...diffFile, new_file: true } });
+ createComponent({ props: { diffFile: { ...diffFile, new_file: true } } });
expect(findTitleLink().text()).toBe(diffFile.file_path);
});
});
describe('for deleted file', () => {
it('displays the path', () => {
- createComponent({ diffFile: { ...diffFile, deleted_file: true } });
+ createComponent({ props: { diffFile: { ...diffFile, deleted_file: true } } });
expect(findTitleLink().text()).toBe(
sprintf(__('%{filePath} deleted'), { filePath: diffFile.file_path }, false),
);
});
it('does not show edit button', () => {
- createComponent({ diffFile: { ...diffFile, deleted_file: true } });
+ createComponent({ props: { diffFile: { ...diffFile, deleted_file: true } } });
expect(findEditButton().exists()).toBe(false);
});
});
@@ -458,11 +499,13 @@ describe('DiffFileHeader component', () => {
describe('for renamed file', () => {
it('displays old and new path if the file was renamed', () => {
createComponent({
- diffFile: {
- ...diffFile,
- renamed_file: true,
- old_path_html: 'old',
- new_path_html: 'new',
+ props: {
+ diffFile: {
+ ...diffFile,
+ renamed_file: true,
+ old_path_html: 'old',
+ new_path_html: 'new',
+ },
},
});
expect(findTitleLink().text()).toMatch(/^old.+new/s);
@@ -473,13 +516,132 @@ describe('DiffFileHeader component', () => {
it('renders view replaced file button', () => {
const replacedViewPath = 'some/path';
createComponent({
- diffFile: {
- ...diffFile,
- replaced_view_path: replacedViewPath,
+ props: {
+ diffFile: {
+ ...diffFile,
+ replaced_view_path: replacedViewPath,
+ },
+ addMergeRequestButtons: true,
},
- addMergeRequestButtons: true,
});
expect(findReplacedFileButton().exists()).toBe(true);
});
});
+
+ describe('file reviews', () => {
+ it('calls the action to set the new review', () => {
+ createComponent({
+ props: {
+ diffFile: {
+ ...diffFile,
+ viewer: {
+ ...diffFile.viewer,
+ automaticallyCollapsed: false,
+ manuallyCollapsed: null,
+ },
+ },
+ showLocalFileReviews: true,
+ addMergeRequestButtons: true,
+ },
+ });
+
+ const file = wrapper.vm.diffFile;
+
+ findReviewFileCheckbox().vm.$emit('change', true);
+
+ return testAction(
+ reviewFile,
+ { file, reviewed: true },
+ {},
+ [{ type: SET_MR_FILE_REVIEWS, payload: { [file.file_identifier_hash]: [file.id] } }],
+ [],
+ );
+ });
+
+ it.each`
+ description | newReviewedStatus | collapseType | aCollapse | mCollapse | callAction
+ ${'does nothing'} | ${true} | ${DIFF_FILE_MANUAL_COLLAPSE} | ${false} | ${true} | ${false}
+ ${'does nothing'} | ${false} | ${DIFF_FILE_AUTOMATIC_COLLAPSE} | ${true} | ${null} | ${false}
+ ${'does nothing'} | ${true} | ${'not collapsed'} | ${false} | ${null} | ${false}
+ ${'does nothing'} | ${false} | ${'not collapsed'} | ${false} | ${null} | ${false}
+ ${'collapses the file'} | ${true} | ${DIFF_FILE_AUTOMATIC_COLLAPSE} | ${true} | ${null} | ${true}
+ `(
+ "$description if the new review status is reviewed = $newReviewedStatus and the file's collapse type is collapse = $collapseType",
+ ({ newReviewedStatus, aCollapse, mCollapse, callAction }) => {
+ createComponent({
+ props: {
+ diffFile: {
+ ...diffFile,
+ viewer: {
+ ...diffFile.viewer,
+ automaticallyCollapsed: aCollapse,
+ manuallyCollapsed: mCollapse,
+ },
+ },
+ showLocalFileReviews: true,
+ addMergeRequestButtons: true,
+ },
+ });
+
+ findReviewFileCheckbox().vm.$emit('change', newReviewedStatus);
+
+ if (callAction) {
+ expect(mockStoreConfig.modules.diffs.actions.setFileCollapsedByUser).toHaveBeenCalled();
+ } else {
+ expect(
+ mockStoreConfig.modules.diffs.actions.setFileCollapsedByUser,
+ ).not.toHaveBeenCalled();
+ }
+ },
+ );
+
+ it.each`
+ description | show | visible
+ ${'shows'} | ${true} | ${true}
+ ${'hides'} | ${false} | ${false}
+ `(
+ '$description the file review feature given { showLocalFileReviewsProp: $show }',
+ ({ show, visible }) => {
+ createComponent({
+ props: {
+ showLocalFileReviews: show,
+ addMergeRequestButtons: true,
+ },
+ });
+
+ expect(findReviewFileCheckbox().exists()).toEqual(visible);
+ },
+ );
+
+ it.each`
+ open | status | fires
+ ${true} | ${true} | ${true}
+ ${false} | ${false} | ${true}
+ ${true} | ${false} | ${false}
+ ${false} | ${true} | ${false}
+ `(
+ 'toggles appropriately when { fileExpanded: $open, newReviewStatus: $status }',
+ ({ open, status, fires }) => {
+ createComponent({
+ props: {
+ diffFile: {
+ ...diffFile,
+ viewer: {
+ ...diffFile.viewer,
+ automaticallyCollapsed: false,
+ manuallyCollapsed: null,
+ },
+ },
+ showLocalFileReviews: true,
+ addMergeRequestButtons: true,
+ expanded: open,
+ },
+ });
+
+ findReviewFileCheckbox().vm.$emit('change', status);
+
+ expect(Boolean(wrapper.emitted().toggleFile)).toBe(fires);
+ },
+ );
+ });
});
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index c715d779986..a8a1c88b0cc 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -6,8 +6,6 @@ import axios from '~/lib/utils/axios_utils';
import httpStatus from '~/lib/utils/http_status';
import createDiffsStore from '~/diffs/store/modules';
import createNotesStore from '~/notes/stores/modules';
-import diffFileMockDataReadable from '../mock_data/diff_file';
-import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable';
import DiffFileComponent from '~/diffs/components/diff_file.vue';
import DiffFileHeaderComponent from '~/diffs/components/diff_file_header.vue';
@@ -21,6 +19,8 @@ import {
} from '~/diffs/constants';
import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
+import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable';
+import diffFileMockDataReadable from '../mock_data/diff_file';
function changeViewer(store, index, { automaticallyCollapsed, manuallyCollapsed, name }) {
const file = store.state.diffs.diffFiles[index];
@@ -66,7 +66,7 @@ function markFileToBeRendered(store, index = 0) {
});
}
-function createComponent({ file, first = false, last = false }) {
+function createComponent({ file, first = false, last = false, options = {}, props = {} }) {
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -89,7 +89,9 @@ function createComponent({ file, first = false, last = false }) {
viewDiffsFileByFile: false,
isFirstFile: first,
isLastFile: last,
+ ...props,
},
+ ...options,
});
return {
@@ -220,6 +222,53 @@ describe('DiffFile', () => {
});
});
+ describe('computed', () => {
+ describe('showLocalFileReviews', () => {
+ let gon;
+
+ function setLoggedIn(bool) {
+ window.gon.current_user_id = bool;
+ }
+
+ beforeAll(() => {
+ gon = window.gon;
+ window.gon = {};
+ });
+
+ afterEach(() => {
+ window.gon = gon;
+ });
+
+ it.each`
+ loggedIn | featureOn | bool
+ ${true} | ${true} | ${true}
+ ${false} | ${true} | ${false}
+ ${true} | ${false} | ${false}
+ ${false} | ${false} | ${false}
+ `(
+ 'should be $bool when { userIsLoggedIn: $loggedIn, featureEnabled: $featureOn }',
+ ({ loggedIn, featureOn, bool }) => {
+ setLoggedIn(loggedIn);
+
+ ({ wrapper } = createComponent({
+ options: {
+ provide: {
+ glFeatures: {
+ localFileReviews: featureOn,
+ },
+ },
+ },
+ props: {
+ file: store.state.diffs.diffFiles[0],
+ },
+ }));
+
+ expect(wrapper.vm.showLocalFileReviews).toBe(bool);
+ },
+ );
+ });
+ });
+
describe('collapsing', () => {
describe(`\`${EVT_EXPAND_ALL_FILES}\` event`, () => {
beforeEach(() => {
@@ -422,9 +471,11 @@ describe('DiffFile', () => {
await wrapper.vm.$nextTick();
- expect(wrapper.vm.$el.innerText).toContain(
- 'This source diff could not be displayed because it is too large',
- );
+ const button = wrapper.find('[data-testid="blob-button"]');
+
+ expect(wrapper.text()).toContain('Changes are too large to be shown.');
+ expect(button.html()).toContain('View file @');
+ expect(button.attributes('href')).toBe('/file/view/path');
});
});
});
diff --git a/spec/frontend/diffs/components/diff_row_spec.js b/spec/frontend/diffs/components/diff_row_spec.js
index c06d8e78316..0bfbbc16f07 100644
--- a/spec/frontend/diffs/components/diff_row_spec.js
+++ b/spec/frontend/diffs/components/diff_row_spec.js
@@ -3,8 +3,8 @@ import { getByTestId, fireEvent } from '@testing-library/dom';
import Vuex from 'vuex';
import diffsModule from '~/diffs/store/modules';
import DiffRow from '~/diffs/components/diff_row.vue';
-import diffFileMockData from '../mock_data/diff_file';
import { mapParallel } from '~/diffs/components/diff_row_utils';
+import diffFileMockData from '../mock_data/diff_file';
describe('DiffRow', () => {
const testLines = [
diff --git a/spec/frontend/diffs/components/diff_row_utils_spec.js b/spec/frontend/diffs/components/diff_row_utils_spec.js
index d70d6b609ac..47ae3cd5867 100644
--- a/spec/frontend/diffs/components/diff_row_utils_spec.js
+++ b/spec/frontend/diffs/components/diff_row_utils_spec.js
@@ -143,10 +143,21 @@ describe('addCommentTooltip', () => {
'Commenting on symbolic links that replace or are replaced by files is currently not supported.';
const brokenRealTooltip =
'Commenting on files that replace or are replaced by symbolic links is currently not supported.';
+ const commentTooltip = 'Add a comment to this line';
+ const dragTooltip = 'Add a comment to this line or drag for multiple lines';
+
it('should return default tooltip', () => {
expect(utils.addCommentTooltip()).toBeUndefined();
});
+ it('should return comment tooltip', () => {
+ expect(utils.addCommentTooltip({})).toEqual(commentTooltip);
+ });
+
+ it('should return drag comment tooltip when dragging is enabled', () => {
+ expect(utils.addCommentTooltip({}, true)).toEqual(dragTooltip);
+ });
+
it('should return broken symlink tooltip', () => {
expect(utils.addCommentTooltip({ commentsDisabled: { wasSymbolic: true } })).toEqual(
brokenSymLinkTooltip,
diff --git a/spec/frontend/diffs/components/diff_view_spec.js b/spec/frontend/diffs/components/diff_view_spec.js
index 3d36ebf14a3..3f8fc8e6afe 100644
--- a/spec/frontend/diffs/components/diff_view_spec.js
+++ b/spec/frontend/diffs/components/diff_view_spec.js
@@ -55,12 +55,12 @@ describe('DiffView', () => {
});
it.each`
- type | side | container | sides | total
- ${'parallel'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {} }, right: { lineDraft: {} } }} | ${2}
- ${'parallel'} | ${'right'} | ${'.new'} | ${{ left: { lineDraft: {} }, right: { lineDraft: {} } }} | ${2}
- ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {} } }} | ${1}
- ${'inline'} | ${'right'} | ${'.new'} | ${{ right: { lineDraft: {} } }} | ${1}
- ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {} }, right: { lineDraft: {} } }} | ${1}
+ type | side | container | sides | total
+ ${'parallel'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true }, right: { lineDraft: {}, renderDiscussion: true } }} | ${2}
+ ${'parallel'} | ${'right'} | ${'.new'} | ${{ left: { lineDraft: {}, renderDiscussion: true }, right: { lineDraft: {}, renderDiscussion: true } }} | ${2}
+ ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true } }} | ${1}
+ ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true } }} | ${1}
+ ${'inline'} | ${'left'} | ${'.old'} | ${{ left: { lineDraft: {}, renderDiscussion: true } }} | ${1}
`(
'renders a $type comment row with comment cell on $side',
({ type, container, sides, total }) => {
diff --git a/spec/frontend/diffs/components/inline_diff_table_row_spec.js b/spec/frontend/diffs/components/inline_diff_table_row_spec.js
index 21e7d7397a0..56a5306752f 100644
--- a/spec/frontend/diffs/components/inline_diff_table_row_spec.js
+++ b/spec/frontend/diffs/components/inline_diff_table_row_spec.js
@@ -2,9 +2,9 @@ import { shallowMount } from '@vue/test-utils';
import { createStore } from '~/mr_notes/stores';
import InlineDiffTableRow from '~/diffs/components/inline_diff_table_row.vue';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
+import { mapInline } from '~/diffs/components/diff_row_utils';
import diffFileMockData from '../mock_data/diff_file';
import discussionsMockData from '../mock_data/diff_discussions';
-import { mapInline } from '~/diffs/components/diff_row_utils';
const TEST_USER_ID = 'abc123';
const TEST_USER = { id: TEST_USER_ID };
diff --git a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
index 445553706b7..51c4c4ae9d6 100644
--- a/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
+++ b/spec/frontend/diffs/components/parallel_diff_table_row_spec.js
@@ -4,8 +4,8 @@ import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { createStore } from '~/mr_notes/stores';
import ParallelDiffTableRow from '~/diffs/components/parallel_diff_table_row.vue';
import { mapParallel } from '~/diffs/components/diff_row_utils';
-import diffFileMockData from '../mock_data/diff_file';
import DiffGutterAvatars from '~/diffs/components/diff_gutter_avatars.vue';
+import diffFileMockData from '../mock_data/diff_file';
import discussionsMockData from '../mock_data/diff_discussions';
describe('ParallelDiffTableRow', () => {
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 056ac23fcf7..c204648337a 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -58,8 +58,8 @@ import axios from '~/lib/utils/axios_utils';
import * as utils from '~/diffs/store/utils';
import * as commonUtils from '~/lib/utils/common_utils';
import { mergeUrlParams } from '~/lib/utils/url_utility';
-import { diffMetadata } from '../mock_data/diff_metadata';
import { deprecatedCreateFlash as createFlash } from '~/flash';
+import { diffMetadata } from '../mock_data/diff_metadata';
jest.mock('~/flash');
diff --git a/spec/frontend/diffs/store/getters_spec.js b/spec/frontend/diffs/store/getters_spec.js
index 4d7f861ac22..85dc52f8bf7 100644
--- a/spec/frontend/diffs/store/getters_spec.js
+++ b/spec/frontend/diffs/store/getters_spec.js
@@ -375,26 +375,4 @@ describe('Diffs Module Getters', () => {
});
});
});
-
- describe('fileReviews', () => {
- const file1 = { id: '123', file_identifier_hash: 'abc' };
- const file2 = { id: '098', file_identifier_hash: 'abc' };
-
- it.each`
- reviews | files | fileReviews
- ${{}} | ${[file1, file2]} | ${[false, false]}
- ${{ abc: ['123'] }} | ${[file1, file2]} | ${[true, false]}
- ${{ abc: ['098'] }} | ${[file1, file2]} | ${[false, true]}
- ${{ def: ['123'] }} | ${[file1, file2]} | ${[false, false]}
- ${{ abc: ['123'], def: ['098'] }} | ${[]} | ${[]}
- `(
- 'returns $fileReviews based on the diff files in state and the existing reviews $reviews',
- ({ reviews, files, fileReviews }) => {
- localState.diffFiles = files;
- localState.mrReviews = reviews;
-
- expect(getters.fileReviews(localState)).toStrictEqual(fileReviews);
- },
- );
- });
});
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index 2c342d8e2a5..e0fefc4e053 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -2,8 +2,8 @@ import createState from '~/diffs/store/modules/diff_state';
import mutations from '~/diffs/store/mutations';
import * as types from '~/diffs/store/mutation_types';
import { INLINE_DIFF_VIEW_TYPE, INLINE_DIFF_LINES_KEY } from '~/diffs/constants';
-import diffFileMockData from '../mock_data/diff_file';
import * as utils from '~/diffs/store/utils';
+import diffFileMockData from '../mock_data/diff_file';
describe('DiffsStoreMutations', () => {
describe('SET_BASE_CONFIG', () => {
diff --git a/spec/frontend/diffs/utils/diff_file_spec.js b/spec/frontend/diffs/utils/diff_file_spec.js
index 2de8db28e71..c6cfdfced65 100644
--- a/spec/frontend/diffs/utils/diff_file_spec.js
+++ b/spec/frontend/diffs/utils/diff_file_spec.js
@@ -1,4 +1,4 @@
-import { prepareRawDiffFile } from '~/diffs/utils/diff_file';
+import { prepareRawDiffFile, getShortShaFromFile } from '~/diffs/utils/diff_file';
function getDiffFiles() {
const loadFull = 'namespace/project/-/merge_requests/12345/diff_for_path?file_identifier=abc';
@@ -143,4 +143,15 @@ describe('diff_file utilities', () => {
expect(preppedFile).not.toHaveProp('id');
});
});
+
+ describe('getShortShaFromFile', () => {
+ it.each`
+ response | cs
+ ${'12345678'} | ${'12345678abcdogcat'}
+ ${null} | ${undefined}
+ ${'hidogcat'} | ${'hidogcatmorethings'}
+ `('returns $response for a file with { content_sha: $cs }', ({ response, cs }) => {
+ expect(getShortShaFromFile({ content_sha: cs })).toBe(response);
+ });
+ });
});
diff --git a/spec/frontend/diffs/utils/file_reviews_spec.js b/spec/frontend/diffs/utils/file_reviews_spec.js
index 819426ee75f..a58c19a7245 100644
--- a/spec/frontend/diffs/utils/file_reviews_spec.js
+++ b/spec/frontend/diffs/utils/file_reviews_spec.js
@@ -5,6 +5,7 @@ import {
setReviewsForMergeRequest,
isFileReviewed,
markFileReview,
+ reviewStatuses,
reviewable,
} from '~/diffs/utils/file_reviews';
@@ -28,6 +29,39 @@ describe('File Review(s) utilities', () => {
localStorage.clear();
});
+ describe('isFileReviewed', () => {
+ it.each`
+ description | diffFile | fileReviews
+ ${'the file does not have an `id`'} | ${{ ...file, id: undefined }} | ${getDefaultReviews()}
+ ${'there are no reviews for the file'} | ${file} | ${{ ...getDefaultReviews(), abc: undefined }}
+ `('returns `false` if $description', ({ diffFile, fileReviews }) => {
+ expect(isFileReviewed(fileReviews, diffFile)).toBe(false);
+ });
+
+ it("returns `true` for a file if it's available in the provided reviews", () => {
+ expect(isFileReviewed(reviews, file)).toBe(true);
+ });
+ });
+
+ describe('reviewStatuses', () => {
+ const file1 = { id: '123', file_identifier_hash: 'abc' };
+ const file2 = { id: '098', file_identifier_hash: 'abc' };
+
+ it.each`
+ mrReviews | files | fileReviews
+ ${{}} | ${[file1, file2]} | ${[false, false]}
+ ${{ abc: ['123'] }} | ${[file1, file2]} | ${[true, false]}
+ ${{ abc: ['098'] }} | ${[file1, file2]} | ${[false, true]}
+ ${{ def: ['123'] }} | ${[file1, file2]} | ${[false, false]}
+ ${{ abc: ['123'], def: ['098'] }} | ${[]} | ${[]}
+ `(
+ 'returns $fileReviews based on the diff files in state and the existing reviews $reviews',
+ ({ mrReviews, files, fileReviews }) => {
+ expect(reviewStatuses(files, mrReviews)).toStrictEqual(fileReviews);
+ },
+ );
+ });
+
describe('getReviewsForMergeRequest', () => {
it('fetches the appropriate stored reviews from localStorage', () => {
getReviewsForMergeRequest(mrPath);
@@ -73,20 +107,6 @@ describe('File Review(s) utilities', () => {
});
});
- describe('isFileReviewed', () => {
- it.each`
- description | diffFile | fileReviews
- ${'the file does not have an `id`'} | ${{ ...file, id: undefined }} | ${getDefaultReviews()}
- ${'there are no reviews for the file'} | ${file} | ${{ ...getDefaultReviews(), abc: undefined }}
- `('returns `false` if $description', ({ diffFile, fileReviews }) => {
- expect(isFileReviewed(fileReviews, diffFile)).toBe(false);
- });
-
- it("returns `true` for a file if it's available in the provided reviews", () => {
- expect(isFileReviewed(reviews, file)).toBe(true);
- });
- });
-
describe('reviewable', () => {
it.each`
response | diffFile | description
diff --git a/spec/frontend/editor/editor_lite_spec.js b/spec/frontend/editor/editor_lite_spec.js
index c3099997287..20937bc7063 100644
--- a/spec/frontend/editor/editor_lite_spec.js
+++ b/spec/frontend/editor/editor_lite_spec.js
@@ -1,14 +1,21 @@
/* eslint-disable max-classes-per-file */
-import { editor as monacoEditor, languages as monacoLanguages, Uri } from 'monaco-editor';
+import { editor as monacoEditor, languages as monacoLanguages } from 'monaco-editor';
import waitForPromises from 'helpers/wait_for_promises';
-import Editor from '~/editor/editor_lite';
+import { joinPaths } from '~/lib/utils/url_utility';
+import EditorLite from '~/editor/editor_lite';
import { EditorLiteExtension } from '~/editor/extensions/editor_lite_extension_base';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
-import { EDITOR_LITE_INSTANCE_ERROR_NO_EL, URI_PREFIX } from '~/editor/constants';
+import {
+ EDITOR_LITE_INSTANCE_ERROR_NO_EL,
+ URI_PREFIX,
+ EDITOR_READY_EVENT,
+} from '~/editor/constants';
describe('Base editor', () => {
let editorEl;
let editor;
+ let defaultArguments;
+ const blobOriginalContent = 'Foo Foo';
const blobContent = 'Foo Bar';
const blobPath = 'test.md';
const blobGlobalId = 'snippet_777';
@@ -17,15 +24,19 @@ describe('Base editor', () => {
beforeEach(() => {
setFixtures('<div id="editor" data-editor-loading></div>');
editorEl = document.getElementById('editor');
- editor = new Editor();
+ defaultArguments = { el: editorEl, blobPath, blobContent, blobGlobalId };
+ editor = new EditorLite();
});
afterEach(() => {
editor.dispose();
editorEl.remove();
+ monacoEditor.getModels().forEach((model) => {
+ model.dispose();
+ });
});
- const createUri = (...paths) => Uri.file([URI_PREFIX, ...paths].join('/'));
+ const uriFilePath = joinPaths('/', URI_PREFIX, blobGlobalId, blobPath);
it('initializes Editor with basic properties', () => {
expect(editor).toBeDefined();
@@ -38,76 +49,192 @@ describe('Base editor', () => {
expect(editorEl.dataset.editorLoading).toBeUndefined();
});
- describe('instance of the Editor', () => {
+ describe('instance of the Editor Lite', () => {
let modelSpy;
let instanceSpy;
- let setModel;
- let dispose;
+ const setModel = jest.fn();
+ const dispose = jest.fn();
+ const mockModelReturn = (res = fakeModel) => {
+ modelSpy = jest.spyOn(monacoEditor, 'createModel').mockImplementation(() => res);
+ };
+ const mockDecorateInstance = (decorations = {}) => {
+ jest.spyOn(EditorLite, 'convertMonacoToELInstance').mockImplementation((inst) => {
+ return Object.assign(inst, decorations);
+ });
+ };
beforeEach(() => {
- setModel = jest.fn();
- dispose = jest.fn();
- modelSpy = jest.spyOn(monacoEditor, 'createModel').mockImplementation(() => fakeModel);
- instanceSpy = jest.spyOn(monacoEditor, 'create').mockImplementation(() => ({
- setModel,
- dispose,
- onDidDispose: jest.fn(),
- }));
+ modelSpy = jest.spyOn(monacoEditor, 'createModel');
});
- it('throws an error if no dom element is supplied', () => {
- expect(() => {
- editor.createInstance();
- }).toThrow(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
+ describe('instance of the Code Editor', () => {
+ beforeEach(() => {
+ instanceSpy = jest.spyOn(monacoEditor, 'create');
+ });
- expect(modelSpy).not.toHaveBeenCalled();
- expect(instanceSpy).not.toHaveBeenCalled();
- expect(setModel).not.toHaveBeenCalled();
- });
+ it('throws an error if no dom element is supplied', () => {
+ mockDecorateInstance();
+ expect(() => {
+ editor.createInstance();
+ }).toThrow(EDITOR_LITE_INSTANCE_ERROR_NO_EL);
- it('creates model to be supplied to Monaco editor', () => {
- editor.createInstance({ el: editorEl, blobPath, blobContent, blobGlobalId: '' });
+ expect(modelSpy).not.toHaveBeenCalled();
+ expect(instanceSpy).not.toHaveBeenCalled();
+ expect(EditorLite.convertMonacoToELInstance).not.toHaveBeenCalled();
+ });
- expect(modelSpy).toHaveBeenCalledWith(blobContent, undefined, createUri(blobPath));
- expect(setModel).toHaveBeenCalledWith(fakeModel);
- });
+ it('creates model to be supplied to Monaco editor', () => {
+ mockModelReturn();
+ mockDecorateInstance({
+ setModel,
+ });
+ editor.createInstance(defaultArguments);
- it('initializes the instance on a supplied DOM node', () => {
- editor.createInstance({ el: editorEl });
+ expect(modelSpy).toHaveBeenCalledWith(
+ blobContent,
+ undefined,
+ expect.objectContaining({
+ path: uriFilePath,
+ }),
+ );
+ expect(setModel).toHaveBeenCalledWith(fakeModel);
+ });
- expect(editor.editorEl).not.toBe(null);
- expect(instanceSpy).toHaveBeenCalledWith(editorEl, expect.anything());
- });
+ it('does not create a model automatically if model is passed as `null`', () => {
+ mockDecorateInstance({
+ setModel,
+ });
+ editor.createInstance({ ...defaultArguments, model: null });
+ expect(modelSpy).not.toHaveBeenCalled();
+ expect(setModel).not.toHaveBeenCalled();
+ });
- it('with blobGlobalId, creates model with id in uri', () => {
- editor.createInstance({ el: editorEl, blobPath, blobContent, blobGlobalId });
+ it('initializes the instance on a supplied DOM node', () => {
+ editor.createInstance({ el: editorEl });
- expect(modelSpy).toHaveBeenCalledWith(
- blobContent,
- undefined,
- createUri(blobGlobalId, blobPath),
- );
- });
+ expect(editor.editorEl).not.toBe(null);
+ expect(instanceSpy).toHaveBeenCalledWith(editorEl, expect.anything());
+ });
- it('initializes instance with passed properties', () => {
- const instanceOptions = {
- foo: 'bar',
- };
- editor.createInstance({
- el: editorEl,
- ...instanceOptions,
+ it('with blobGlobalId, creates model with the id in uri', () => {
+ editor.createInstance(defaultArguments);
+
+ expect(modelSpy).toHaveBeenCalledWith(
+ blobContent,
+ undefined,
+ expect.objectContaining({
+ path: uriFilePath,
+ }),
+ );
+ });
+
+ it('initializes instance with passed properties', () => {
+ const instanceOptions = {
+ foo: 'bar',
+ };
+ editor.createInstance({
+ el: editorEl,
+ ...instanceOptions,
+ });
+ expect(instanceSpy).toHaveBeenCalledWith(
+ editorEl,
+ expect.objectContaining(instanceOptions),
+ );
+ });
+
+ it('disposes instance when the global editor is disposed', () => {
+ mockDecorateInstance({
+ dispose,
+ });
+ editor.createInstance(defaultArguments);
+
+ expect(dispose).not.toHaveBeenCalled();
+
+ editor.dispose();
+
+ expect(dispose).toHaveBeenCalled();
+ });
+
+ it("removes the disposed instance from the global editor's storage and disposes the associated model", () => {
+ mockModelReturn();
+ mockDecorateInstance({
+ setModel,
+ });
+ const instance = editor.createInstance(defaultArguments);
+
+ expect(editor.instances).toHaveLength(1);
+ expect(fakeModel.dispose).not.toHaveBeenCalled();
+
+ instance.dispose();
+
+ expect(editor.instances).toHaveLength(0);
+ expect(fakeModel.dispose).toHaveBeenCalled();
});
- expect(instanceSpy).toHaveBeenCalledWith(editorEl, expect.objectContaining(instanceOptions));
});
- it('disposes instance when the editor is disposed', () => {
- editor.createInstance({ el: editorEl, blobPath, blobContent, blobGlobalId });
+ describe('instance of the Diff Editor', () => {
+ beforeEach(() => {
+ instanceSpy = jest.spyOn(monacoEditor, 'createDiffEditor');
+ });
- expect(dispose).not.toHaveBeenCalled();
+ it('Diff Editor goes through the normal path of Code Editor just with the flag ON', () => {
+ const spy = jest.spyOn(editor, 'createInstance').mockImplementation(() => {});
+ editor.createDiffInstance();
+ expect(spy).toHaveBeenCalledWith(
+ expect.objectContaining({
+ isDiff: true,
+ }),
+ );
+ });
- editor.dispose();
+ it('initializes the instance on a supplied DOM node', () => {
+ const wrongInstanceSpy = jest.spyOn(monacoEditor, 'create').mockImplementation(() => ({}));
+ editor.createDiffInstance({ ...defaultArguments, blobOriginalContent });
+
+ expect(editor.editorEl).not.toBe(null);
+ expect(wrongInstanceSpy).not.toHaveBeenCalled();
+ expect(instanceSpy).toHaveBeenCalledWith(editorEl, expect.anything());
+ });
+
+ it('creates correct model for the Diff Editor', () => {
+ const instance = editor.createDiffInstance({ ...defaultArguments, blobOriginalContent });
+ const getDiffModelValue = (model) => instance.getModel()[model].getValue();
+
+ expect(modelSpy).toHaveBeenCalledTimes(2);
+ expect(modelSpy.mock.calls[0]).toEqual([
+ blobContent,
+ undefined,
+ expect.objectContaining({
+ path: uriFilePath,
+ }),
+ ]);
+ expect(modelSpy.mock.calls[1]).toEqual([blobOriginalContent, 'markdown']);
+ expect(getDiffModelValue('original')).toBe(blobOriginalContent);
+ expect(getDiffModelValue('modified')).toBe(blobContent);
+ });
- expect(dispose).toHaveBeenCalled();
+ it('correctly disposes the diff editor model', () => {
+ const modifiedModel = fakeModel;
+ const originalModel = { ...fakeModel };
+ mockDecorateInstance({
+ getModel: jest.fn().mockReturnValue({
+ original: originalModel,
+ modified: modifiedModel,
+ }),
+ });
+
+ const instance = editor.createDiffInstance({ ...defaultArguments, blobOriginalContent });
+
+ expect(editor.instances).toHaveLength(1);
+ expect(originalModel.dispose).not.toHaveBeenCalled();
+ expect(modifiedModel.dispose).not.toHaveBeenCalled();
+
+ instance.dispose();
+
+ expect(editor.instances).toHaveLength(0);
+ expect(originalModel.dispose).toHaveBeenCalled();
+ expect(modifiedModel.dispose).toHaveBeenCalled();
+ });
});
});
@@ -127,16 +254,14 @@ describe('Base editor', () => {
editorEl2 = document.getElementById('editor2');
inst1Args = {
el: editorEl1,
- blobGlobalId,
};
inst2Args = {
el: editorEl2,
blobContent,
blobPath,
- blobGlobalId,
};
- editor = new Editor();
+ editor = new EditorLite();
instanceSpy = jest.spyOn(monacoEditor, 'create');
});
@@ -166,8 +291,20 @@ describe('Base editor', () => {
expect(model1).not.toEqual(model2);
});
+ it('does not create a new model if a model for the path & globalId combo already exists', () => {
+ const modelSpy = jest.spyOn(monacoEditor, 'createModel');
+ inst1 = editor.createInstance({ ...inst2Args, blobGlobalId });
+ inst2 = editor.createInstance({ ...inst2Args, el: editorEl1, blobGlobalId });
+
+ const model1 = inst1.getModel();
+ const model2 = inst2.getModel();
+
+ expect(modelSpy).toHaveBeenCalledTimes(1);
+ expect(model1).toBe(model2);
+ });
+
it('shares global editor options among all instances', () => {
- editor = new Editor({
+ editor = new EditorLite({
readOnly: true,
});
@@ -179,7 +316,7 @@ describe('Base editor', () => {
});
it('allows overriding editor options on the instance level', () => {
- editor = new Editor({
+ editor = new EditorLite({
readOnly: true,
});
inst1 = editor.createInstance({
@@ -200,6 +337,7 @@ describe('Base editor', () => {
expect(monacoEditor.getModels()).toHaveLength(2);
inst1.dispose();
+
expect(inst1.getModel()).toBe(null);
expect(inst2.getModel()).not.toBe(null);
expect(editor.instances).toHaveLength(1);
@@ -402,19 +540,20 @@ describe('Base editor', () => {
el: editorEl,
blobPath,
blobContent,
- blobGlobalId,
extensions,
});
};
beforeEach(() => {
- editorExtensionSpy = jest.spyOn(Editor, 'pushToImportsArray').mockImplementation((arr) => {
- arr.push(
- Promise.resolve({
- default: {},
- }),
- );
- });
+ editorExtensionSpy = jest
+ .spyOn(EditorLite, 'pushToImportsArray')
+ .mockImplementation((arr) => {
+ arr.push(
+ Promise.resolve({
+ default: {},
+ }),
+ );
+ });
});
it.each([undefined, [], [''], ''])(
@@ -446,15 +585,20 @@ describe('Base editor', () => {
expect(editorExtensionSpy).toHaveBeenCalledWith(expect.any(Array), expectation);
});
- it('emits editor-ready event after all extensions were applied', async () => {
+ it('emits EDITOR_READY_EVENT event after all extensions were applied', async () => {
const calls = [];
const eventSpy = jest.fn().mockImplementation(() => {
calls.push('event');
});
- const useSpy = jest.spyOn(editor, 'use').mockImplementation(() => {
+ const useSpy = jest.fn().mockImplementation(() => {
calls.push('use');
});
- editorEl.addEventListener('editor-ready', eventSpy);
+ jest.spyOn(EditorLite, 'convertMonacoToELInstance').mockImplementation((inst) => {
+ const decoratedInstance = inst;
+ decoratedInstance.use = useSpy;
+ return decoratedInstance;
+ });
+ editorEl.addEventListener(EDITOR_READY_EVENT, eventSpy);
instance = instanceConstructor('foo, bar');
await waitForPromises();
expect(useSpy.mock.calls).toHaveLength(2);
@@ -487,12 +631,6 @@ describe('Base editor', () => {
expect(inst1.alpha()).toEqual(alphaRes);
expect(inst2.alpha()).toEqual(alphaRes);
});
-
- it('extends specific instance if it has been passed', () => {
- editor.use(AlphaExt, inst2);
- expect(inst1.alpha).toBeUndefined();
- expect(inst2.alpha()).toEqual(alphaRes);
- });
});
});
@@ -526,7 +664,7 @@ describe('Base editor', () => {
it('sets default syntax highlighting theme', () => {
const expectedTheme = themes.find((t) => t.name === DEFAULT_THEME);
- editor = new Editor();
+ editor = new EditorLite();
expect(themeDefineSpy).toHaveBeenCalledWith(DEFAULT_THEME, expectedTheme.data);
expect(themeSetSpy).toHaveBeenCalledWith(DEFAULT_THEME);
@@ -538,7 +676,7 @@ describe('Base editor', () => {
expect(expectedTheme.name).not.toBe(DEFAULT_THEME);
window.gon.user_color_scheme = expectedTheme.name;
- editor = new Editor();
+ editor = new EditorLite();
expect(themeDefineSpy).toHaveBeenCalledWith(expectedTheme.name, expectedTheme.data);
expect(themeSetSpy).toHaveBeenCalledWith(expectedTheme.name);
@@ -549,7 +687,7 @@ describe('Base editor', () => {
const nonExistentTheme = { name };
window.gon.user_color_scheme = nonExistentTheme.name;
- editor = new Editor();
+ editor = new EditorLite();
expect(themeDefineSpy).not.toHaveBeenCalled();
expect(themeSetSpy).toHaveBeenCalledWith(DEFAULT_THEME);
diff --git a/spec/frontend/environments/environment_actions_spec.js b/spec/frontend/environments/environment_actions_spec.js
index 875a01c07ea..279b275eb58 100644
--- a/spec/frontend/environments/environment_actions_spec.js
+++ b/spec/frontend/environments/environment_actions_spec.js
@@ -1,6 +1,6 @@
import { shallowMount, mount } from '@vue/test-utils';
-import { TEST_HOST } from 'helpers/test_constants';
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlIcon } from '@gitlab/ui';
+import { TEST_HOST } from 'helpers/test_constants';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import eventHub from '~/environments/event_hub';
import EnvironmentActions from '~/environments/components/environment_actions.vue';
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 da12237b1d9..d41233a6b72 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -5,8 +5,8 @@ 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';
import { trackErrorListViewsOptions, trackErrorStatusUpdateOptions } from '~/error_tracking/utils';
-import errorsList from './list_mock.json';
import Tracking from '~/tracking';
+import errorsList from './list_mock.json';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
index a754c682356..e66e37a4ae6 100644
--- a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
@@ -75,6 +75,8 @@ describe('Edit feature flag form', () => {
});
const findAlert = () => wrapper.find(GlAlert);
+ const findWarningGlAlert = () =>
+ wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'warning');
it('should display the iid', () => {
expect(wrapper.find('h3').text()).toContain('^5');
@@ -88,7 +90,7 @@ describe('Edit feature flag form', () => {
expect(wrapper.find(GlToggle).props('value')).toBe(true);
});
- it('should not alert users that feature flags are changing soon', () => {
+ it('should alert users the flag is read only', () => {
expect(findAlert().text()).toContain('GitLab is moving to a new way of managing feature flags');
});
@@ -96,8 +98,9 @@ describe('Edit feature flag form', () => {
it('should render the error', () => {
store.dispatch('receiveUpdateFeatureFlagError', { message: ['The name is required'] });
return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.alert-danger').exists()).toEqual(true);
- expect(wrapper.find('.alert-danger').text()).toContain('The name is required');
+ const warningGlAlert = findWarningGlAlert();
+ expect(warningGlAlert.at(1).exists()).toEqual(true);
+ expect(warningGlAlert.at(1).text()).toContain('The name is required');
});
});
});
diff --git a/spec/frontend/feature_flags/components/form_spec.js b/spec/frontend/feature_flags/components/form_spec.js
index 3a057aedde9..6c9ccba5181 100644
--- a/spec/frontend/feature_flags/components/form_spec.js
+++ b/spec/frontend/feature_flags/components/form_spec.js
@@ -1,6 +1,7 @@
import { uniqueId } from 'lodash';
import { shallowMount } from '@vue/test-utils';
-import { GlFormTextarea, GlFormCheckbox, GlButton } from '@gitlab/ui';
+import { GlFormTextarea, GlFormCheckbox, GlButton, GlToggle } from '@gitlab/ui';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import Api from '~/api';
import Form from '~/feature_flags/components/form.vue';
import EnvironmentsDropdown from '~/feature_flags/components/environments_dropdown.vue';
@@ -14,7 +15,6 @@ import {
NEW_VERSION_FLAG,
} from '~/feature_flags/constants';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
-import ToggleButton from '~/vue_shared/components/toggle_button.vue';
import { featureFlag, userList, allUsersStrategy } from '../mock_data';
jest.mock('~/api.js');
@@ -35,14 +35,19 @@ describe('feature flag form', () => {
},
};
+ const findAddNewScopeRow = () => wrapper.findByTestId('add-new-scope');
+ const findGlToggle = () => wrapper.find(GlToggle);
+
const factory = (props = {}, provide = {}) => {
- wrapper = shallowMount(Form, {
- propsData: { ...requiredProps, ...props },
- provide: {
- ...requiredInjections,
- ...provide,
- },
- });
+ wrapper = extendedWrapper(
+ shallowMount(Form, {
+ propsData: { ...requiredProps, ...props },
+ provide: {
+ ...requiredInjections,
+ ...provide,
+ },
+ }),
+ );
};
beforeEach(() => {
@@ -102,13 +107,13 @@ describe('feature flag form', () => {
});
it('should render scopes table with a new row ', () => {
- expect(wrapper.find('.js-add-new-scope').exists()).toBe(true);
+ expect(findAddNewScopeRow().exists()).toBe(true);
});
describe('status toggle', () => {
describe('without filled text input', () => {
it('should add a new scope with the text value empty and the status', () => {
- wrapper.find(ToggleButton).vm.$emit('change', true);
+ findGlToggle().vm.$emit('change', true);
expect(wrapper.vm.formScopes).toHaveLength(1);
expect(wrapper.vm.formScopes[0].active).toEqual(true);
@@ -121,7 +126,7 @@ describe('feature flag form', () => {
it('should be disabled if the feature flag is not active', (done) => {
wrapper.setProps({ active: false });
wrapper.vm.$nextTick(() => {
- expect(wrapper.find(ToggleButton).props('disabledInput')).toBe(true);
+ expect(findGlToggle().props('disabled')).toBe(true);
done();
});
});
@@ -166,11 +171,11 @@ describe('feature flag form', () => {
describe('scopes', () => {
it('should be possible to remove a scope', () => {
- expect(wrapper.find('.js-feature-flag-delete').exists()).toEqual(true);
+ expect(wrapper.findByTestId('feature-flag-delete').exists()).toEqual(true);
});
it('renders empty row to add a new scope', () => {
- expect(wrapper.find('.js-add-new-scope').exists()).toEqual(true);
+ expect(findAddNewScopeRow().exists()).toEqual(true);
});
it('renders the user id checkbox', () => {
@@ -186,7 +191,7 @@ describe('feature flag form', () => {
describe('update scope', () => {
describe('on click on toggle', () => {
it('should update the scope', () => {
- wrapper.find(ToggleButton).vm.$emit('change', false);
+ findGlToggle().vm.$emit('change', false);
expect(wrapper.vm.formScopes[0].active).toBe(false);
});
@@ -195,7 +200,7 @@ describe('feature flag form', () => {
wrapper.setProps({ active: false });
wrapper.vm.$nextTick(() => {
- expect(wrapper.find(ToggleButton).props('disabledInput')).toBe(true);
+ expect(findGlToggle().props('disabled')).toBe(true);
done();
});
});
@@ -294,7 +299,7 @@ describe('feature flag form', () => {
const row = wrapper.findAll('.gl-responsive-table-row').at(2);
expect(row.find(EnvironmentsDropdown).vm.disabled).toBe(true);
- expect(row.find(ToggleButton).vm.disabledInput).toBe(true);
+ expect(row.find(GlToggle).props('disabled')).toBe(true);
expect(row.find('.js-delete-scope').exists()).toBe(false);
});
});
@@ -347,10 +352,10 @@ describe('feature flag form', () => {
return wrapper.vm.$nextTick();
})
.then(() => {
- wrapper.find('.js-add-new-scope').find(ToggleButton).vm.$emit('change', true);
+ findAddNewScopeRow().find(GlToggle).vm.$emit('change', true);
})
.then(() => {
- wrapper.find(ToggleButton).vm.$emit('change', true);
+ findGlToggle().vm.$emit('change', true);
return wrapper.vm.$nextTick();
})
diff --git a/spec/frontend/feature_flags/components/new_feature_flag_spec.js b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
index e317ac4b092..2dfcdf201fb 100644
--- a/spec/frontend/feature_flags/components/new_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/new_feature_flag_spec.js
@@ -41,6 +41,9 @@ describe('New feature flag form', () => {
});
};
+ const findWarningGlAlert = () =>
+ wrapper.findAll(GlAlert).filter((c) => c.props('variant') === 'warning');
+
beforeEach(() => {
factory();
});
@@ -53,8 +56,9 @@ describe('New feature flag form', () => {
it('should render the error', () => {
store.dispatch('receiveCreateFeatureFlagError', { message: ['The name is required'] });
return wrapper.vm.$nextTick(() => {
- expect(wrapper.find('.alert').exists()).toEqual(true);
- expect(wrapper.find('.alert').text()).toContain('The name is required');
+ const warningGlAlert = findWarningGlAlert();
+ expect(warningGlAlert.at(0).exists()).toBe(true);
+ expect(warningGlAlert.at(0).text()).toContain('The name is required');
});
});
});
@@ -81,10 +85,6 @@ describe('New feature flag form', () => {
expect(wrapper.find(Form).props('scopes')).toContainEqual(defaultScope);
});
- it('should not alert users that feature flags are changing soon', () => {
- expect(wrapper.find(GlAlert).exists()).toBe(false);
- });
-
it('has an all users strategy by default', () => {
const strategies = wrapper.find(Form).props('strategies');
diff --git a/spec/frontend/filtered_search/recent_searches_root_spec.js b/spec/frontend/filtered_search/recent_searches_root_spec.js
index 6bb9e68d591..fa3267c98a1 100644
--- a/spec/frontend/filtered_search/recent_searches_root_spec.js
+++ b/spec/frontend/filtered_search/recent_searches_root_spec.js
@@ -1,32 +1,51 @@
-import Vue from 'vue';
+import { setHTMLFixture } from 'helpers/fixtures';
import RecentSearchesRoot from '~/filtered_search/recent_searches_root';
-jest.mock('vue');
+const containerId = 'test-container';
+const dropdownElementId = 'test-dropdown-element';
describe('RecentSearchesRoot', () => {
describe('render', () => {
- let recentSearchesRoot;
- let data;
- let template;
+ let recentSearchesRootMockInstance;
+ let vm;
+ let containerEl;
beforeEach(() => {
- recentSearchesRoot = {
+ setHTMLFixture(`
+ <div id="${containerId}">
+ <div id="${dropdownElementId}"></div>
+ </div>
+ `);
+
+ containerEl = document.getElementById(containerId);
+
+ recentSearchesRootMockInstance = {
store: {
- state: 'state',
+ state: {
+ recentSearches: ['foo', 'bar', 'qux'],
+ isLocalStorageAvailable: true,
+ allowedKeys: ['test'],
+ },
},
+ wrapperElement: document.getElementById(dropdownElementId),
};
- Vue.mockImplementation((options) => {
- ({ data, template } = options);
- });
+ RecentSearchesRoot.prototype.render.call(recentSearchesRootMockInstance);
+ vm = recentSearchesRootMockInstance.vm;
- RecentSearchesRoot.prototype.render.call(recentSearchesRoot);
+ return vm.$nextTick();
});
- it('should instantiate Vue', () => {
- expect(Vue).toHaveBeenCalled();
- expect(data()).toBe(recentSearchesRoot.store.state);
- expect(template).toContain(':is-local-storage-available="isLocalStorageAvailable"');
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render the recent searches', () => {
+ const { recentSearches } = recentSearchesRootMockInstance.store.state;
+
+ recentSearches.forEach((recentSearch) => {
+ expect(containerEl.textContent).toContain(recentSearch);
+ });
});
});
});
diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js
index b74e4ac45cf..5b1d397ec0b 100644
--- a/spec/frontend/frequent_items/components/app_spec.js
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -8,8 +8,8 @@ import appComponent from '~/frequent_items/components/app.vue';
import eventHub from '~/frequent_items/event_hub';
import { FREQUENT_ITEMS, HOUR_IN_MS } from '~/frequent_items/constants';
import { getTopFrequentItems } from '~/frequent_items/utils';
-import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data';
import { createStore } from '~/frequent_items/store';
+import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data';
useLocalStorageSpy();
diff --git a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
index cdd8b127676..eb6ed44e840 100644
--- a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
@@ -2,7 +2,6 @@ import { shallowMount } from '@vue/test-utils';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import searchComponent from '~/frequent_items/components/frequent_items_search_input.vue';
import { createStore } from '~/frequent_items/store';
-import eventHub from '~/frequent_items/event_hub';
describe('FrequentItemsSearchInputComponent', () => {
let wrapper;
@@ -45,39 +44,6 @@ describe('FrequentItemsSearchInputComponent', () => {
});
});
- describe('mounted', () => {
- it('should listen `dropdownOpen` event', (done) => {
- jest.spyOn(eventHub, '$on').mockImplementation(() => {});
- const vmX = createComponent().vm;
-
- vmX.$nextTick(() => {
- expect(eventHub.$on).toHaveBeenCalledWith(
- `${vmX.namespace}-dropdownOpen`,
- expect.any(Function),
- );
- done();
- });
- });
- });
-
- describe('beforeDestroy', () => {
- it('should unbind event listeners on eventHub', (done) => {
- const vmX = createComponent().vm;
- jest.spyOn(eventHub, '$off').mockImplementation(() => {});
-
- vmX.$mount();
- vmX.$destroy();
-
- vmX.$nextTick(() => {
- expect(eventHub.$off).toHaveBeenCalledWith(
- `${vmX.namespace}-dropdownOpen`,
- expect.any(Function),
- );
- done();
- });
- });
- });
-
describe('template', () => {
it('should render component element', () => {
expect(wrapper.classes()).toContain('search-input-container');
diff --git a/spec/frontend/frequent_items/store/actions_spec.js b/spec/frontend/frequent_items/store/actions_spec.js
index 351fde25f49..acea46eae60 100644
--- a/spec/frontend/frequent_items/store/actions_spec.js
+++ b/spec/frontend/frequent_items/store/actions_spec.js
@@ -1,5 +1,5 @@
-import testAction from 'helpers/vuex_action_helper';
import MockAdapter from 'axios-mock-adapter';
+import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import AccessorUtilities from '~/lib/utils/accessor';
import * as actions from '~/frequent_items/store/actions';
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index c2ff66f6afc..aefd465532a 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -1,16 +1,13 @@
/* eslint no-param-reassign: "off" */
import $ from 'jquery';
+import MockAdapter from 'axios-mock-adapter';
import { emojiFixtureMap, initEmojiMock, describeEmojiFields } from 'helpers/emoji';
import '~/lib/utils/jquery_at_who';
import GfmAutoComplete, { membersBeforeSave } from 'ee_else_ce/gfm_auto_complete';
-
import { TEST_HOST } from 'helpers/test_constants';
import { getJSONFixture } from 'helpers/fixtures';
-
import waitForPromises from 'helpers/wait_for_promises';
-
-import MockAdapter from 'axios-mock-adapter';
import AjaxCache from '~/lib/utils/ajax_cache';
import axios from '~/lib/utils/axios_utils';
@@ -493,7 +490,7 @@ describe('GfmAutoComplete', () => {
username: 'my-group',
avatarTag: '<div class="avatar rect-avatar center avatar-inline s26">M</div>',
title: 'My Group (2)',
- search: 'my-group My Group',
+ search: 'MyGroup my-group',
icon: '',
},
]);
@@ -506,7 +503,7 @@ describe('GfmAutoComplete', () => {
avatarTag:
'<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline center s26"/>',
title: 'My Group (2)',
- search: 'my-group My Group',
+ search: 'MyGroup my-group',
icon: '',
},
]);
@@ -519,7 +516,7 @@ describe('GfmAutoComplete', () => {
avatarTag:
'<img src="./group.jpg" alt="my-group" class="avatar rect-avatar avatar-inline center s26"/>',
title: 'My Group',
- search: 'my-group My Group',
+ search: 'MyGroup my-group',
icon:
'<svg class="s16 vertical-align-middle gl-ml-2"><use xlink:href="undefined#notifications-off" /></svg>',
},
@@ -537,7 +534,7 @@ describe('GfmAutoComplete', () => {
avatarTag:
'<img src="./users.jpg" alt="my-user" class="avatar avatar-inline center s26"/>',
title: 'My User',
- search: 'my-user My User',
+ search: 'MyUser my-user',
icon: '',
},
]);
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 e880f585daa..0fc4343ec3c 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
@@ -8,13 +8,13 @@ exports[`grafana integration component default state to match the default snapsh
<div
class="settings-header"
>
- <h3
- class="js-section-header h4"
+ <h4
+ class="js-section-header"
>
Grafana authentication
- </h3>
+ </h4>
<gl-button-stub
buttontextclasses=""
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 9244e4f331e..9680fa8bfd2 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -60,8 +60,8 @@ describe('AppComponent', () => {
beforeEach(() => {
mock = new AxiosMockAdapter(axios);
mock.onGet('/dashboard/groups.json').reply(200, mockGroups);
- Vue.component('group-folder', groupFolderComponent);
- Vue.component('group-item', groupItemComponent);
+ Vue.component('GroupFolder', groupFolderComponent);
+ Vue.component('GroupItem', groupItemComponent);
createShallowComponent();
getGroupsSpy = jest.spyOn(vm.service, 'getGroups');
diff --git a/spec/frontend/groups/components/group_folder_spec.js b/spec/frontend/groups/components/group_folder_spec.js
index a40fa9bece8..1d8e10479b6 100644
--- a/spec/frontend/groups/components/group_folder_spec.js
+++ b/spec/frontend/groups/components/group_folder_spec.js
@@ -19,7 +19,7 @@ describe('GroupFolderComponent', () => {
let vm;
beforeEach(() => {
- Vue.component('group-item', groupItemComponent);
+ Vue.component('GroupItem', groupItemComponent);
vm = createComponent();
vm.$mount();
diff --git a/spec/frontend/groups/components/group_item_spec.js b/spec/frontend/groups/components/group_item_spec.js
index d70ea709dee..c80f9a83fc4 100644
--- a/spec/frontend/groups/components/group_item_spec.js
+++ b/spec/frontend/groups/components/group_item_spec.js
@@ -20,7 +20,7 @@ describe('GroupItemComponent', () => {
let vm;
beforeEach(() => {
- Vue.component('group-folder', groupFolderComponent);
+ Vue.component('GroupFolder', groupFolderComponent);
vm = createComponent();
diff --git a/spec/frontend/groups/components/groups_spec.js b/spec/frontend/groups/components/groups_spec.js
index 6205400eb03..1fde8fa3d6e 100644
--- a/spec/frontend/groups/components/groups_spec.js
+++ b/spec/frontend/groups/components/groups_spec.js
@@ -22,8 +22,8 @@ describe('GroupsComponent', () => {
let vm;
beforeEach(() => {
- Vue.component('group-folder', groupFolderComponent);
- Vue.component('group-item', groupItemComponent);
+ Vue.component('GroupFolder', groupFolderComponent);
+ Vue.component('GroupItem', groupItemComponent);
vm = createComponent();
diff --git a/spec/frontend/groups/components/item_actions_spec.js b/spec/frontend/groups/components/item_actions_spec.js
index 9adbc9abe13..ffbdf9b1aa6 100644
--- a/spec/frontend/groups/components/item_actions_spec.js
+++ b/spec/frontend/groups/components/item_actions_spec.js
@@ -66,6 +66,22 @@ describe('ItemActions', () => {
});
});
+ it('emits `showLeaveGroupModal` event with the correct prefix if `action` prop is passed', () => {
+ const group = {
+ ...mockParentGroupItem,
+ canEdit: true,
+ canLeave: true,
+ };
+ createComponent({
+ group,
+ action: 'test',
+ });
+ jest.spyOn(eventHub, '$emit');
+ findLeaveGroupBtn().vm.$emit('click', { stopPropagation: () => {} });
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('testshowLeaveGroupModal', group, parentGroup);
+ });
+
it('does not render leave button if group can not be left', () => {
createComponent({
group: {
diff --git a/spec/frontend/groups/members/mock_data.js b/spec/frontend/groups/members/mock_data.js
deleted file mode 100644
index b84c9c6d446..00000000000
--- a/spec/frontend/groups/members/mock_data.js
+++ /dev/null
@@ -1,33 +0,0 @@
-export const membersJsonString =
- '[{"requested_at":null,"can_update":true,"can_remove":true,"can_override":false,"access_level":{"integer_value":50,"string_value":"Owner"},"source":{"id":323,"name":"My group / my subgroup","web_url":"http://127.0.0.1:3000/groups/my-group/my-subgroup"},"user":{"id":1,"name":"Administrator","username":"root","web_url":"http://127.0.0.1:3000/root","avatar_url":"https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80\u0026d=identicon","blocked":false,"two_factor_enabled":false},"id":524,"created_at":"2020-08-21T21:33:27.631Z","expires_at":null,"using_license":false,"group_sso":false,"group_managed_account":false}]';
-
-export const membersParsed = [
- {
- requestedAt: null,
- canUpdate: true,
- canRemove: true,
- canOverride: false,
- accessLevel: { integerValue: 50, stringValue: 'Owner' },
- source: {
- id: 323,
- name: 'My group / my subgroup',
- webUrl: 'http://127.0.0.1:3000/groups/my-group/my-subgroup',
- },
- user: {
- id: 1,
- name: 'Administrator',
- username: 'root',
- webUrl: 'http://127.0.0.1:3000/root',
- avatarUrl:
- 'https://www.gravatar.com/avatar/4816142ef496f956a277bedf1a40607b?s=80&d=identicon',
- blocked: false,
- twoFactorEnabled: false,
- },
- id: 524,
- createdAt: '2020-08-21T21:33:27.631Z',
- expiresAt: null,
- usingLicense: false,
- groupSso: false,
- groupManagedAccount: false,
- },
-];
diff --git a/spec/frontend/groups/members/utils_spec.js b/spec/frontend/groups/members/utils_spec.js
index 68945174e9d..0912e66e3e8 100644
--- a/spec/frontend/groups/members/utils_spec.js
+++ b/spec/frontend/groups/members/utils_spec.js
@@ -1,53 +1,14 @@
-import { membersJsonString, membersParsed } from './mock_data';
-import {
- parseDataAttributes,
- memberRequestFormatter,
- groupLinkRequestFormatter,
-} from '~/groups/members/utils';
+import { groupMemberRequestFormatter } from '~/groups/members/utils';
describe('group member utils', () => {
- describe('parseDataAttributes', () => {
- let el;
-
- beforeEach(() => {
- el = document.createElement('div');
- el.setAttribute('data-members', membersJsonString);
- el.setAttribute('data-group-id', '234');
- el.setAttribute('data-can-manage-members', 'true');
- });
-
- afterEach(() => {
- el = null;
- });
-
- it('correctly parses the data attributes', () => {
- expect(parseDataAttributes(el)).toEqual({
- members: membersParsed,
- sourceId: 234,
- canManageMembers: true,
- });
- });
- });
-
- describe('memberRequestFormatter', () => {
+ describe('groupMemberRequestFormatter', () => {
it('returns expected format', () => {
expect(
- memberRequestFormatter({
+ groupMemberRequestFormatter({
accessLevel: 50,
expires_at: '2020-10-16',
}),
).toEqual({ group_member: { access_level: 50, expires_at: '2020-10-16' } });
});
});
-
- describe('groupLinkRequestFormatter', () => {
- it('returns expected format', () => {
- expect(
- groupLinkRequestFormatter({
- accessLevel: 50,
- expires_at: '2020-10-16',
- }),
- ).toEqual({ group_link: { group_access: 50, expires_at: '2020-10-16' } });
- });
- });
});
diff --git a/spec/frontend/ide/components/activity_bar_spec.js b/spec/frontend/ide/components/activity_bar_spec.js
index 1a4b6ca0b71..6ea823ce4b5 100644
--- a/spec/frontend/ide/components/activity_bar_spec.js
+++ b/spec/frontend/ide/components/activity_bar_spec.js
@@ -9,6 +9,8 @@ describe('IDE activity bar', () => {
let vm;
let store;
+ const findChangesBadge = () => vm.$el.querySelector('.badge');
+
beforeEach(() => {
store = createStore();
@@ -69,4 +71,19 @@ describe('IDE activity bar', () => {
});
});
});
+
+ describe('changes badge', () => {
+ it('is rendered when files are staged', () => {
+ store.state.stagedFiles = [{ path: '/path/to/file' }];
+ vm.$mount();
+
+ expect(findChangesBadge()).toBeTruthy();
+ expect(findChangesBadge().textContent.trim()).toBe('1');
+ });
+
+ it('is not rendered when no changes are present', () => {
+ vm.$mount();
+ expect(findChangesBadge()).toBeFalsy();
+ });
+ });
});
diff --git a/spec/frontend/ide/components/commit_sidebar/actions_spec.js b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
index 91751bd34ea..596b5e81b2a 100644
--- a/spec/frontend/ide/components/commit_sidebar/actions_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/actions_spec.js
@@ -3,7 +3,10 @@ import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
import { projectData, branches } from 'jest/ide/mock_data';
import { createStore } from '~/ide/stores';
import commitActions from '~/ide/components/commit_sidebar/actions.vue';
-import consts from '~/ide/stores/modules/commit/constants';
+import {
+ COMMIT_TO_NEW_BRANCH,
+ COMMIT_TO_CURRENT_BRANCH,
+} from '~/ide/stores/modules/commit/constants';
const ACTION_UPDATE_COMMIT_ACTION = 'commit/updateCommitAction';
@@ -126,16 +129,16 @@ describe('IDE commit sidebar actions', () => {
it.each`
input | expectedOption
- ${{ currentBranchId: BRANCH_DEFAULT }} | ${consts.COMMIT_TO_NEW_BRANCH}
- ${{ currentBranchId: BRANCH_DEFAULT, emptyRepo: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
- ${{ currentBranchId: BRANCH_PROTECTED, hasMR: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
- ${{ currentBranchId: BRANCH_PROTECTED, hasMR: false }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
- ${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: true }} | ${consts.COMMIT_TO_NEW_BRANCH}
- ${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: false }} | ${consts.COMMIT_TO_NEW_BRANCH}
- ${{ currentBranchId: BRANCH_REGULAR, hasMR: true }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
- ${{ currentBranchId: BRANCH_REGULAR, hasMR: false }} | ${consts.COMMIT_TO_CURRENT_BRANCH}
- ${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: true }} | ${consts.COMMIT_TO_NEW_BRANCH}
- ${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: false }} | ${consts.COMMIT_TO_NEW_BRANCH}
+ ${{ currentBranchId: BRANCH_DEFAULT }} | ${COMMIT_TO_NEW_BRANCH}
+ ${{ currentBranchId: BRANCH_DEFAULT, emptyRepo: true }} | ${COMMIT_TO_CURRENT_BRANCH}
+ ${{ currentBranchId: BRANCH_PROTECTED, hasMR: true }} | ${COMMIT_TO_CURRENT_BRANCH}
+ ${{ currentBranchId: BRANCH_PROTECTED, hasMR: false }} | ${COMMIT_TO_CURRENT_BRANCH}
+ ${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: true }} | ${COMMIT_TO_NEW_BRANCH}
+ ${{ currentBranchId: BRANCH_PROTECTED_NO_ACCESS, hasMR: false }} | ${COMMIT_TO_NEW_BRANCH}
+ ${{ currentBranchId: BRANCH_REGULAR, hasMR: true }} | ${COMMIT_TO_CURRENT_BRANCH}
+ ${{ currentBranchId: BRANCH_REGULAR, hasMR: false }} | ${COMMIT_TO_CURRENT_BRANCH}
+ ${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: true }} | ${COMMIT_TO_NEW_BRANCH}
+ ${{ currentBranchId: BRANCH_REGULAR_NO_ACCESS, hasMR: false }} | ${COMMIT_TO_NEW_BRANCH}
`(
'with $input, it dispatches update commit action with $expectedOption',
({ input, expectedOption }) => {
diff --git a/spec/frontend/ide/components/commit_sidebar/form_spec.js b/spec/frontend/ide/components/commit_sidebar/form_spec.js
index abd7e3bb8fc..857d8f400a7 100644
--- a/spec/frontend/ide/components/commit_sidebar/form_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/form_spec.js
@@ -1,11 +1,14 @@
import Vue from 'vue';
-import { getByText } from '@testing-library/dom';
-import { createComponentWithStore } from 'helpers/vue_mount_component_helper';
+import { shallowMount } from '@vue/test-utils';
+import { GlModal } from '@gitlab/ui';
import { projectData } from 'jest/ide/mock_data';
+import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { createStore } from '~/ide/stores';
-import consts from '~/ide/stores/modules/commit/constants';
+import { COMMIT_TO_NEW_BRANCH } from '~/ide/stores/modules/commit/constants';
import CommitForm from '~/ide/components/commit_sidebar/form.vue';
+import CommitMessageField from '~/ide/components/commit_sidebar/message_field.vue';
import { leftSidebarViews } from '~/ide/constants';
import {
createCodeownersCommitError,
@@ -15,256 +18,283 @@ import {
} from '~/ide/lib/errors';
describe('IDE commit form', () => {
- const Component = Vue.extend(CommitForm);
- let vm;
+ let wrapper;
let store;
- const beginCommitButton = () => vm.$el.querySelector('[data-testid="begin-commit-button"]');
+ const createComponent = () => {
+ wrapper = shallowMount(CommitForm, {
+ store,
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal),
+ },
+ });
+ };
+
+ const setLastCommitMessage = (msg) => {
+ store.state.lastCommitMsg = msg;
+ };
+ const goToCommitView = () => {
+ store.state.currentActivityView = leftSidebarViews.commit.name;
+ };
+ const goToEditView = () => {
+ store.state.currentActivityView = leftSidebarViews.edit.name;
+ };
+ const findBeginCommitButton = () => wrapper.find('[data-testid="begin-commit-button"]');
+ const findBeginCommitButtonTooltip = () =>
+ wrapper.find('[data-testid="begin-commit-button-tooltip"]');
+ const findBeginCommitButtonData = () => ({
+ disabled: findBeginCommitButton().props('disabled'),
+ tooltip: getBinding(findBeginCommitButtonTooltip().element, 'gl-tooltip').value.title,
+ });
+ const findCommitButton = () => wrapper.find('[data-testid="commit-button"]');
+ const findCommitButtonTooltip = () => wrapper.find('[data-testid="commit-button-tooltip"]');
+ const findCommitButtonData = () => ({
+ disabled: findCommitButton().props('disabled'),
+ tooltip: getBinding(findCommitButtonTooltip().element, 'gl-tooltip').value.title,
+ });
+ const clickCommitButton = () => findCommitButton().vm.$emit('click');
+ const findForm = () => wrapper.find('form');
+ const submitForm = () => findForm().trigger('submit');
+ const findCommitMessageInput = () => wrapper.find(CommitMessageField);
+ const setCommitMessageInput = (val) => findCommitMessageInput().vm.$emit('input', val);
+ const findDiscardDraftButton = () => wrapper.find('[data-testid="discard-draft"]');
beforeEach(() => {
store = createStore();
- store.state.changedFiles.push('test');
+ store.state.stagedFiles.push('test');
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
- Vue.set(store.state.projects, 'abcproject', { ...projectData });
-
- vm = createComponentWithStore(Component, store).$mount();
+ Vue.set(store.state.projects, 'abcproject', {
+ ...projectData,
+ userPermissions: { pushCode: true },
+ });
});
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
- it('enables begin commit button when there are changes', () => {
- expect(beginCommitButton()).not.toHaveAttr('disabled');
- });
+ // Notes:
+ // - When there are no changes, there is no commit button so there's nothing to test :)
+ describe.each`
+ desc | stagedFiles | userPermissions | viewFn | buttonFn | disabled | tooltip
+ ${'when there are no changes'} | ${[]} | ${{ pushCode: true }} | ${goToEditView} | ${findBeginCommitButtonData} | ${true} | ${''}
+ ${'when there are changes'} | ${['test']} | ${{ pushCode: true }} | ${goToEditView} | ${findBeginCommitButtonData} | ${false} | ${''}
+ ${'when there are changes'} | ${['test']} | ${{ pushCode: true }} | ${goToCommitView} | ${findCommitButtonData} | ${false} | ${''}
+ ${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToEditView} | ${findBeginCommitButtonData} | ${true} | ${CommitForm.MSG_CANNOT_PUSH_CODE}
+ ${'when user cannot push'} | ${['test']} | ${{ pushCode: false }} | ${goToCommitView} | ${findCommitButtonData} | ${true} | ${CommitForm.MSG_CANNOT_PUSH_CODE}
+ `('$desc', ({ stagedFiles, userPermissions, viewFn, buttonFn, disabled, tooltip }) => {
+ beforeEach(async () => {
+ store.state.stagedFiles = stagedFiles;
+ store.state.projects.abcproject.userPermissions = userPermissions;
+
+ createComponent();
+ });
+
+ it(`at view=${viewFn.name}, ${buttonFn.name} has disabled=${disabled} tooltip=${tooltip}`, async () => {
+ viewFn();
- it('disables begin commit button when there are no changes', async () => {
- store.state.changedFiles = [];
- await vm.$nextTick();
+ await wrapper.vm.$nextTick();
- expect(beginCommitButton()).toHaveAttr('disabled');
+ expect(buttonFn()).toEqual({
+ disabled,
+ tooltip,
+ });
+ });
});
- describe('compact', () => {
- beforeEach(() => {
- vm.isCompact = true;
+ describe('on edit tab', () => {
+ beforeEach(async () => {
+ // Test that we react to switching to compact view.
+ goToCommitView();
+
+ createComponent();
+
+ goToEditView();
- return vm.$nextTick();
+ await wrapper.vm.$nextTick();
});
it('renders commit button in compact mode', () => {
- expect(beginCommitButton()).not.toBeNull();
- expect(beginCommitButton().textContent).toContain('Commit');
+ expect(findBeginCommitButton().exists()).toBe(true);
+ expect(findBeginCommitButton().text()).toBe('Commit…');
});
it('does not render form', () => {
- expect(vm.$el.querySelector('form')).toBeNull();
+ expect(findForm().exists()).toBe(false);
});
it('renders overview text', () => {
- vm.$store.state.stagedFiles.push('test');
-
- return vm.$nextTick(() => {
- expect(vm.$el.querySelector('p').textContent).toContain('1 changed file');
- });
+ expect(wrapper.find('p').text()).toBe('1 changed file');
});
- it('shows form when clicking commit button', () => {
- beginCommitButton().click();
+ it('when begin commit button is clicked, shows form', async () => {
+ findBeginCommitButton().vm.$emit('click');
- return vm.$nextTick(() => {
- expect(vm.$el.querySelector('form')).not.toBeNull();
- });
- });
+ await wrapper.vm.$nextTick();
- it('toggles activity bar view when clicking commit button', () => {
- beginCommitButton().click();
-
- return vm.$nextTick(() => {
- expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
- });
+ expect(findForm().exists()).toBe(true);
});
- it('collapses if lastCommitMsg is set to empty and current view is not commit view', async () => {
- store.state.lastCommitMsg = 'abc';
- store.state.currentActivityView = leftSidebarViews.edit.name;
- await vm.$nextTick();
-
- // if commit message is set, form is uncollapsed
- expect(vm.isCompact).toBe(false);
+ it('when begin commit button is clicked, sets activity view', async () => {
+ findBeginCommitButton().vm.$emit('click');
- store.state.lastCommitMsg = '';
- await vm.$nextTick();
+ await wrapper.vm.$nextTick();
- // collapsed when set to empty
- expect(vm.isCompact).toBe(true);
+ expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
});
- it('collapses if in commit view but there are no changes and vice versa', async () => {
- store.state.currentActivityView = leftSidebarViews.commit.name;
- await vm.$nextTick();
+ it('collapses if lastCommitMsg is set to empty and current view is not commit view', async () => {
+ // Test that it expands when lastCommitMsg is set
+ setLastCommitMessage('test');
+ goToEditView();
- // expanded by default if there are changes
- expect(vm.isCompact).toBe(false);
+ await wrapper.vm.$nextTick();
- store.state.changedFiles = [];
- await vm.$nextTick();
+ expect(findForm().exists()).toBe(true);
- expect(vm.isCompact).toBe(true);
+ // Now test that it collapses when lastCommitMsg is cleared
+ setLastCommitMessage('');
- store.state.changedFiles.push('test');
- await vm.$nextTick();
+ await wrapper.vm.$nextTick();
- // uncollapsed once again
- expect(vm.isCompact).toBe(false);
+ expect(findForm().exists()).toBe(false);
});
+ });
- it('collapses if switched from commit view to edit view and vice versa', async () => {
- store.state.currentActivityView = leftSidebarViews.edit.name;
- await vm.$nextTick();
-
- expect(vm.isCompact).toBe(true);
+ describe('on commit tab when window height is less than MAX_WINDOW_HEIGHT', () => {
+ let oldHeight;
- store.state.currentActivityView = leftSidebarViews.commit.name;
- await vm.$nextTick();
+ beforeEach(async () => {
+ oldHeight = window.innerHeight;
+ window.innerHeight = 700;
- expect(vm.isCompact).toBe(false);
+ createComponent();
- store.state.currentActivityView = leftSidebarViews.edit.name;
- await vm.$nextTick();
+ goToCommitView();
- expect(vm.isCompact).toBe(true);
+ await wrapper.vm.$nextTick();
});
- describe('when window height is less than MAX_WINDOW_HEIGHT', () => {
- let oldHeight;
-
- beforeEach(() => {
- oldHeight = window.innerHeight;
- window.innerHeight = 700;
- });
+ afterEach(() => {
+ window.innerHeight = oldHeight;
+ });
- afterEach(() => {
- window.innerHeight = oldHeight;
- });
+ it('stays collapsed if changes are added or removed', async () => {
+ expect(findForm().exists()).toBe(false);
- it('stays collapsed when switching from edit view to commit view and back', async () => {
- store.state.currentActivityView = leftSidebarViews.edit.name;
- await vm.$nextTick();
+ store.state.stagedFiles = [];
+ await wrapper.vm.$nextTick();
- expect(vm.isCompact).toBe(true);
+ expect(findForm().exists()).toBe(false);
- store.state.currentActivityView = leftSidebarViews.commit.name;
- await vm.$nextTick();
+ store.state.stagedFiles.push('test');
+ await wrapper.vm.$nextTick();
- expect(vm.isCompact).toBe(true);
+ expect(findForm().exists()).toBe(false);
+ });
+ });
- store.state.currentActivityView = leftSidebarViews.edit.name;
- await vm.$nextTick();
+ describe('on commit tab', () => {
+ beforeEach(async () => {
+ // Test that the component reacts to switching to full view
+ goToEditView();
- expect(vm.isCompact).toBe(true);
- });
+ createComponent();
- it('stays uncollapsed if changes are added or removed', async () => {
- store.state.currentActivityView = leftSidebarViews.commit.name;
- await vm.$nextTick();
+ goToCommitView();
- expect(vm.isCompact).toBe(true);
+ await wrapper.vm.$nextTick();
+ });
- store.state.changedFiles = [];
- await vm.$nextTick();
+ it('shows form', () => {
+ expect(findForm().exists()).toBe(true);
+ });
- expect(vm.isCompact).toBe(true);
+ it('hides begin commit button', () => {
+ expect(findBeginCommitButton().exists()).toBe(false);
+ });
- store.state.changedFiles.push('test');
- await vm.$nextTick();
+ describe('when no changed files', () => {
+ beforeEach(async () => {
+ store.state.stagedFiles = [];
+ await wrapper.vm.$nextTick();
+ });
- expect(vm.isCompact).toBe(true);
+ it('hides form', () => {
+ expect(findForm().exists()).toBe(false);
});
- it('uncollapses when clicked on Commit button in the edit view', async () => {
- store.state.currentActivityView = leftSidebarViews.edit.name;
- beginCommitButton().click();
- await waitForPromises();
+ it('expands again when staged files are added', async () => {
+ store.state.stagedFiles.push('test');
+ await wrapper.vm.$nextTick();
- expect(vm.isCompact).toBe(false);
+ expect(findForm().exists()).toBe(true);
});
});
- });
- describe('full', () => {
- beforeEach(() => {
- vm.isCompact = false;
+ it('updates commitMessage in store on input', async () => {
+ setCommitMessageInput('testing commit message');
+
+ await wrapper.vm.$nextTick();
- return vm.$nextTick();
+ expect(store.state.commit.commitMessage).toBe('testing commit message');
});
- it('updates commitMessage in store on input', () => {
- const textarea = vm.$el.querySelector('textarea');
+ describe('discard draft button', () => {
+ it('hidden when commitMessage is empty', () => {
+ expect(findDiscardDraftButton().exists()).toBe(false);
+ });
+
+ it('resets commitMessage when clicking discard button', async () => {
+ setCommitMessageInput('testing commit message');
- textarea.value = 'testing commit message';
+ await wrapper.vm.$nextTick();
- textarea.dispatchEvent(new Event('input'));
+ expect(findCommitMessageInput().props('text')).toBe('testing commit message');
- return vm.$nextTick().then(() => {
- expect(vm.$store.state.commit.commitMessage).toBe('testing commit message');
- });
- });
+ // Test that commitMessage is cleared on click
+ findDiscardDraftButton().vm.$emit('click');
- it('updating currentActivityView not to commit view sets compact mode', () => {
- store.state.currentActivityView = 'a';
+ await wrapper.vm.$nextTick();
- return vm.$nextTick(() => {
- expect(vm.isCompact).toBe(true);
+ expect(findCommitMessageInput().props('text')).toBe('');
});
});
- it('always opens itself in full view current activity view is not commit view when clicking commit button', () => {
- beginCommitButton().click();
+ describe('when submitting', () => {
+ beforeEach(async () => {
+ goToEditView();
- return vm.$nextTick(() => {
- expect(store.state.currentActivityView).toBe(leftSidebarViews.commit.name);
- expect(vm.isCompact).toBe(false);
- });
- });
+ createComponent();
- describe('discard draft button', () => {
- it('hidden when commitMessage is empty', () => {
- expect(vm.$el.querySelector('.btn-default').textContent).toContain('Collapse');
- });
+ goToCommitView();
+
+ await wrapper.vm.$nextTick();
+
+ setCommitMessageInput('testing commit message');
- it('resets commitMessage when clicking discard button', () => {
- vm.$store.state.commit.commitMessage = 'testing commit message';
-
- return vm
- .$nextTick()
- .then(() => {
- vm.$el.querySelector('.btn-default').click();
- })
- .then(() => vm.$nextTick())
- .then(() => {
- expect(vm.$store.state.commit.commitMessage).not.toBe('testing commit message');
- });
+ await wrapper.vm.$nextTick();
+
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
});
- });
- describe('when submitting', () => {
- beforeEach(() => {
- jest.spyOn(vm, 'commitChanges');
+ it.each([clickCommitButton, submitForm])('when %p, commits changes', (fn) => {
+ fn();
- vm.$store.state.stagedFiles.push('test');
- vm.$store.state.commit.commitMessage = 'testing commit message';
+ expect(store.dispatch).toHaveBeenCalledWith('commit/commitChanges', undefined);
});
- it('calls commitChanges', () => {
- vm.commitChanges.mockResolvedValue({ success: true });
+ it('when cannot push code, submitting does nothing', async () => {
+ store.state.projects.abcproject.userPermissions.pushCode = false;
+ await wrapper.vm.$nextTick();
- return vm.$nextTick().then(() => {
- vm.$el.querySelector('.btn-success').click();
+ submitForm();
- expect(vm.commitChanges).toHaveBeenCalled();
- });
+ expect(store.dispatch).not.toHaveBeenCalled();
});
it.each`
@@ -272,31 +302,32 @@ describe('IDE commit form', () => {
${() => createCodeownersCommitError('test message')} | ${{ actionPrimary: { text: 'Create new branch' } }}
${createUnexpectedCommitError} | ${{ actionPrimary: null }}
`('opens error modal if commitError with $error', async ({ createError, props }) => {
- jest.spyOn(vm.$refs.commitErrorModal, 'show');
+ const modal = wrapper.find(GlModal);
+ modal.vm.show = jest.fn();
const error = createError();
store.state.commit.commitError = error;
- await vm.$nextTick();
+ await wrapper.vm.$nextTick();
- expect(vm.$refs.commitErrorModal.show).toHaveBeenCalled();
- expect(vm.$refs.commitErrorModal).toMatchObject({
+ expect(modal.vm.show).toHaveBeenCalled();
+ expect(modal.props()).toMatchObject({
actionCancel: { text: 'Cancel' },
...props,
});
// Because of the legacy 'mountComponent' approach here, the only way to
// test the text of the modal is by viewing the content of the modal added to the document.
- expect(document.body).toHaveText(error.messageHTML);
+ expect(modal.html()).toContain(error.messageHTML);
});
});
describe('with error modal with primary', () => {
beforeEach(() => {
- jest.spyOn(vm.$store, 'dispatch').mockReturnValue(Promise.resolve());
+ jest.spyOn(store, 'dispatch').mockResolvedValue();
});
const commitActions = [
- ['commit/updateCommitAction', consts.COMMIT_TO_NEW_BRANCH],
+ ['commit/updateCommitAction', COMMIT_TO_NEW_BRANCH],
['commit/commitChanges'],
];
@@ -310,27 +341,15 @@ describe('IDE commit form', () => {
async ({ commitError, expectedActions }) => {
store.state.commit.commitError = commitError('test message');
- await vm.$nextTick();
+ await wrapper.vm.$nextTick();
- getByText(document.body, 'Create new branch').click();
+ wrapper.find(GlModal).vm.$emit('ok');
await waitForPromises();
- expect(vm.$store.dispatch.mock.calls).toEqual(expectedActions);
+ expect(store.dispatch.mock.calls).toEqual(expectedActions);
},
);
});
});
-
- describe('commitButtonText', () => {
- it('returns commit text when staged files exist', () => {
- vm.$store.state.stagedFiles.push('testing');
-
- expect(vm.commitButtonText).toBe('Commit');
- });
-
- it('returns stage & commit text when staged files do not exist', () => {
- expect(vm.commitButtonText).toBe('Stage & Commit');
- });
- });
});
diff --git a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
index 50da64abbbe..f8ab7aa4d0f 100644
--- a/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
+++ b/spec/frontend/ide/components/commit_sidebar/new_merge_request_option_spec.js
@@ -4,7 +4,10 @@ import { projectData, branches } from 'jest/ide/mock_data';
import NewMergeRequestOption from '~/ide/components/commit_sidebar/new_merge_request_option.vue';
import { createStore } from '~/ide/stores';
import { PERMISSION_CREATE_MR } from '~/ide/constants';
-import consts from '~/ide/stores/modules/commit/constants';
+import {
+ COMMIT_TO_CURRENT_BRANCH,
+ COMMIT_TO_NEW_BRANCH,
+} from '~/ide/stores/modules/commit/constants';
describe('create new MR checkbox', () => {
let store;
@@ -27,8 +30,8 @@ describe('create new MR checkbox', () => {
vm = createComponentWithStore(Component, store);
vm.$store.state.commit.commitAction = createNewBranch
- ? consts.COMMIT_TO_NEW_BRANCH
- : consts.COMMIT_TO_CURRENT_BRANCH;
+ ? COMMIT_TO_NEW_BRANCH
+ : COMMIT_TO_CURRENT_BRANCH;
vm.$store.state.currentBranchId = currentBranchId;
diff --git a/spec/frontend/ide/components/ide_sidebar_nav_spec.js b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
index 6b4cb9bd03d..841e19532a1 100644
--- a/spec/frontend/ide/components/ide_sidebar_nav_spec.js
+++ b/spec/frontend/ide/components/ide_sidebar_nav_spec.js
@@ -3,6 +3,7 @@ import { GlIcon } from '@gitlab/ui';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import IdeSidebarNav from '~/ide/components/ide_sidebar_nav.vue';
import { SIDE_RIGHT, SIDE_LEFT } from '~/ide/constants';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
const TEST_TABS = [
{
@@ -74,7 +75,7 @@ describe('ide/components/ide_sidebar_nav', () => {
createComponent({ isOpen, side });
bsTooltipHide = jest.fn();
- wrapper.vm.$root.$on('bv::hide::tooltip', bsTooltipHide);
+ wrapper.vm.$root.$on(BV_HIDE_TOOLTIP, bsTooltipHide);
});
it('renders buttons', () => {
diff --git a/spec/frontend/ide/components/ide_spec.js b/spec/frontend/ide/components/ide_spec.js
index 805fa898611..ef8c55c8b1c 100644
--- a/spec/frontend/ide/components/ide_spec.js
+++ b/spec/frontend/ide/components/ide_spec.js
@@ -1,9 +1,10 @@
import Vuex from 'vuex';
import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { createStore } from '~/ide/stores';
import ErrorMessage from '~/ide/components/error_message.vue';
-import ide from '~/ide/components/ide.vue';
+import Ide from '~/ide/components/ide.vue';
import { file } from '../helpers';
import { projectData } from '../mock_data';
@@ -15,12 +16,12 @@ describe('WebIDE', () => {
let wrapper;
- function createComponent({ projData = emptyProjData, state = {} } = {}) {
+ const createComponent = ({ projData = emptyProjData, state = {} } = {}) => {
const store = createStore();
store.state.currentProjectId = 'abcproject';
store.state.currentBranchId = 'master';
- store.state.projects.abcproject = { ...projData };
+ store.state.projects.abcproject = projData && { ...projData };
store.state.trees['abcproject/master'] = {
tree: [],
loading: false,
@@ -29,11 +30,13 @@ describe('WebIDE', () => {
store.state[key] = state[key];
});
- return shallowMount(ide, {
+ wrapper = shallowMount(Ide, {
store,
localVue,
});
- }
+ };
+
+ const findAlert = () => wrapper.find(GlAlert);
afterEach(() => {
wrapper.destroy();
@@ -42,7 +45,7 @@ describe('WebIDE', () => {
describe('ide component, empty repo', () => {
beforeEach(() => {
- wrapper = createComponent({
+ createComponent({
projData: {
empty_repo: true,
},
@@ -63,7 +66,7 @@ describe('WebIDE', () => {
`(
'should error message exists=$exists when errorMessage=$errorMessage',
async ({ errorMessage, exists }) => {
- wrapper = createComponent({
+ createComponent({
state: {
errorMessage,
},
@@ -78,12 +81,12 @@ describe('WebIDE', () => {
describe('onBeforeUnload', () => {
it('returns undefined when no staged files or changed files', () => {
- wrapper = createComponent();
+ createComponent();
expect(wrapper.vm.onBeforeUnload()).toBe(undefined);
});
it('returns warning text when their are changed files', () => {
- wrapper = createComponent({
+ createComponent({
state: {
changedFiles: [file()],
},
@@ -93,7 +96,7 @@ describe('WebIDE', () => {
});
it('returns warning text when their are staged files', () => {
- wrapper = createComponent({
+ createComponent({
state: {
stagedFiles: [file()],
},
@@ -104,7 +107,7 @@ describe('WebIDE', () => {
it('updates event object', () => {
const event = {};
- wrapper = createComponent({
+ createComponent({
state: {
stagedFiles: [file()],
},
@@ -118,7 +121,7 @@ describe('WebIDE', () => {
describe('non-existent branch', () => {
it('does not render "New file" button for non-existent branch when repo is not empty', () => {
- wrapper = createComponent({
+ createComponent({
state: {
projects: {},
},
@@ -130,7 +133,7 @@ describe('WebIDE', () => {
describe('branch with files', () => {
beforeEach(() => {
- wrapper = createComponent({
+ createComponent({
projData: {
empty_repo: false,
},
@@ -142,4 +145,31 @@ describe('WebIDE', () => {
});
});
});
+
+ it('when user cannot push code, shows alert', () => {
+ createComponent({
+ projData: {
+ userPermissions: {
+ pushCode: false,
+ },
+ },
+ });
+
+ expect(findAlert().props()).toMatchObject({
+ dismissible: false,
+ });
+ expect(findAlert().text()).toBe(Ide.MSG_CANNOT_PUSH_CODE);
+ });
+
+ it.each`
+ desc | projData
+ ${'when user can push code'} | ${{ userPermissions: { pushCode: true } }}
+ ${'when project is not ready'} | ${null}
+ `('$desc, no alert is shown', ({ projData }) => {
+ createComponent({
+ projData,
+ });
+
+ expect(findAlert().exists()).toBe(false);
+ });
});
diff --git a/spec/frontend/ide/components/preview/navigator_spec.js b/spec/frontend/ide/components/preview/navigator_spec.js
index ba5ac3bbbea..e3272734755 100644
--- a/spec/frontend/ide/components/preview/navigator_spec.js
+++ b/spec/frontend/ide/components/preview/navigator_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import { TEST_HOST } from 'helpers/test_constants';
import { GlLoadingIcon } from '@gitlab/ui';
import { listen } from 'codesandbox-api';
+import { TEST_HOST } from 'helpers/test_constants';
import ClientsideNavigator from '~/ide/components/preview/navigator.vue';
jest.mock('codesandbox-api', () => ({
diff --git a/spec/frontend/ide/lib/decorations/controller_spec.js b/spec/frontend/ide/lib/decorations/controller_spec.js
index e9b7faaadfe..2790563fd6a 100644
--- a/spec/frontend/ide/lib/decorations/controller_spec.js
+++ b/spec/frontend/ide/lib/decorations/controller_spec.js
@@ -1,8 +1,8 @@
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';
+import { file } from '../../helpers';
describe('Multi-file editor library decorations controller', () => {
let editorInstance;
diff --git a/spec/frontend/ide/lib/editor_spec.js b/spec/frontend/ide/lib/editor_spec.js
index 12779c61dc3..b379d778966 100644
--- a/spec/frontend/ide/lib/editor_spec.js
+++ b/spec/frontend/ide/lib/editor_spec.js
@@ -7,6 +7,7 @@ import {
import Editor from '~/ide/lib/editor';
import { createStore } from '~/ide/stores';
import { defaultEditorOptions } from '~/ide/lib/editor_options';
+import { EDITOR_TYPE_DIFF } from '~/editor/constants';
import { file } from '../helpers';
describe('Multi-file editor library', () => {
@@ -125,7 +126,7 @@ describe('Multi-file editor library', () => {
});
it('sets original & modified when diff editor', () => {
- jest.spyOn(instance.instance, 'getEditorType').mockReturnValue('vs.editor.IDiffEditor');
+ jest.spyOn(instance.instance, 'getEditorType').mockReturnValue(EDITOR_TYPE_DIFF);
jest.spyOn(instance.instance, 'setModel').mockImplementation(() => {});
instance.attachModel(model);
diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js
index 036bc91cd11..d925e9a0844 100644
--- a/spec/frontend/ide/stores/actions_spec.js
+++ b/spec/frontend/ide/stores/actions_spec.js
@@ -20,8 +20,8 @@ import {
} from '~/ide/stores/actions';
import axios from '~/lib/utils/axios_utils';
import * as types from '~/ide/stores/mutation_types';
-import { file, createTriggerRenameAction, createTriggerChangeAction } from '../helpers';
import eventHub from '~/ide/eventhub';
+import { file, createTriggerRenameAction, createTriggerChangeAction } from '../helpers';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index 1787f9e9361..1289c1aec75 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -2,6 +2,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import * as getters from '~/ide/stores/getters';
import { createStore } from '~/ide/stores';
import { file } from '../helpers';
+import { DEFAULT_PERMISSIONS } from '../../../../app/assets/javascripts/ide/constants';
const TEST_PROJECT_ID = 'test_project';
@@ -386,7 +387,9 @@ describe('IDE store getters', () => {
describe('findProjectPermissions', () => {
it('returns false if project not found', () => {
- expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toEqual({});
+ expect(localStore.getters.findProjectPermissions(TEST_PROJECT_ID)).toEqual(
+ DEFAULT_PERMISSIONS,
+ );
});
it('finds permission in given project', () => {
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index 5be0e22a9fc..ac495a657a2 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -1,13 +1,16 @@
-import { file } from 'jest/ide/helpers';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { file } from 'jest/ide/helpers';
import testAction from 'helpers/vuex_action_helper';
import { visitUrl } from '~/lib/utils/url_utility';
import { createStore } from '~/ide/stores';
import service from '~/ide/services';
import { createRouter } from '~/ide/ide_router';
import eventHub from '~/ide/eventhub';
-import consts from '~/ide/stores/modules/commit/constants';
+import {
+ COMMIT_TO_CURRENT_BRANCH,
+ COMMIT_TO_NEW_BRANCH,
+} from '~/ide/stores/modules/commit/constants';
import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
import * as actions from '~/ide/stores/modules/commit/actions';
import { createUnexpectedCommitError } from '~/ide/lib/errors';
@@ -425,12 +428,12 @@ describe('IDE commit module actions', () => {
});
it('resets stores commit actions', (done) => {
- store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
store
.dispatch('commit/commitChanges')
.then(() => {
- expect(store.state.commit.commitAction).not.toBe(consts.COMMIT_TO_NEW_BRANCH);
+ expect(store.state.commit.commitAction).not.toBe(COMMIT_TO_NEW_BRANCH);
})
.then(done)
.catch(done.fail);
@@ -450,7 +453,7 @@ describe('IDE commit module actions', () => {
it('redirects to new merge request page', (done) => {
jest.spyOn(eventHub, '$on').mockImplementation();
- store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
store.state.commit.shouldCreateMR = true;
store
@@ -468,7 +471,7 @@ describe('IDE commit module actions', () => {
it('does not redirect to new merge request page when shouldCreateMR is not checked', (done) => {
jest.spyOn(eventHub, '$on').mockImplementation();
- store.state.commit.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
store.state.commit.shouldCreateMR = false;
store
@@ -483,7 +486,7 @@ describe('IDE commit module actions', () => {
it('does not redirect to merge request page if shouldCreateMR is checked, but branch is the default branch', async () => {
jest.spyOn(eventHub, '$on').mockImplementation();
- store.state.commit.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ store.state.commit.commitAction = COMMIT_TO_CURRENT_BRANCH;
store.state.commit.shouldCreateMR = true;
await store.dispatch('commit/commitChanges');
diff --git a/spec/frontend/ide/stores/modules/commit/getters_spec.js b/spec/frontend/ide/stores/modules/commit/getters_spec.js
index 66ed51dbd13..a97b7e49dfc 100644
--- a/spec/frontend/ide/stores/modules/commit/getters_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/getters_spec.js
@@ -1,6 +1,9 @@
import commitState from '~/ide/stores/modules/commit/state';
import * as getters from '~/ide/stores/modules/commit/getters';
-import consts from '~/ide/stores/modules/commit/constants';
+import {
+ COMMIT_TO_CURRENT_BRANCH,
+ COMMIT_TO_NEW_BRANCH,
+} from '~/ide/stores/modules/commit/constants';
describe('IDE commit module getters', () => {
let state;
@@ -147,13 +150,13 @@ describe('IDE commit module getters', () => {
describe('isCreatingNewBranch', () => {
it('returns false if NOT creating a new branch', () => {
- state.commitAction = consts.COMMIT_TO_CURRENT_BRANCH;
+ state.commitAction = COMMIT_TO_CURRENT_BRANCH;
expect(getters.isCreatingNewBranch(state)).toBeFalsy();
});
it('returns true if creating a new branch', () => {
- state.commitAction = consts.COMMIT_TO_NEW_BRANCH;
+ state.commitAction = COMMIT_TO_NEW_BRANCH;
expect(getters.isCreatingNewBranch(state)).toBeTruthy();
});
diff --git a/spec/frontend/import_entities/import_groups/components/import_table_spec.js b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
index cd184bb65cc..34e2a945333 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -1,6 +1,6 @@
import { shallowMount, createLocalVue } from '@vue/test-utils';
import VueApollo from 'vue-apollo';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlEmptyState, GlLoadingIcon, GlSearchBoxByClick, GlSprintf } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import ImportTableRow from '~/import_entities/import_groups/components/import_table_row.vue';
@@ -8,6 +8,7 @@ import ImportTable from '~/import_entities/import_groups/components/import_table
import setTargetNamespaceMutation from '~/import_entities/import_groups/graphql/mutations/set_target_namespace.mutation.graphql';
import setNewNameMutation from '~/import_entities/import_groups/graphql/mutations/set_new_name.mutation.graphql';
import importGroupMutation from '~/import_entities/import_groups/graphql/mutations/import_group.mutation.graphql';
+import PaginationLinks from '~/vue_shared/components/pagination_links.vue';
import { STATUSES } from '~/import_entities/constants';
@@ -20,6 +21,9 @@ describe('import table', () => {
let wrapper;
let apolloProvider;
+ const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE });
+ const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
+
const createComponent = ({ bulkImportSourceGroups }) => {
apolloProvider = createMockApollo([], {
Query: {
@@ -34,6 +38,12 @@ describe('import table', () => {
});
wrapper = shallowMount(ImportTable, {
+ propsData: {
+ sourceUrl: 'https://demo.host',
+ },
+ stubs: {
+ GlSprintf,
+ },
localVue,
apolloProvider,
});
@@ -62,25 +72,50 @@ describe('import table', () => {
expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
});
+ it('renders message about empty state when no groups are available for import', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [],
+ pageInfo: FAKE_PAGE_INFO,
+ }),
+ });
+ await waitForPromises();
+
+ expect(wrapper.find(GlEmptyState).props().title).toBe('No groups available for import');
+ });
+
it('renders import row for each group in response', async () => {
const FAKE_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({ id: 2, status: STATUSES.FINISHED }),
];
createComponent({
- bulkImportSourceGroups: () => FAKE_GROUPS,
+ bulkImportSourceGroups: () => ({
+ nodes: FAKE_GROUPS,
+ pageInfo: FAKE_PAGE_INFO,
+ }),
});
await waitForPromises();
expect(wrapper.findAll(ImportTableRow)).toHaveLength(FAKE_GROUPS.length);
});
- describe('converts row events to mutation invocations', () => {
- const FAKE_GROUP = generateFakeEntry({ id: 1, status: STATUSES.NONE });
+ it('does not render status string when result list is empty', async () => {
+ createComponent({
+ bulkImportSourceGroups: jest.fn().mockResolvedValue({
+ nodes: [],
+ pageInfo: FAKE_PAGE_INFO,
+ }),
+ });
+ await waitForPromises();
+ expect(wrapper.text()).not.toContain('Showing 1-0');
+ });
+
+ describe('converts row events to mutation invocations', () => {
beforeEach(() => {
createComponent({
- bulkImportSourceGroups: () => [FAKE_GROUP],
+ bulkImportSourceGroups: () => ({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO }),
});
return waitForPromises();
});
@@ -100,4 +135,115 @@ describe('import table', () => {
});
});
});
+
+ describe('pagination', () => {
+ const bulkImportSourceGroupsQueryMock = jest
+ .fn()
+ .mockResolvedValue({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO });
+
+ beforeEach(() => {
+ createComponent({
+ bulkImportSourceGroups: bulkImportSourceGroupsQueryMock,
+ });
+ return waitForPromises();
+ });
+
+ it('correctly passes pagination info from query', () => {
+ expect(wrapper.find(PaginationLinks).props().pageInfo).toStrictEqual(FAKE_PAGE_INFO);
+ });
+
+ it('updates page when page change is requested', async () => {
+ const REQUESTED_PAGE = 2;
+ wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
+
+ await waitForPromises();
+ expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ page: REQUESTED_PAGE }),
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+
+ it('updates status text when page is changed', async () => {
+ const REQUESTED_PAGE = 2;
+ bulkImportSourceGroupsQueryMock.mockResolvedValue({
+ nodes: [FAKE_GROUP],
+ pageInfo: {
+ page: 2,
+ total: 38,
+ perPage: 20,
+ totalPages: 2,
+ },
+ });
+ wrapper.find(PaginationLinks).props().change(REQUESTED_PAGE);
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain('Showing 21-21 of 38');
+ });
+ });
+
+ describe('filters', () => {
+ const bulkImportSourceGroupsQueryMock = jest
+ .fn()
+ .mockResolvedValue({ nodes: [FAKE_GROUP], pageInfo: FAKE_PAGE_INFO });
+
+ beforeEach(() => {
+ createComponent({
+ bulkImportSourceGroups: bulkImportSourceGroupsQueryMock,
+ });
+ return waitForPromises();
+ });
+
+ const findFilterInput = () => wrapper.find(GlSearchBoxByClick);
+
+ it('properly passes filter to graphql query when search box is submitted', async () => {
+ createComponent({
+ bulkImportSourceGroups: bulkImportSourceGroupsQueryMock,
+ });
+ await waitForPromises();
+
+ const FILTER_VALUE = 'foo';
+ findFilterInput().vm.$emit('submit', FILTER_VALUE);
+ await waitForPromises();
+
+ expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ filter: FILTER_VALUE }),
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+
+ it('updates status string when search box is submitted', async () => {
+ createComponent({
+ bulkImportSourceGroups: bulkImportSourceGroupsQueryMock,
+ });
+ await waitForPromises();
+
+ const FILTER_VALUE = 'foo';
+ findFilterInput().vm.$emit('submit', FILTER_VALUE);
+ await waitForPromises();
+
+ expect(wrapper.text()).toContain('Showing 1-1 of 40 groups matching filter "foo"');
+ });
+
+ it('properly resets filter in graphql query when search box is cleared', async () => {
+ const FILTER_VALUE = 'foo';
+ findFilterInput().vm.$emit('submit', FILTER_VALUE);
+ await waitForPromises();
+
+ bulkImportSourceGroupsQueryMock.mockClear();
+ await apolloProvider.defaultClient.resetStore();
+ findFilterInput().vm.$emit('clear');
+ await waitForPromises();
+
+ expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ filter: '' }),
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+ });
});
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index 514ed411138..94a2e4f75b4 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -79,33 +79,56 @@ describe('Bulk import resolvers', () => {
axiosMockAdapter
.onGet(FAKE_ENDPOINTS.availableNamespaces)
.reply(httpStatus.OK, availableNamespacesFixture);
-
- const response = await client.query({ query: bulkImportSourceGroupsQuery });
- results = response.data.bulkImportSourceGroups;
});
- it('mirrors REST endpoint response fields', () => {
- const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url'];
- expect(
- results.every((r, idx) =>
- MIRRORED_FIELDS.every(
- (field) => r[field] === statusEndpointFixture.importable_data[idx][field],
+ describe('when called', () => {
+ beforeEach(async () => {
+ const response = await client.query({ query: bulkImportSourceGroupsQuery });
+ results = response.data.bulkImportSourceGroups.nodes;
+ });
+
+ it('mirrors REST endpoint response fields', () => {
+ const MIRRORED_FIELDS = ['id', 'full_name', 'full_path', 'web_url'];
+ expect(
+ results.every((r, idx) =>
+ MIRRORED_FIELDS.every(
+ (field) => r[field] === statusEndpointFixture.importable_data[idx][field],
+ ),
),
- ),
- ).toBe(true);
- });
+ ).toBe(true);
+ });
- it('populates each result instance with status field default to none', () => {
- expect(results.every((r) => r.status === STATUSES.NONE)).toBe(true);
- });
+ it('populates each result instance with status field default to none', () => {
+ expect(results.every((r) => r.status === STATUSES.NONE)).toBe(true);
+ });
- it('populates each result instance with import_target defaulted to first available namespace', () => {
- expect(
- results.every(
- (r) => r.import_target.target_namespace === availableNamespacesFixture[0].full_path,
- ),
- ).toBe(true);
+ it('populates each result instance with import_target defaulted to first available namespace', () => {
+ expect(
+ results.every(
+ (r) => r.import_target.target_namespace === availableNamespacesFixture[0].full_path,
+ ),
+ ).toBe(true);
+ });
});
+
+ it.each`
+ variable | queryParam | value
+ ${'filter'} | ${'filter'} | ${'demo'}
+ ${'perPage'} | ${'per_page'} | ${30}
+ ${'page'} | ${'page'} | ${3}
+ `(
+ 'properly passes GraphQL variable $variable as REST $queryParam query parameter',
+ async ({ variable, queryParam, value }) => {
+ await client.query({
+ query: bulkImportSourceGroupsQuery,
+ variables: { [variable]: value },
+ });
+ const restCall = axiosMockAdapter.history.get.find(
+ (q) => q.url === FAKE_ENDPOINTS.status,
+ );
+ expect(restCall.params[queryParam]).toBe(value);
+ },
+ );
});
});
@@ -117,20 +140,28 @@ describe('Bulk import resolvers', () => {
client.writeQuery({
query: bulkImportSourceGroupsQuery,
data: {
- bulkImportSourceGroups: [
- {
- __typename: clientTypenames.BulkImportSourceGroup,
- id: GROUP_ID,
- status: STATUSES.NONE,
- web_url: 'https://fake.host/1',
- full_path: 'fake_group_1',
- full_name: 'fake_name_1',
- import_target: {
- target_namespace: 'root',
- new_name: 'group1',
+ bulkImportSourceGroups: {
+ nodes: [
+ {
+ __typename: clientTypenames.BulkImportSourceGroup,
+ id: GROUP_ID,
+ status: STATUSES.NONE,
+ web_url: 'https://fake.host/1',
+ full_path: 'fake_group_1',
+ full_name: 'fake_name_1',
+ import_target: {
+ target_namespace: 'root',
+ new_name: 'group1',
+ },
},
+ ],
+ pageInfo: {
+ page: 1,
+ perPage: 20,
+ total: 37,
+ totalPages: 2,
},
- ],
+ },
},
});
@@ -140,7 +171,7 @@ describe('Bulk import resolvers', () => {
fetchPolicy: 'cache-only',
})
.subscribe(({ data }) => {
- results = data.bulkImportSourceGroups;
+ results = data.bulkImportSourceGroups.nodes;
});
});
@@ -174,7 +205,9 @@ describe('Bulk import resolvers', () => {
});
await waitForPromises();
- const { bulkImportSourceGroups: intermediateResults } = client.readQuery({
+ const {
+ bulkImportSourceGroups: { nodes: intermediateResults },
+ } = client.readQuery({
query: bulkImportSourceGroupsQuery,
});
diff --git a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
index e7f1626f81d..8d0e1dbd65f 100644
--- a/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/services/status_poller_spec.js
@@ -4,6 +4,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createFlash from '~/flash';
import { StatusPoller } from '~/import_entities/import_groups/graphql/services/status_poller';
+import { clientTypenames } from '~/import_entities/import_groups/graphql/client_factory';
import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
import { STATUSES } from '~/import_entities/constants';
import { SourceGroupsManager } from '~/import_entities/import_groups/graphql/services/source_groups_manager';
@@ -17,6 +18,7 @@ jest.mock('~/import_entities/import_groups/graphql/services/source_groups_manage
}));
const TEST_POLL_INTERVAL = 1000;
+const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
describe('Bulk import status poller', () => {
let poller;
@@ -25,6 +27,25 @@ describe('Bulk import status poller', () => {
const listQueryCacheCalls = () =>
clientMock.readQuery.mock.calls.filter((call) => call[0].query === bulkImportSourceGroupsQuery);
+ const generateFakeGroups = (statuses) =>
+ statuses.map((status, idx) => generateFakeEntry({ status, id: idx }));
+
+ const writeFakeGroupsQuery = (nodes) => {
+ clientMock.cache.writeQuery({
+ query: bulkImportSourceGroupsQuery,
+ data: {
+ bulkImportSourceGroups: {
+ __typename: clientTypenames.BulkImportSourceGroupConnection,
+ nodes,
+ pageInfo: {
+ __typename: clientTypenames.BulkImportPageInfo,
+ ...FAKE_PAGE_INFO,
+ },
+ },
+ },
+ });
+ };
+
beforeEach(() => {
clientMock = createMockClient({
cache: new InMemoryCache({
@@ -42,10 +63,7 @@ describe('Bulk import status poller', () => {
describe('general behavior', () => {
beforeEach(() => {
- clientMock.cache.writeQuery({
- query: bulkImportSourceGroupsQuery,
- data: { bulkImportSourceGroups: [] },
- });
+ writeFakeGroupsQuery([]);
});
it('does not perform polling when constructed', () => {
@@ -94,14 +112,7 @@ describe('Bulk import status poller', () => {
});
it('does not query server when no groups have STARTED status', async () => {
- clientMock.cache.writeQuery({
- query: bulkImportSourceGroupsQuery,
- data: {
- bulkImportSourceGroups: [STATUSES.NONE, STATUSES.FINISHED].map((status, idx) =>
- generateFakeEntry({ status, id: idx }),
- ),
- },
- });
+ writeFakeGroupsQuery(generateFakeGroups([STATUSES.NONE, STATUSES.FINISHED]));
jest.spyOn(clientMock, 'query');
poller.startPolling();
@@ -111,44 +122,23 @@ describe('Bulk import status poller', () => {
describe('when there are groups which have STARTED status', () => {
const TARGET_NAMESPACE = 'root';
- const STARTED_GROUP_1 = {
+ const STARTED_GROUP_1 = generateFakeEntry({
status: STATUSES.STARTED,
id: 'started1',
- import_target: {
- target_namespace: TARGET_NAMESPACE,
- new_name: 'group1',
- },
- };
+ });
- const STARTED_GROUP_2 = {
+ const STARTED_GROUP_2 = generateFakeEntry({
status: STATUSES.STARTED,
id: 'started2',
- import_target: {
- target_namespace: TARGET_NAMESPACE,
- new_name: 'group2',
- },
- };
+ });
- const NOT_STARTED_GROUP = {
+ const NOT_STARTED_GROUP = generateFakeEntry({
status: STATUSES.NONE,
id: 'not_started',
- import_target: {
- target_namespace: TARGET_NAMESPACE,
- new_name: 'group3',
- },
- };
+ });
it('query server only for groups with STATUSES.STARTED', async () => {
- clientMock.cache.writeQuery({
- query: bulkImportSourceGroupsQuery,
- data: {
- bulkImportSourceGroups: [
- STARTED_GROUP_1,
- NOT_STARTED_GROUP,
- STARTED_GROUP_2,
- ].map((group) => generateFakeEntry(group)),
- },
- });
+ writeFakeGroupsQuery([STARTED_GROUP_1, NOT_STARTED_GROUP, STARTED_GROUP_2]);
clientMock.query = jest.fn().mockResolvedValue({ data: {} });
poller.startPolling();
@@ -166,14 +156,7 @@ describe('Bulk import status poller', () => {
});
it('updates statuses only for groups in response', async () => {
- clientMock.cache.writeQuery({
- query: bulkImportSourceGroupsQuery,
- data: {
- bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map((group) =>
- generateFakeEntry(group),
- ),
- },
- });
+ writeFakeGroupsQuery([STARTED_GROUP_1, STARTED_GROUP_2]);
clientMock.query = jest.fn().mockResolvedValue({ data: { group0: {} } });
poller.startPolling();
@@ -188,14 +171,7 @@ describe('Bulk import status poller', () => {
describe('when error occurs', () => {
beforeEach(() => {
- clientMock.cache.writeQuery({
- query: bulkImportSourceGroupsQuery,
- data: {
- bulkImportSourceGroups: [STARTED_GROUP_1, STARTED_GROUP_2].map((group) =>
- generateFakeEntry(group),
- ),
- },
- });
+ writeFakeGroupsQuery([STARTED_GROUP_1, STARTED_GROUP_2]);
clientMock.query = jest.fn().mockRejectedValue(new Error('dummy error'));
poller.startPolling();
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 2b3c803be08..4398d568501 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,9 +9,7 @@ exports[`IncidentsSettingTabs should render the component 1`] = `
<div
class="settings-header"
>
- <h4
- class="gl-my-3! gl-py-1"
- >
+ <h4>
Incidents
diff --git a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
index 50d0de8a753..0d98c09e16f 100644
--- a/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
+++ b/spec/frontend/incidents_settings/components/pagerduty_form_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import waitForPromises from 'helpers/wait_for_promises';
import { GlAlert, GlModal } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
import PagerDutySettingsForm from '~/incidents_settings/components/pagerduty_form.vue';
describe('Alert integration settings form', () => {
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 97e77ac87ab..4888a8c1a5a 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -1,5 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { mockIntegrationProps } from 'jest/integrations/edit/mock_data';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { setHTMLFixture } from 'helpers/fixtures';
import { createStore } from '~/integrations/edit/store';
import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
@@ -15,24 +17,31 @@ import { integrationLevels } from '~/integrations/edit/constants';
describe('IntegrationForm', () => {
let wrapper;
- const createComponent = (customStateProps = {}, featureFlags = {}, initialState = {}) => {
- wrapper = shallowMount(IntegrationForm, {
- propsData: {},
- store: createStore({
- customState: { ...mockIntegrationProps, ...customStateProps },
- ...initialState,
+ const createComponent = ({
+ customStateProps = {},
+ featureFlags = {},
+ initialState = {},
+ props = {},
+ } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(IntegrationForm, {
+ propsData: { ...props },
+ store: createStore({
+ customState: { ...mockIntegrationProps, ...customStateProps },
+ ...initialState,
+ }),
+ stubs: {
+ OverrideDropdown,
+ ActiveCheckbox,
+ ConfirmationModal,
+ JiraTriggerFields,
+ TriggerFields,
+ },
+ provide: {
+ glFeatures: featureFlags,
+ },
}),
- stubs: {
- OverrideDropdown,
- ActiveCheckbox,
- ConfirmationModal,
- JiraTriggerFields,
- TriggerFields,
- },
- provide: {
- glFeatures: featureFlags,
- },
- });
+ );
};
afterEach(() => {
@@ -63,7 +72,9 @@ describe('IntegrationForm', () => {
describe('showActive is false', () => {
it('does not render ActiveCheckbox', () => {
createComponent({
- showActive: false,
+ customStateProps: {
+ showActive: false,
+ },
});
expect(findActiveCheckbox().exists()).toBe(false);
@@ -73,7 +84,9 @@ describe('IntegrationForm', () => {
describe('integrationLevel is instance', () => {
it('renders ConfirmationModal', () => {
createComponent({
- integrationLevel: integrationLevels.INSTANCE,
+ customStateProps: {
+ integrationLevel: integrationLevels.INSTANCE,
+ },
});
expect(findConfirmationModal().exists()).toBe(true);
@@ -82,7 +95,9 @@ describe('IntegrationForm', () => {
describe('resetPath is empty', () => {
it('does not render ResetConfirmationModal and button', () => {
createComponent({
- integrationLevel: integrationLevels.INSTANCE,
+ customStateProps: {
+ integrationLevel: integrationLevels.INSTANCE,
+ },
});
expect(findResetButton().exists()).toBe(false);
@@ -93,8 +108,10 @@ describe('IntegrationForm', () => {
describe('resetPath is present', () => {
it('renders ResetConfirmationModal and button', () => {
createComponent({
- integrationLevel: integrationLevels.INSTANCE,
- resetPath: 'resetPath',
+ customStateProps: {
+ integrationLevel: integrationLevels.INSTANCE,
+ resetPath: 'resetPath',
+ },
});
expect(findResetButton().exists()).toBe(true);
@@ -106,7 +123,9 @@ describe('IntegrationForm', () => {
describe('integrationLevel is group', () => {
it('renders ConfirmationModal', () => {
createComponent({
- integrationLevel: integrationLevels.GROUP,
+ customStateProps: {
+ integrationLevel: integrationLevels.GROUP,
+ },
});
expect(findConfirmationModal().exists()).toBe(true);
@@ -115,7 +134,9 @@ describe('IntegrationForm', () => {
describe('resetPath is empty', () => {
it('does not render ResetConfirmationModal and button', () => {
createComponent({
- integrationLevel: integrationLevels.GROUP,
+ customStateProps: {
+ integrationLevel: integrationLevels.GROUP,
+ },
});
expect(findResetButton().exists()).toBe(false);
@@ -126,8 +147,10 @@ describe('IntegrationForm', () => {
describe('resetPath is present', () => {
it('renders ResetConfirmationModal and button', () => {
createComponent({
- integrationLevel: integrationLevels.GROUP,
- resetPath: 'resetPath',
+ customStateProps: {
+ integrationLevel: integrationLevels.GROUP,
+ resetPath: 'resetPath',
+ },
});
expect(findResetButton().exists()).toBe(true);
@@ -139,7 +162,9 @@ describe('IntegrationForm', () => {
describe('integrationLevel is project', () => {
it('does not render ConfirmationModal', () => {
createComponent({
- integrationLevel: 'project',
+ customStateProps: {
+ integrationLevel: 'project',
+ },
});
expect(findConfirmationModal().exists()).toBe(false);
@@ -147,8 +172,10 @@ describe('IntegrationForm', () => {
it('does not render ResetConfirmationModal and button', () => {
createComponent({
- integrationLevel: 'project',
- resetPath: 'resetPath',
+ customStateProps: {
+ integrationLevel: 'project',
+ resetPath: 'resetPath',
+ },
});
expect(findResetButton().exists()).toBe(false);
@@ -158,7 +185,9 @@ describe('IntegrationForm', () => {
describe('type is "slack"', () => {
beforeEach(() => {
- createComponent({ type: 'slack' });
+ createComponent({
+ customStateProps: { type: 'slack' },
+ });
});
it('does not render JiraTriggerFields', () => {
@@ -172,14 +201,19 @@ describe('IntegrationForm', () => {
describe('type is "jira"', () => {
it('renders JiraTriggerFields', () => {
- createComponent({ type: 'jira' });
+ createComponent({
+ customStateProps: { type: 'jira' },
+ });
expect(findJiraTriggerFields().exists()).toBe(true);
});
describe('featureFlag jiraIssuesIntegration is false', () => {
it('does not render JiraIssuesFields', () => {
- createComponent({ type: 'jira' }, { jiraIssuesIntegration: false });
+ createComponent({
+ customStateProps: { type: 'jira' },
+ featureFlags: { jiraIssuesIntegration: false },
+ });
expect(findJiraIssuesFields().exists()).toBe(false);
});
@@ -187,8 +221,10 @@ describe('IntegrationForm', () => {
describe('featureFlag jiraIssuesIntegration is true', () => {
it('renders JiraIssuesFields', () => {
- createComponent({ type: 'jira' }, { jiraIssuesIntegration: true });
-
+ createComponent({
+ customStateProps: { type: 'jira' },
+ featureFlags: { jiraIssuesIntegration: true },
+ });
expect(findJiraIssuesFields().exists()).toBe(true);
});
});
@@ -200,8 +236,10 @@ describe('IntegrationForm', () => {
const type = 'slack';
createComponent({
- triggerEvents: events,
- type,
+ customStateProps: {
+ triggerEvents: events,
+ type,
+ },
});
expect(findTriggerFields().exists()).toBe(true);
@@ -218,7 +256,9 @@ describe('IntegrationForm', () => {
];
createComponent({
- fields,
+ customStateProps: {
+ fields,
+ },
});
const dynamicFields = wrapper.findAll(DynamicField);
@@ -232,13 +272,11 @@ describe('IntegrationForm', () => {
describe('defaultState state is null', () => {
it('does not render OverrideDropdown', () => {
- createComponent(
- {},
- {},
- {
+ createComponent({
+ initialState: {
defaultState: null,
},
- );
+ });
expect(findOverrideDropdown().exists()).toBe(false);
});
@@ -246,18 +284,43 @@ describe('IntegrationForm', () => {
describe('defaultState state is an object', () => {
it('renders OverrideDropdown', () => {
- createComponent(
- {},
- {},
- {
+ createComponent({
+ initialState: {
defaultState: {
...mockIntegrationProps,
},
},
- );
+ });
expect(findOverrideDropdown().exists()).toBe(true);
});
});
+
+ describe('with `helpHtml` prop', () => {
+ const mockTestId = 'jest-help-html-test';
+
+ setHTMLFixture(`
+ <div data-testid="${mockTestId}">
+ <svg class="gl-icon">
+ <use></use>
+ </svg>
+ </div>
+ `);
+
+ it('renders `helpHtml`', async () => {
+ const mockHelpHtml = document.querySelector(`[data-testid="${mockTestId}"]`);
+
+ createComponent({
+ props: {
+ helpHtml: mockHelpHtml.outerHTML,
+ },
+ });
+
+ const helpHtml = wrapper.findByTestId(mockTestId);
+
+ expect(helpHtml.isVisible()).toBe(true);
+ expect(helpHtml.find('svg').isVisible()).toBe(true);
+ });
+ });
});
});
diff --git a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
index eaeed2703d1..b866cf8bc03 100644
--- a/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
+++ b/spec/frontend/integrations/edit/components/jira_issues_fields_spec.js
@@ -3,18 +3,22 @@ import { mount } from '@vue/test-utils';
import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import JiraIssuesFields from '~/integrations/edit/components/jira_issues_fields.vue';
+import eventHub from '~/integrations/edit/event_hub';
describe('JiraIssuesFields', () => {
let wrapper;
const defaultProps = {
- showJiraIssuesIntegration: true,
editProjectPath: '/edit',
+ showJiraIssuesIntegration: true,
+ showJiraVulnerabilitiesIntegration: true,
};
- const createComponent = (props) => {
+ const createComponent = ({ props, ...options } = {}) => {
wrapper = mount(JiraIssuesFields, {
propsData: { ...defaultProps, ...props },
+ stubs: ['jira-issue-creation-vulnerabilities'],
+ ...options,
});
};
@@ -28,11 +32,14 @@ describe('JiraIssuesFields', () => {
const findEnableCheckbox = () => wrapper.find(GlFormCheckbox);
const findProjectKey = () => wrapper.find(GlFormInput);
const expectedBannerText = 'This is a Premium feature';
+ const findJiraForVulnerabilities = () => wrapper.find('[data-testid="jira-for-vulnerabilities"]');
+ const setEnableCheckbox = async (isEnabled = true) =>
+ findEnableCheckbox().vm.$emit('input', isEnabled);
describe('template', () => {
describe('upgrade banner for non-Premium user', () => {
beforeEach(() => {
- createComponent({ initialProjectKey: '', showJiraIssuesIntegration: false });
+ createComponent({ props: { initialProjectKey: '', showJiraIssuesIntegration: false } });
});
it('shows upgrade banner', () => {
@@ -47,7 +54,7 @@ describe('JiraIssuesFields', () => {
describe('Enable Jira issues checkbox', () => {
beforeEach(() => {
- createComponent({ initialProjectKey: '' });
+ createComponent({ props: { initialProjectKey: '' } });
});
it('does not show upgrade banner', () => {
@@ -69,20 +76,16 @@ describe('JiraIssuesFields', () => {
});
describe('on enable issues', () => {
- it('enables project_key input', () => {
- findEnableCheckbox().vm.$emit('input', true);
+ it('enables project_key input', async () => {
+ await setEnableCheckbox(true);
- return wrapper.vm.$nextTick().then(() => {
- expect(findProjectKey().attributes('disabled')).toBeUndefined();
- });
+ expect(findProjectKey().attributes('disabled')).toBeUndefined();
});
- it('requires project_key input', () => {
- findEnableCheckbox().vm.$emit('input', true);
+ it('requires project_key input', async () => {
+ await setEnableCheckbox(true);
- return wrapper.vm.$nextTick().then(() => {
- expect(findProjectKey().attributes('required')).toBe('required');
- });
+ expect(findProjectKey().attributes('required')).toBe('required');
});
});
});
@@ -103,10 +106,46 @@ describe('JiraIssuesFields', () => {
});
it('does not contain warning when GitLab issues is disabled', () => {
- createComponent({ gitlabIssuesEnabled: false });
+ createComponent({ props: { gitlabIssuesEnabled: false } });
expect(wrapper.text()).not.toContain(expectedText);
});
});
+
+ describe('Vulnerabilities creation', () => {
+ beforeEach(() => {
+ createComponent({ provide: { glFeatures: { jiraForVulnerabilities: true } } });
+ });
+
+ it.each([true, false])(
+ 'shows the jira-vulnerabilities component correctly when jira issues enables is set to "%s"',
+ async (hasJiraIssuesEnabled) => {
+ await setEnableCheckbox(hasJiraIssuesEnabled);
+
+ expect(findJiraForVulnerabilities().exists()).toBe(hasJiraIssuesEnabled);
+ },
+ );
+
+ it('emits "getJiraIssueTypes" to the eventHub when the jira-vulnerabilities component requests to fetch issue types', async () => {
+ const eventHubEmitSpy = jest.spyOn(eventHub, '$emit');
+
+ await setEnableCheckbox(true);
+ await findJiraForVulnerabilities().vm.$emit('request-get-issue-types');
+
+ expect(eventHubEmitSpy).toHaveBeenCalledWith('getJiraIssueTypes');
+ });
+
+ describe('with "jiraForVulnerabilities" feature flag disabled', () => {
+ beforeEach(async () => {
+ createComponent({
+ provide: { glFeatures: { jiraForVulnerabilities: false } },
+ });
+ });
+
+ it('does not show section', () => {
+ expect(findJiraForVulnerabilities().exists()).toBe(false);
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/integrations/edit/store/actions_spec.js b/spec/frontend/integrations/edit/store/actions_spec.js
index 1ff881c265d..4b7060fae55 100644
--- a/spec/frontend/integrations/edit/store/actions_spec.js
+++ b/spec/frontend/integrations/edit/store/actions_spec.js
@@ -9,6 +9,9 @@ import {
requestResetIntegration,
receiveResetIntegrationSuccess,
receiveResetIntegrationError,
+ requestJiraIssueTypes,
+ receiveJiraIssueTypesSuccess,
+ receiveJiraIssueTypesError,
} from '~/integrations/edit/store/actions';
import * as types from '~/integrations/edit/store/mutation_types';
@@ -70,4 +73,34 @@ describe('Integration form store actions', () => {
]);
});
});
+
+ describe('requestJiraIssueTypes', () => {
+ it('should commit SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE and SET_IS_LOADING_JIRA_ISSUE_TYPES mutations', () => {
+ return testAction(requestJiraIssueTypes, null, state, [
+ { type: types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, payload: '' },
+ { type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: true },
+ ]);
+ });
+ });
+
+ describe('receiveJiraIssueTypesSuccess', () => {
+ it('should commit SET_IS_LOADING_JIRA_ISSUE_TYPES and SET_JIRA_ISSUE_TYPES mutations', () => {
+ const issueTypes = ['issue', 'epic'];
+ return testAction(receiveJiraIssueTypesSuccess, issueTypes, state, [
+ { type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: false },
+ { type: types.SET_JIRA_ISSUE_TYPES, payload: issueTypes },
+ ]);
+ });
+ });
+
+ describe('receiveJiraIssueTypesError', () => {
+ it('should commit SET_IS_LOADING_JIRA_ISSUE_TYPES, SET_JIRA_ISSUE_TYPES and SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE mutations', () => {
+ const errorMessage = 'something went wrong';
+ return testAction(receiveJiraIssueTypesError, errorMessage, state, [
+ { type: types.SET_IS_LOADING_JIRA_ISSUE_TYPES, payload: false },
+ { type: types.SET_JIRA_ISSUE_TYPES, payload: [] },
+ { type: types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE, payload: errorMessage },
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/integrations/edit/store/mutations_spec.js b/spec/frontend/integrations/edit/store/mutations_spec.js
index 81f39adb87f..bf2a51d4dad 100644
--- a/spec/frontend/integrations/edit/store/mutations_spec.js
+++ b/spec/frontend/integrations/edit/store/mutations_spec.js
@@ -56,4 +56,30 @@ describe('Integration form store mutations', () => {
expect(state.isResetting).toBe(false);
});
});
+
+ describe(`${types.SET_JIRA_ISSUE_TYPES}`, () => {
+ it('sets jiraIssueTypes', () => {
+ const jiraIssueTypes = ['issue', 'epic'];
+ mutations[types.SET_JIRA_ISSUE_TYPES](state, jiraIssueTypes);
+
+ expect(state.jiraIssueTypes).toBe(jiraIssueTypes);
+ });
+ });
+
+ describe(`${types.SET_IS_LOADING_JIRA_ISSUE_TYPES}`, () => {
+ it.each([true, false])('sets isLoadingJiraIssueTypes to "%s"', (isLoading) => {
+ mutations[types.SET_IS_LOADING_JIRA_ISSUE_TYPES](state, isLoading);
+
+ expect(state.isLoadingJiraIssueTypes).toBe(isLoading);
+ });
+ });
+
+ describe(`${types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE}`, () => {
+ it('sets loadingJiraIssueTypesErrorMessage', () => {
+ const errorMessage = 'something went wrong';
+ mutations[types.SET_JIRA_ISSUE_TYPES_ERROR_MESSAGE](state, errorMessage);
+
+ expect(state.loadingJiraIssueTypesErrorMessage).toBe(errorMessage);
+ });
+ });
});
diff --git a/spec/frontend/integrations/edit/store/state_spec.js b/spec/frontend/integrations/edit/store/state_spec.js
index 4d0f4a1da71..6cd84836395 100644
--- a/spec/frontend/integrations/edit/store/state_spec.js
+++ b/spec/frontend/integrations/edit/store/state_spec.js
@@ -9,6 +9,9 @@ describe('Integration form state factory', () => {
isTesting: false,
isResetting: false,
override: false,
+ isLoadingJiraIssueTypes: false,
+ jiraIssueTypes: [],
+ loadingJiraIssueTypesErrorMessage: '',
});
});
diff --git a/spec/frontend/integrations/integration_settings_form_spec.js b/spec/frontend/integrations/integration_settings_form_spec.js
index bba851ad796..373f8f196f9 100644
--- a/spec/frontend/integrations/integration_settings_form_spec.js
+++ b/spec/frontend/integrations/integration_settings_form_spec.js
@@ -132,4 +132,83 @@ describe('IntegrationSettingsForm', () => {
expect(dispatchSpy).toHaveBeenCalledWith('setIsTesting', false);
});
});
+
+ describe('getJiraIssueTypes', () => {
+ let integrationSettingsForm;
+ let formData;
+ let mock;
+
+ beforeEach(() => {
+ mock = new MockAdaptor(axios);
+
+ jest.spyOn(axios, 'put');
+
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ integrationSettingsForm.init();
+
+ // eslint-disable-next-line no-jquery/no-serialize
+ formData = integrationSettingsForm.$form.serialize();
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('should always dispatch `requestJiraIssueTypes`', async () => {
+ const dispatchSpy = jest.fn();
+
+ mock.onPut(integrationSettingsForm.testEndPoint).networkError();
+
+ integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
+
+ await integrationSettingsForm.getJiraIssueTypes();
+
+ expect(dispatchSpy).toHaveBeenCalledWith('requestJiraIssueTypes');
+ });
+
+ it('should make an ajax request with provided `formData`', async () => {
+ await integrationSettingsForm.getJiraIssueTypes(formData);
+
+ expect(axios.put).toHaveBeenCalledWith(integrationSettingsForm.testEndPoint, formData);
+ });
+
+ it('should dispatch `receiveJiraIssueTypesSuccess` with the correct payload if ajax request is successful', async () => {
+ const mockData = ['ISSUE', 'EPIC'];
+ const dispatchSpy = jest.fn();
+
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: false,
+ issuetypes: mockData,
+ });
+
+ integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
+
+ await integrationSettingsForm.getJiraIssueTypes(formData);
+
+ expect(dispatchSpy).toHaveBeenCalledWith('receiveJiraIssueTypesSuccess', mockData);
+ });
+
+ it.each(['something went wrong', undefined])(
+ 'should dispatch "receiveJiraIssueTypesError" with a message if the backend responds with error',
+ async (responseErrorMessage) => {
+ const defaultErrorMessage = 'Connection failed. Please check your settings.';
+ const expectedErrorMessage = responseErrorMessage || defaultErrorMessage;
+ const dispatchSpy = jest.fn();
+
+ mock.onPut(integrationSettingsForm.testEndPoint).reply(200, {
+ error: true,
+ message: responseErrorMessage,
+ });
+
+ integrationSettingsForm.vue.$store = { dispatch: dispatchSpy };
+
+ await integrationSettingsForm.getJiraIssueTypes(formData);
+
+ expect(dispatchSpy).toHaveBeenCalledWith(
+ 'receiveJiraIssueTypesError',
+ expectedErrorMessage,
+ );
+ },
+ );
+ });
});
diff --git a/spec/frontend/issuable_list/components/issuable_item_spec.js b/spec/frontend/issuable_list/components/issuable_item_spec.js
index 3c01bf2d319..65e52d05084 100644
--- a/spec/frontend/issuable_list/components/issuable_item_spec.js
+++ b/spec/frontend/issuable_list/components/issuable_item_spec.js
@@ -257,6 +257,23 @@ describe('IssuableItem', () => {
);
});
+ it('renders issuable confidential icon when issuable is confidential', async () => {
+ wrapper.setProps({
+ issuable: {
+ ...mockIssuable,
+ confidential: true,
+ },
+ });
+
+ await wrapper.vm.$nextTick();
+
+ const confidentialEl = wrapper.find('[data-testid="issuable-title"]').find(GlIcon);
+
+ expect(confidentialEl.exists()).toBe(true);
+ expect(confidentialEl.props('name')).toBe('eye-slash');
+ expect(confidentialEl.attributes('title')).toBe('Confidential');
+ });
+
it('renders issuable reference', () => {
const referenceEl = wrapper.find('[data-testid="issuable-reference"]');
diff --git a/spec/frontend/issue_show/components/app_spec.js b/spec/frontend/issue_show/components/app_spec.js
index ec2055ca7d1..9be45686d91 100644
--- a/spec/frontend/issue_show/components/app_spec.js
+++ b/spec/frontend/issue_show/components/app_spec.js
@@ -7,6 +7,10 @@ import { visitUrl } from '~/lib/utils/url_utility';
import '~/behaviors/markdown/render_gfm';
import IssuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
+import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
+import DescriptionComponent from '~/issue_show/components/description.vue';
+import PinnedLinks from '~/issue_show/components/pinned_links.vue';
+import { IssuableStatus, IssuableStatusText } from '~/issue_show/constants';
import {
appProps,
initialRequest,
@@ -14,10 +18,6 @@ import {
secondRequest,
zoomMeetingUrl,
} from '../mock_data';
-import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
-import DescriptionComponent from '~/issue_show/components/description.vue';
-import PinnedLinks from '~/issue_show/components/pinned_links.vue';
-import { IssuableStatus, IssuableStatusText } from '~/issue_show/constants';
function formatText(text) {
return text.trim().replace(/\s\s+/g, ' ');
diff --git a/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js b/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
index 416870d1408..ef825688b69 100644
--- a/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
+++ b/spec/frontend/issue_show/components/incidents/incident_tabs_spec.js
@@ -4,12 +4,12 @@ import { GlTab } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import INVALID_URL from '~/lib/utils/invalid_url';
import IncidentTabs from '~/issue_show/components/incidents/incident_tabs.vue';
-import { descriptionProps } from '../../mock_data';
import DescriptionComponent from '~/issue_show/components/description.vue';
import HighlightBar from '~/issue_show/components/incidents/highlight_bar.vue';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
import Tracking from '~/tracking';
import { trackIncidentDetailsViewsOptions } from '~/incidents/constants';
+import { descriptionProps } from '../../mock_data';
const mockAlert = {
__typename: 'AlertManagementAlert',
diff --git a/spec/frontend/issue_show/issue_spec.js b/spec/frontend/issue_show/issue_spec.js
index 818f501882b..51f3a580e6e 100644
--- a/spec/frontend/issue_show/issue_spec.js
+++ b/spec/frontend/issue_show/issue_spec.js
@@ -4,8 +4,8 @@ import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import { initIssuableApp } from '~/issue_show/issue';
import * as parseData from '~/issue_show/utils/parse_data';
-import { appProps } from './mock_data';
import createStore from '~/notes/stores';
+import { appProps } from './mock_data';
const mock = new MockAdapter(axios);
mock.onGet().reply(200);
diff --git a/spec/frontend/issues_list/components/issuable_spec.js b/spec/frontend/issues_list/components/issuable_spec.js
index b47a84ad7f6..dcc3542e905 100644
--- a/spec/frontend/issues_list/components/issuable_spec.js
+++ b/spec/frontend/issues_list/components/issuable_spec.js
@@ -7,8 +7,8 @@ import { formatDate } from '~/lib/utils/datetime_utility';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import Issuable from '~/issues_list/components/issuable.vue';
import IssueAssignees from '~/vue_shared/components/issue/issue_assignees.vue';
-import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data';
import { isScopedLabel } from '~/lib/utils/common_utils';
+import { simpleIssue, testAssignees, testLabels } from '../issuable_list_test_data';
jest.mock('~/user_popovers');
diff --git a/spec/frontend/jira_connect/api_spec.js b/spec/frontend/jira_connect/api_spec.js
index 8fecbee9ca7..f98d56a6621 100644
--- a/spec/frontend/jira_connect/api_spec.js
+++ b/spec/frontend/jira_connect/api_spec.js
@@ -14,7 +14,7 @@ describe('JiraConnect API', () => {
const mockJwt = 'jwt';
const mockResponse = { success: true };
- const tokenSpy = jest.fn().mockReturnValue(mockJwt);
+ const tokenSpy = jest.fn((callback) => callback(mockJwt));
window.AP = {
context: {
diff --git a/spec/frontend/jira_connect/components/app_spec.js b/spec/frontend/jira_connect/components/app_spec.js
index be990d5061c..a9194cfc883 100644
--- a/spec/frontend/jira_connect/components/app_spec.js
+++ b/spec/frontend/jira_connect/components/app_spec.js
@@ -1,19 +1,20 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
+import { GlAlert, GlButton, GlModal } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import { GlAlert } from '@gitlab/ui';
+
import JiraConnectApp from '~/jira_connect/components/app.vue';
import createStore from '~/jira_connect/store';
import { SET_ERROR_MESSAGE } from '~/jira_connect/store/mutation_types';
-Vue.use(Vuex);
+jest.mock('~/jira_connect/api');
describe('JiraConnectApp', () => {
let wrapper;
let store;
const findAlert = () => wrapper.findComponent(GlAlert);
+ const findGlButton = () => wrapper.findComponent(GlButton);
+ const findGlModal = () => wrapper.findComponent(GlModal);
const findHeader = () => wrapper.findByTestId('new-jira-connect-ui-heading');
const findHeaderText = () => findHeader().text();
@@ -44,6 +45,33 @@ describe('JiraConnectApp', () => {
expect(findHeaderText()).toBe('Linked namespaces');
});
+ describe('when user is not logged in', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: { newJiraConnectUi: true },
+ usersPath: '/users',
+ },
+ });
+ });
+
+ it('renders "Sign in" button', () => {
+ expect(findGlButton().text()).toBe('Sign in to add namespaces');
+ expect(findGlModal().exists()).toBe(false);
+ });
+ });
+
+ describe('when user is logged in', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders "Add" button and modal', () => {
+ expect(findGlButton().text()).toBe('Add namespace');
+ expect(findGlModal().exists()).toBe(true);
+ });
+ });
+
describe('newJiraConnectUi is false', () => {
it('does not render new UI', () => {
createComponent({
diff --git a/spec/frontend/jira_connect/components/groups_list_item_spec.js b/spec/frontend/jira_connect/components/groups_list_item_spec.js
index 77577c53cf4..2499be1ea5f 100644
--- a/spec/frontend/jira_connect/components/groups_list_item_spec.js
+++ b/spec/frontend/jira_connect/components/groups_list_item_spec.js
@@ -1,27 +1,37 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlAvatar } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import { GlAvatar, GlButton } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import { mockGroup1 } from '../mock_data';
+import waitForPromises from 'helpers/wait_for_promises';
import GroupsListItem from '~/jira_connect/components/groups_list_item.vue';
+import * as JiraConnectApi from '~/jira_connect/api';
+import { mockGroup1 } from '../mock_data';
describe('GroupsListItem', () => {
let wrapper;
+ const mockSubscriptionPath = 'subscriptionPath';
- const createComponent = () => {
+ const reloadSpy = jest.fn();
+
+ global.AP = {
+ navigator: {
+ reload: reloadSpy,
+ },
+ };
+
+ const createComponent = ({ mountFn = shallowMount } = {}) => {
wrapper = extendedWrapper(
- shallowMount(GroupsListItem, {
+ mountFn(GroupsListItem, {
propsData: {
group: mockGroup1,
},
+ provide: {
+ subscriptionsPath: mockSubscriptionPath,
+ },
}),
);
};
- beforeEach(() => {
- createComponent();
- });
-
afterEach(() => {
wrapper.destroy();
wrapper = null;
@@ -30,17 +40,82 @@ describe('GroupsListItem', () => {
const findGlAvatar = () => wrapper.find(GlAvatar);
const findGroupName = () => wrapper.findByTestId('group-list-item-name');
const findGroupDescription = () => wrapper.findByTestId('group-list-item-description');
+ const findLinkButton = () => wrapper.find(GlButton);
+ const clickLinkButton = () => findLinkButton().trigger('click');
- it('renders group avatar', () => {
- expect(findGlAvatar().exists()).toBe(true);
- expect(findGlAvatar().props('src')).toBe(mockGroup1.avatar_url);
- });
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders group avatar', () => {
+ expect(findGlAvatar().exists()).toBe(true);
+ expect(findGlAvatar().props('src')).toBe(mockGroup1.avatar_url);
+ });
+
+ it('renders group name', () => {
+ expect(findGroupName().text()).toBe(mockGroup1.full_name);
+ });
- it('renders group name', () => {
- expect(findGroupName().text()).toBe(mockGroup1.full_name);
+ it('renders group description', () => {
+ expect(findGroupDescription().text()).toBe(mockGroup1.description);
+ });
+
+ it('renders Link button', () => {
+ expect(findLinkButton().exists()).toBe(true);
+ expect(findLinkButton().text()).toBe('Link');
+ });
});
- it('renders group description', () => {
- expect(findGroupDescription().text()).toBe(mockGroup1.description);
+ describe('on Link button click', () => {
+ let addSubscriptionSpy;
+
+ beforeEach(() => {
+ createComponent({ mountFn: mount });
+
+ addSubscriptionSpy = jest.spyOn(JiraConnectApi, 'addSubscription').mockResolvedValue();
+ });
+
+ it('sets button to loading and sends request', async () => {
+ expect(findLinkButton().props('loading')).toBe(false);
+
+ clickLinkButton();
+
+ await wrapper.vm.$nextTick();
+
+ expect(findLinkButton().props('loading')).toBe(true);
+
+ expect(addSubscriptionSpy).toHaveBeenCalledWith(mockSubscriptionPath, mockGroup1.full_path);
+ });
+
+ describe('when request is successful', () => {
+ it('reloads the page', async () => {
+ clickLinkButton();
+
+ await waitForPromises();
+
+ expect(reloadSpy).toHaveBeenCalled();
+ });
+ });
+
+ describe('when request has errors', () => {
+ const mockErrorMessage = 'error message';
+ const mockError = { response: { data: { error: mockErrorMessage } } };
+
+ beforeEach(() => {
+ addSubscriptionSpy = jest
+ .spyOn(JiraConnectApi, 'addSubscription')
+ .mockRejectedValue(mockError);
+ });
+
+ it('emits `error` event', async () => {
+ clickLinkButton();
+
+ await waitForPromises();
+
+ expect(reloadSpy).not.toHaveBeenCalled();
+ expect(wrapper.emitted('error')[0][0]).toBe(mockErrorMessage);
+ });
+ });
});
});
diff --git a/spec/frontend/jira_connect/components/groups_list_spec.js b/spec/frontend/jira_connect/components/groups_list_spec.js
index 94f158e6344..b18a913ca6a 100644
--- a/spec/frontend/jira_connect/components/groups_list_spec.js
+++ b/spec/frontend/jira_connect/components/groups_list_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import { GlLoadingIcon } from '@gitlab/ui';
+import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
import { fetchGroups } from '~/jira_connect/api';
@@ -28,6 +28,7 @@ describe('GroupsList', () => {
wrapper = null;
});
+ const findGlAlert = () => wrapper.find(GlAlert);
const findGlLoadingIcon = () => wrapper.find(GlLoadingIcon);
const findAllItems = () => wrapper.findAll(GroupsListItem);
const findFirstItem = () => findAllItems().at(0);
@@ -45,6 +46,18 @@ describe('GroupsList', () => {
});
});
+ describe('error fetching groups', () => {
+ it('renders error message', async () => {
+ fetchGroups.mockRejectedValue();
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findGlAlert().exists()).toBe(true);
+ expect(findGlAlert().text()).toBe('Failed to load namespaces. Please try again.');
+ });
+ });
+
describe('no groups returned', () => {
it('renders empty state', async () => {
fetchGroups.mockResolvedValue(mockEmptyResponse);
@@ -57,15 +70,28 @@ describe('GroupsList', () => {
});
describe('with groups returned', () => {
- it('renders groups list', async () => {
+ beforeEach(async () => {
fetchGroups.mockResolvedValue({ data: [mockGroup1, mockGroup2] });
createComponent();
await waitForPromises();
+ });
+ it('renders groups list', () => {
expect(findAllItems().length).toBe(2);
expect(findFirstItem().props('group')).toBe(mockGroup1);
expect(findSecondItem().props('group')).toBe(mockGroup2);
});
+
+ it('shows error message on $emit from item', async () => {
+ const errorMessage = 'error message';
+
+ findFirstItem().vm.$emit('error', errorMessage);
+
+ await wrapper.vm.$nextTick();
+
+ expect(findGlAlert().exists()).toBe(true);
+ expect(findGlAlert().text()).toContain(errorMessage);
+ });
});
});
diff --git a/spec/frontend/jira_connect/index_spec.js b/spec/frontend/jira_connect/index_spec.js
new file mode 100644
index 00000000000..eb54fe6476f
--- /dev/null
+++ b/spec/frontend/jira_connect/index_spec.js
@@ -0,0 +1,56 @@
+import waitForPromises from 'helpers/wait_for_promises';
+import { initJiraConnect } from '~/jira_connect';
+import { removeSubscription } from '~/jira_connect/api';
+
+jest.mock('~/jira_connect/api', () => ({
+ removeSubscription: jest.fn().mockResolvedValue(),
+ getLocation: jest.fn().mockResolvedValue('test/location'),
+}));
+
+describe('initJiraConnect', () => {
+ window.AP = {
+ navigator: {
+ reload: jest.fn(),
+ },
+ };
+
+ beforeEach(async () => {
+ setFixtures(`
+ <a class="js-jira-connect-sign-in" href="https://gitlab.com">Sign In</a>
+ <a class="js-jira-connect-sign-in" href="https://gitlab.com">Another Sign In</a>
+
+ <a href="https://gitlab.com/sub1" class="js-jira-connect-remove-subscription">Remove</a>
+ <a href="https://gitlab.com/sub2" class="js-jira-connect-remove-subscription">Remove</a>
+ <a href="https://gitlab.com/sub3" class="js-jira-connect-remove-subscription">Remove</a>
+ `);
+
+ await initJiraConnect();
+ });
+
+ describe('Sign in links', () => {
+ it('have `return_to` query parameter', () => {
+ Array.from(document.querySelectorAll('.js-jira-connect-sign-in')).forEach((el) => {
+ expect(el.href).toContain('return_to=test/location');
+ });
+ });
+ });
+
+ describe('`remove subscription` buttons', () => {
+ describe('on click', () => {
+ it('calls `removeSubscription`', () => {
+ Array.from(document.querySelectorAll('.js-jira-connect-remove-subscription')).forEach(
+ (removeSubscriptionButton) => {
+ removeSubscriptionButton.dispatchEvent(new Event('click'));
+
+ waitForPromises();
+
+ expect(removeSubscription).toHaveBeenCalledWith(removeSubscriptionButton.href);
+ expect(removeSubscription).toHaveBeenCalledTimes(1);
+
+ removeSubscription.mockClear();
+ },
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/jira_connect/mock_data.js b/spec/frontend/jira_connect/mock_data.js
index 31565912489..22255fabc3d 100644
--- a/spec/frontend/jira_connect/mock_data.js
+++ b/spec/frontend/jira_connect/mock_data.js
@@ -3,6 +3,7 @@ export const mockGroup1 = {
avatar_url: 'avatar.png',
name: 'Gitlab Org',
full_name: 'Gitlab Org',
+ full_path: 'gitlab-org',
description: 'Open source software to collaborate on code',
};
@@ -11,5 +12,6 @@ export const mockGroup2 = {
avatar_url: 'avatar.png',
name: 'Gitlab Com',
full_name: 'Gitlab Com',
+ full_path: 'gitlab-com',
description: 'For GitLab company related projects',
};
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 00fb8f5435e..1cbd0f5ad30 100644
--- a/spec/frontend/jira_import/components/jira_import_form_spec.js
+++ b/spec/frontend/jira_import/components/jira_import_form_spec.js
@@ -1,4 +1,13 @@
-import { GlAlert, GlButton, GlDropdown, GlFormSelect, GlLabel, GlTable } from '@gitlab/ui';
+import {
+ GlAlert,
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlFormSelect,
+ GlLabel,
+ GlSearchBoxByType,
+ GlTable,
+} from '@gitlab/ui';
import { getByRole } from '@testing-library/dom';
import { mount, shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
@@ -6,6 +15,7 @@ import axios from '~/lib/utils/axios_utils';
import JiraImportForm from '~/jira_import/components/jira_import_form.vue';
import getJiraUserMappingMutation from '~/jira_import/queries/get_jira_user_mapping.mutation.graphql';
import initiateJiraImportMutation from '~/jira_import/queries/initiate_jira_import.mutation.graphql';
+import searchProjectMembersQuery from '~/jira_import/queries/search_project_members.query.graphql';
import {
imports,
issuesPath,
@@ -19,6 +29,7 @@ import {
describe('JiraImportForm', () => {
let axiosMock;
let mutateSpy;
+ let querySpy;
let wrapper;
const currentUsername = 'mrgitlab';
@@ -72,6 +83,7 @@ describe('JiraImportForm', () => {
$apollo: {
loading,
mutate,
+ query: querySpy,
},
},
currentUsername,
@@ -79,19 +91,21 @@ describe('JiraImportForm', () => {
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
- mutateSpy = jest.fn(() =>
- Promise.resolve({
- data: {
- jiraImportStart: { errors: [] },
- jiraImportUsers: { jiraUsers: [], errors: [] },
- },
- }),
- );
+ mutateSpy = jest.fn().mockResolvedValue({
+ data: {
+ jiraImportStart: { errors: [] },
+ jiraImportUsers: { jiraUsers: [], errors: [] },
+ },
+ });
+ querySpy = jest.fn().mockResolvedValue({
+ data: { project: { projectMembers: { nodes: [] } } },
+ });
});
afterEach(() => {
axiosMock.restore();
mutateSpy.mockRestore();
+ querySpy.mockRestore();
wrapper.destroy();
wrapper = null;
});
@@ -236,6 +250,53 @@ describe('JiraImportForm', () => {
});
});
+ describe('member search', () => {
+ describe('when searching for a member', () => {
+ beforeEach(() => {
+ querySpy = jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ projectMembers: {
+ nodes: [
+ {
+ user: {
+ id: 7,
+ name: 'Frederic Chopin',
+ username: 'fchopin',
+ },
+ },
+ ],
+ },
+ },
+ },
+ });
+
+ wrapper = mountComponent({ mountFunction: mount });
+
+ wrapper.find(GlSearchBoxByType).vm.$emit('input', 'fred');
+ });
+
+ it('makes a GraphQL call', () => {
+ const queryArgument = {
+ query: searchProjectMembersQuery,
+ variables: {
+ fullPath: projectPath,
+ search: 'fred',
+ },
+ };
+
+ expect(querySpy).toHaveBeenCalledWith(expect.objectContaining(queryArgument));
+ });
+
+ it('updates the user list', () => {
+ expect(getUserDropdown().findAll(GlDropdownItem)).toHaveLength(1);
+ expect(getUserDropdown().find(GlDropdownItem).text()).toContain(
+ 'fchopin (Frederic Chopin)',
+ );
+ });
+ });
+ });
+
describe('buttons', () => {
describe('"Continue" button', () => {
it('is shown', () => {
diff --git a/spec/frontend/jobs/components/erased_block_spec.js b/spec/frontend/jobs/components/erased_block_spec.js
index b3e1d28eb16..9557ecdbb91 100644
--- a/spec/frontend/jobs/components/erased_block_spec.js
+++ b/spec/frontend/jobs/components/erased_block_spec.js
@@ -10,6 +10,8 @@ describe('Erased block', () => {
const timeago = getTimeago();
const formattedDate = timeago.format(erasedAt);
+ const findLink = () => wrapper.find(GlLink);
+
const createComponent = (props) => {
wrapper = mount(ErasedBlock, {
propsData: props,
@@ -32,7 +34,7 @@ describe('Erased block', () => {
});
it('renders username and link', () => {
- expect(wrapper.find(GlLink).attributes('href')).toEqual('gitlab.com/root');
+ expect(findLink().attributes('href')).toEqual('gitlab.com/root');
expect(wrapper.text().trim()).toContain('Job has been erased by');
expect(wrapper.text().trim()).toContain('root');
diff --git a/spec/frontend/jobs/components/job_app_spec.js b/spec/frontend/jobs/components/job_app_spec.js
index 657687b5e2a..b9d27932dff 100644
--- a/spec/frontend/jobs/components/job_app_spec.js
+++ b/spec/frontend/jobs/components/job_app_spec.js
@@ -34,7 +34,6 @@ describe('Job App', () => {
const props = {
artifactHelpUrl: 'help/artifact',
- runnerHelpUrl: 'help/runner',
deploymentHelpUrl: 'help/deployment',
runnerSettingsUrl: 'settings/ci-cd/runners',
variablesSettingsUrl: 'settings/ci-cd/variables',
diff --git a/spec/frontend/jobs/components/job_sidebar_details_container_spec.js b/spec/frontend/jobs/components/job_sidebar_details_container_spec.js
index bc0d455c309..a29d88e5c1e 100644
--- a/spec/frontend/jobs/components/job_sidebar_details_container_spec.js
+++ b/spec/frontend/jobs/components/job_sidebar_details_container_spec.js
@@ -116,14 +116,5 @@ describe('Job Sidebar Details Container', () => {
expect(findJobTimeout().exists()).toBe(false);
});
-
- it('should pass the help URL', async () => {
- const helpUrl = 'fakeUrl';
- const props = { runnerHelpUrl: helpUrl };
- createWrapper({ props });
- await store.dispatch('receiveJobSuccess', { metadata: { timeout_human_readable } });
-
- expect(findJobTimeout().props('helpUrl')).toBe(helpUrl);
- });
});
});
diff --git a/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js b/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js
index 4bf697ab7cc..8fc5b071e54 100644
--- a/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js
+++ b/spec/frontend/jobs/components/job_sidebar_retry_button_spec.js
@@ -1,8 +1,8 @@
import { GlButton, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import job from '../mock_data';
import JobsSidebarRetryButton from '~/jobs/components/job_sidebar_retry_button.vue';
import createStore from '~/jobs/store';
+import job from '../mock_data';
describe('Job Sidebar Retry Button', () => {
let store;
diff --git a/spec/frontend/jobs/components/trigger_block_spec.js b/spec/frontend/jobs/components/trigger_block_spec.js
index 16ea276ee4a..0af768c6c52 100644
--- a/spec/frontend/jobs/components/trigger_block_spec.js
+++ b/spec/frontend/jobs/components/trigger_block_spec.js
@@ -1,100 +1,86 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import component from '~/jobs/components/trigger_block.vue';
+import { mount } from '@vue/test-utils';
+import { GlButton, GlTable } from '@gitlab/ui';
+import TriggerBlock from '~/jobs/components/trigger_block.vue';
describe('Trigger block', () => {
- const Component = Vue.extend(component);
- let vm;
+ let wrapper;
+
+ const findRevealButton = () => wrapper.find(GlButton);
+ const findVariableTable = () => wrapper.find(GlTable);
+ const findShortToken = () => wrapper.find('[data-testid="trigger-short-token"]');
+ const findVariableValue = (index) =>
+ wrapper.findAll('[data-testid="trigger-build-value"]').at(index);
+ const findVariableKey = (index) => wrapper.findAll('[data-testid="trigger-build-key"]').at(index);
+
+ const createComponent = (props) => {
+ wrapper = mount(TriggerBlock, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
+ wrapper = null;
});
- describe('with short token', () => {
+ describe('with short token and no variables', () => {
it('renders short token', () => {
- vm = mountComponent(Component, {
+ createComponent({
trigger: {
short_token: '0a666b2',
+ variables: [],
},
});
- expect(vm.$el.querySelector('.js-short-token').textContent).toContain('0a666b2');
+ expect(findShortToken().text()).toContain('0a666b2');
});
});
- describe('without short token', () => {
+ describe('without variables or short token', () => {
+ beforeEach(() => {
+ createComponent({ trigger: { variables: [] } });
+ });
+
it('does not render short token', () => {
- vm = mountComponent(Component, { trigger: {} });
+ expect(findShortToken().exists()).toBe(false);
+ });
- expect(vm.$el.querySelector('.js-short-token')).toBeNull();
+ it('does not render variables', () => {
+ expect(findRevealButton().exists()).toBe(false);
+ expect(findVariableTable().exists()).toBe(false);
});
});
describe('with variables', () => {
describe('hide/reveal variables', () => {
- it('should toggle variables on click', (done) => {
- vm = mountComponent(Component, {
+ it('should toggle variables on click', async () => {
+ const hiddenValue = '••••••';
+ const gcsVar = { key: 'UPLOAD_TO_GCS', value: 'false', public: false };
+ const s3Var = { key: 'UPLOAD_TO_S3', value: 'true', public: false };
+
+ createComponent({
trigger: {
- short_token: 'bd7e',
- variables: [
- { key: 'UPLOAD_TO_GCS', value: 'false', public: false },
- { key: 'UPLOAD_TO_S3', value: 'true', public: false },
- ],
+ variables: [gcsVar, s3Var],
},
});
- vm.$el.querySelector('.js-reveal-variables').click();
-
- vm.$nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.js-build-variables')).not.toBeNull();
- expect(vm.$el.querySelector('.js-reveal-variables').textContent.trim()).toEqual(
- 'Hide values',
- );
-
- expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
- 'UPLOAD_TO_GCS',
- );
+ expect(findRevealButton().text()).toBe('Reveal values');
- expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('false');
- expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
- 'UPLOAD_TO_S3',
- );
+ expect(findVariableValue(0).text()).toBe(hiddenValue);
+ expect(findVariableValue(1).text()).toBe(hiddenValue);
- expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('true');
+ expect(findVariableKey(0).text()).toBe(gcsVar.key);
+ expect(findVariableKey(1).text()).toBe(s3Var.key);
- vm.$el.querySelector('.js-reveal-variables').click();
- })
- .then(vm.$nextTick)
- .then(() => {
- expect(vm.$el.querySelector('.js-reveal-variables').textContent.trim()).toEqual(
- 'Reveal values',
- );
+ await findRevealButton().trigger('click');
- expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
- 'UPLOAD_TO_GCS',
- );
+ expect(findRevealButton().text()).toBe('Hide values');
- expect(vm.$el.querySelector('.js-build-value').textContent).toContain('••••••');
-
- expect(vm.$el.querySelector('.js-build-variables').textContent).toContain(
- 'UPLOAD_TO_S3',
- );
-
- expect(vm.$el.querySelector('.js-build-value').textContent).toContain('••••••');
- })
- .then(done)
- .catch(done.fail);
+ expect(findVariableValue(0).text()).toBe(gcsVar.value);
+ expect(findVariableValue(1).text()).toBe(s3Var.value);
});
});
});
-
- describe('without variables', () => {
- it('does not render variables', () => {
- vm = mountComponent(Component, { trigger: {} });
-
- expect(vm.$el.querySelector('.js-reveal-variables')).toBeNull();
- expect(vm.$el.querySelector('.js-build-variables')).toBeNull();
- });
- });
});
diff --git a/spec/frontend/lib/utils/array_utility_spec.js b/spec/frontend/lib/utils/array_utility_spec.js
new file mode 100644
index 00000000000..b95286ff254
--- /dev/null
+++ b/spec/frontend/lib/utils/array_utility_spec.js
@@ -0,0 +1,32 @@
+import * as arrayUtils from '~/lib/utils/array_utility';
+
+describe('array_utility', () => {
+ describe('swapArrayItems', () => {
+ it.each`
+ array | leftIndex | rightIndex | result
+ ${[]} | ${0} | ${0} | ${[]}
+ ${[1]} | ${0} | ${1} | ${[1]}
+ ${[1, 2]} | ${0} | ${0} | ${[1, 2]}
+ ${[1, 2]} | ${0} | ${1} | ${[2, 1]}
+ ${[1, 2]} | ${1} | ${2} | ${[1, 2]}
+ ${[1, 2]} | ${2} | ${1} | ${[1, 2]}
+ ${[1, 2]} | ${1} | ${10} | ${[1, 2]}
+ ${[1, 2]} | ${10} | ${1} | ${[1, 2]}
+ ${[1, 2]} | ${1} | ${-1} | ${[1, 2]}
+ ${[1, 2]} | ${-1} | ${1} | ${[1, 2]}
+ ${[1, 2, 3]} | ${1} | ${1} | ${[1, 2, 3]}
+ ${[1, 2, 3]} | ${0} | ${2} | ${[3, 2, 1]}
+ ${[1, 2, 3, 4]} | ${0} | ${2} | ${[3, 2, 1, 4]}
+ ${[1, 2, 3, 4, 5]} | ${0} | ${4} | ${[5, 2, 3, 4, 1]}
+ ${[1, 2, 3, 4, 5]} | ${1} | ${2} | ${[1, 3, 2, 4, 5]}
+ ${[1, 2, 3, 4, 5]} | ${2} | ${1} | ${[1, 3, 2, 4, 5]}
+ `(
+ 'given $array with index $leftIndex and $rightIndex will return $result',
+ ({ array, leftIndex, rightIndex, result }) => {
+ const actual = arrayUtils.swapArrayItems(array, leftIndex, rightIndex);
+ expect(actual).toEqual(result);
+ expect(actual).not.toBe(array);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/lib/utils/color_utils_spec.js b/spec/frontend/lib/utils/color_utils_spec.js
index 433e9d5a85e..8c846abd77f 100644
--- a/spec/frontend/lib/utils/color_utils_spec.js
+++ b/spec/frontend/lib/utils/color_utils_spec.js
@@ -1,4 +1,4 @@
-import { textColorForBackground, hexToRgb } from '~/lib/utils/color_utils';
+import { textColorForBackground, hexToRgb, validateHexColor } from '~/lib/utils/color_utils';
describe('Color utils', () => {
describe('Converting hex code to rgb', () => {
@@ -32,4 +32,19 @@ describe('Color utils', () => {
expect(textColorForBackground('#000')).toEqual('#FFFFFF');
});
});
+
+ describe('Validate hex color', () => {
+ it.each`
+ color | output
+ ${undefined} | ${null}
+ ${null} | ${null}
+ ${''} | ${null}
+ ${'ABC123'} | ${false}
+ ${'#ZZZ'} | ${false}
+ ${'#FF0'} | ${true}
+ ${'#FF0000'} | ${true}
+ `('returns $output when $color is given', ({ color, output }) => {
+ expect(validateHexColor(color)).toEqual(output);
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 90222f0f718..18be88a0b8b 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1045,4 +1045,12 @@ describe('common_utils', () => {
expect(commonUtils.getDashPath('/some/url')).toEqual(null);
});
});
+
+ describe('convertArrayToCamelCase', () => {
+ it('returns a new array with snake_case string elements converted camelCase', () => {
+ const result = commonUtils.convertArrayToCamelCase(['hello', 'hello_world']);
+
+ expect(result).toEqual(['hello', 'helloWorld']);
+ });
+ });
});
diff --git a/spec/frontend/lib/utils/datetime_utility_spec.js b/spec/frontend/lib/utils/datetime_utility_spec.js
index 66efd43262b..f2f9362ae99 100644
--- a/spec/frontend/lib/utils/datetime_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime_utility_spec.js
@@ -584,22 +584,6 @@ describe('secondsToMilliseconds', () => {
});
});
-describe('dayAfter', () => {
- const date = new Date('2019-07-16T00:00:00.000Z');
-
- it('returns the following date', () => {
- const nextDay = datetimeUtility.dayAfter(date);
- const expectedNextDate = new Date('2019-07-17T00:00:00.000Z');
-
- expect(nextDay).toStrictEqual(expectedNextDate);
- });
-
- it('does not modifiy the original date', () => {
- datetimeUtility.dayAfter(date);
- expect(date).toStrictEqual(new Date('2019-07-16T00:00:00.000Z'));
- });
-});
-
describe('secondsToDays', () => {
it('converts seconds to days correctly', () => {
expect(datetimeUtility.secondsToDays(0)).toBe(0);
@@ -608,90 +592,214 @@ describe('secondsToDays', () => {
});
});
-describe('nDaysAfter', () => {
- const date = new Date('2019-07-16T00:00:00.000Z');
+describe('date addition/subtraction methods', () => {
+ beforeEach(() => {
+ timezoneMock.register('US/Eastern');
+ });
- it.each`
- numberOfDays | expectedResult
- ${1} | ${new Date('2019-07-17T00:00:00.000Z').valueOf()}
- ${90} | ${new Date('2019-10-14T00:00:00.000Z').valueOf()}
- ${-1} | ${new Date('2019-07-15T00:00:00.000Z').valueOf()}
- ${0} | ${date.valueOf()}
- ${0.9} | ${date.valueOf()}
- `('returns $numberOfDays day(s) after the provided date', ({ numberOfDays, expectedResult }) => {
- expect(datetimeUtility.nDaysAfter(date, numberOfDays)).toBe(expectedResult);
+ afterEach(() => {
+ timezoneMock.unregister();
});
-});
-describe('nDaysBefore', () => {
- const date = new Date('2019-07-16T00:00:00.000Z');
+ describe('dayAfter', () => {
+ const input = '2019-03-10T00:00:00.000Z';
+ const expectedLocalResult = '2019-03-10T23:00:00.000Z';
+ const expectedUTCResult = '2019-03-11T00:00:00.000Z';
+
+ it.each`
+ inputAsString | options | expectedAsString
+ ${input} | ${undefined} | ${expectedLocalResult}
+ ${input} | ${{}} | ${expectedLocalResult}
+ ${input} | ${{ utc: false }} | ${expectedLocalResult}
+ ${input} | ${{ utc: true }} | ${expectedUTCResult}
+ `(
+ 'when the provided date is $inputAsString and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.dayAfter(inputDate, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
+
+ it('does not modifiy the original date', () => {
+ const inputDate = new Date(input);
+ datetimeUtility.dayAfter(inputDate);
+ expect(inputDate.toISOString()).toBe(input);
+ });
+ });
- it.each`
- numberOfDays | expectedResult
- ${1} | ${new Date('2019-07-15T00:00:00.000Z').valueOf()}
- ${90} | ${new Date('2019-04-17T00:00:00.000Z').valueOf()}
- ${-1} | ${new Date('2019-07-17T00:00:00.000Z').valueOf()}
- ${0} | ${date.valueOf()}
- ${0.9} | ${new Date('2019-07-15T00:00:00.000Z').valueOf()}
- `('returns $numberOfDays day(s) before the provided date', ({ numberOfDays, expectedResult }) => {
- expect(datetimeUtility.nDaysBefore(date, numberOfDays)).toBe(expectedResult);
+ describe('nDaysAfter', () => {
+ const input = '2019-07-16T00:00:00.000Z';
+
+ it.each`
+ inputAsString | numberOfDays | options | expectedAsString
+ ${input} | ${1} | ${undefined} | ${'2019-07-17T00:00:00.000Z'}
+ ${input} | ${-1} | ${undefined} | ${'2019-07-15T00:00:00.000Z'}
+ ${input} | ${0} | ${undefined} | ${'2019-07-16T00:00:00.000Z'}
+ ${input} | ${0.9} | ${undefined} | ${'2019-07-16T00:00:00.000Z'}
+ ${input} | ${120} | ${undefined} | ${'2019-11-13T01:00:00.000Z'}
+ ${input} | ${120} | ${{}} | ${'2019-11-13T01:00:00.000Z'}
+ ${input} | ${120} | ${{ utc: false }} | ${'2019-11-13T01:00:00.000Z'}
+ ${input} | ${120} | ${{ utc: true }} | ${'2019-11-13T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString, numberOfDays is $numberOfDays, and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, numberOfDays, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.nDaysAfter(inputDate, numberOfDays, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
});
-});
-describe('nMonthsAfter', () => {
- // February has 28 days
- const feb2019 = new Date('2019-02-15T00:00:00.000Z');
- // Except in 2020, it had 29 days
- const feb2020 = new Date('2020-02-15T00:00:00.000Z');
- // April has 30 days
- const apr2020 = new Date('2020-04-15T00:00:00.000Z');
- // May has 31 days
- const may2020 = new Date('2020-05-15T00:00:00.000Z');
+ describe('nDaysBefore', () => {
+ const input = '2019-07-16T00:00:00.000Z';
+
+ it.each`
+ inputAsString | numberOfDays | options | expectedAsString
+ ${input} | ${1} | ${undefined} | ${'2019-07-15T00:00:00.000Z'}
+ ${input} | ${-1} | ${undefined} | ${'2019-07-17T00:00:00.000Z'}
+ ${input} | ${0} | ${undefined} | ${'2019-07-16T00:00:00.000Z'}
+ ${input} | ${0.9} | ${undefined} | ${'2019-07-15T00:00:00.000Z'}
+ ${input} | ${180} | ${undefined} | ${'2019-01-17T01:00:00.000Z'}
+ ${input} | ${180} | ${{}} | ${'2019-01-17T01:00:00.000Z'}
+ ${input} | ${180} | ${{ utc: false }} | ${'2019-01-17T01:00:00.000Z'}
+ ${input} | ${180} | ${{ utc: true }} | ${'2019-01-17T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString, numberOfDays is $numberOfDays, and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, numberOfDays, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.nDaysBefore(inputDate, numberOfDays, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
+ });
- it.each`
- date | numberOfMonths | expectedResult
- ${feb2019} | ${1} | ${new Date('2019-03-15T00:00:00.000Z').valueOf()}
- ${feb2020} | ${1} | ${new Date('2020-03-15T00:00:00.000Z').valueOf()}
- ${apr2020} | ${1} | ${new Date('2020-05-15T00:00:00.000Z').valueOf()}
- ${may2020} | ${1} | ${new Date('2020-06-15T00:00:00.000Z').valueOf()}
- ${may2020} | ${12} | ${new Date('2021-05-15T00:00:00.000Z').valueOf()}
- ${may2020} | ${-1} | ${new Date('2020-04-15T00:00:00.000Z').valueOf()}
- ${may2020} | ${0} | ${may2020.valueOf()}
- ${may2020} | ${0.9} | ${may2020.valueOf()}
- `(
- 'returns $numberOfMonths month(s) after the provided date',
- ({ date, numberOfMonths, expectedResult }) => {
- expect(datetimeUtility.nMonthsAfter(date, numberOfMonths)).toBe(expectedResult);
- },
- );
-});
+ describe('nWeeksAfter', () => {
+ const input = '2021-07-16T00:00:00.000Z';
+
+ it.each`
+ inputAsString | numberOfWeeks | options | expectedAsString
+ ${input} | ${1} | ${undefined} | ${'2021-07-23T00:00:00.000Z'}
+ ${input} | ${3} | ${undefined} | ${'2021-08-06T00:00:00.000Z'}
+ ${input} | ${-1} | ${undefined} | ${'2021-07-09T00:00:00.000Z'}
+ ${input} | ${0} | ${undefined} | ${'2021-07-16T00:00:00.000Z'}
+ ${input} | ${0.6} | ${undefined} | ${'2021-07-20T00:00:00.000Z'}
+ ${input} | ${18} | ${undefined} | ${'2021-11-19T01:00:00.000Z'}
+ ${input} | ${18} | ${{}} | ${'2021-11-19T01:00:00.000Z'}
+ ${input} | ${18} | ${{ utc: false }} | ${'2021-11-19T01:00:00.000Z'}
+ ${input} | ${18} | ${{ utc: true }} | ${'2021-11-19T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString, numberOfWeeks is $numberOfWeeks, and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, numberOfWeeks, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.nWeeksAfter(inputDate, numberOfWeeks, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
+ });
-describe('nMonthsBefore', () => {
- // The previous month (February) has 28 days
- const march2019 = new Date('2019-03-15T00:00:00.000Z');
- // Except in 2020, it had 29 days
- const march2020 = new Date('2020-03-15T00:00:00.000Z');
- // The previous month (April) has 30 days
- const may2020 = new Date('2020-05-15T00:00:00.000Z');
- // The previous month (May) has 31 days
- const june2020 = new Date('2020-06-15T00:00:00.000Z');
+ describe('nWeeksBefore', () => {
+ const input = '2021-07-16T00:00:00.000Z';
+
+ it.each`
+ inputAsString | numberOfWeeks | options | expectedAsString
+ ${input} | ${1} | ${undefined} | ${'2021-07-09T00:00:00.000Z'}
+ ${input} | ${3} | ${undefined} | ${'2021-06-25T00:00:00.000Z'}
+ ${input} | ${-1} | ${undefined} | ${'2021-07-23T00:00:00.000Z'}
+ ${input} | ${0} | ${undefined} | ${'2021-07-16T00:00:00.000Z'}
+ ${input} | ${0.6} | ${undefined} | ${'2021-07-11T00:00:00.000Z'}
+ ${input} | ${20} | ${undefined} | ${'2021-02-26T01:00:00.000Z'}
+ ${input} | ${20} | ${{}} | ${'2021-02-26T01:00:00.000Z'}
+ ${input} | ${20} | ${{ utc: false }} | ${'2021-02-26T01:00:00.000Z'}
+ ${input} | ${20} | ${{ utc: true }} | ${'2021-02-26T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString, numberOfWeeks is $numberOfWeeks, and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, numberOfWeeks, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.nWeeksBefore(inputDate, numberOfWeeks, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
+ });
- it.each`
- date | numberOfMonths | expectedResult
- ${march2019} | ${1} | ${new Date('2019-02-15T00:00:00.000Z').valueOf()}
- ${march2020} | ${1} | ${new Date('2020-02-15T00:00:00.000Z').valueOf()}
- ${may2020} | ${1} | ${new Date('2020-04-15T00:00:00.000Z').valueOf()}
- ${june2020} | ${1} | ${new Date('2020-05-15T00:00:00.000Z').valueOf()}
- ${june2020} | ${12} | ${new Date('2019-06-15T00:00:00.000Z').valueOf()}
- ${june2020} | ${-1} | ${new Date('2020-07-15T00:00:00.000Z').valueOf()}
- ${june2020} | ${0} | ${june2020.valueOf()}
- ${june2020} | ${0.9} | ${new Date('2020-05-15T00:00:00.000Z').valueOf()}
- `(
- 'returns $numberOfMonths month(s) before the provided date',
- ({ date, numberOfMonths, expectedResult }) => {
- expect(datetimeUtility.nMonthsBefore(date, numberOfMonths)).toBe(expectedResult);
- },
- );
+ describe('nMonthsAfter', () => {
+ // February has 28 days
+ const feb2019 = '2019-02-15T00:00:00.000Z';
+ // Except in 2020, it had 29 days
+ const feb2020 = '2020-02-15T00:00:00.000Z';
+ // April has 30 days
+ const apr2020 = '2020-04-15T00:00:00.000Z';
+ // May has 31 days
+ const may2020 = '2020-05-15T00:00:00.000Z';
+ // November 1, 2020 was the day Daylight Saving Time ended in 2020 (in the US)
+ const oct2020 = '2020-10-15T00:00:00.000Z';
+
+ it.each`
+ inputAsString | numberOfMonths | options | expectedAsString
+ ${feb2019} | ${1} | ${undefined} | ${'2019-03-14T23:00:00.000Z'}
+ ${feb2020} | ${1} | ${undefined} | ${'2020-03-14T23:00:00.000Z'}
+ ${apr2020} | ${1} | ${undefined} | ${'2020-05-15T00:00:00.000Z'}
+ ${may2020} | ${1} | ${undefined} | ${'2020-06-15T00:00:00.000Z'}
+ ${may2020} | ${12} | ${undefined} | ${'2021-05-15T00:00:00.000Z'}
+ ${may2020} | ${-1} | ${undefined} | ${'2020-04-15T00:00:00.000Z'}
+ ${may2020} | ${0} | ${undefined} | ${may2020}
+ ${may2020} | ${0.9} | ${undefined} | ${may2020}
+ ${oct2020} | ${1} | ${undefined} | ${'2020-11-15T01:00:00.000Z'}
+ ${oct2020} | ${1} | ${{}} | ${'2020-11-15T01:00:00.000Z'}
+ ${oct2020} | ${1} | ${{ utc: false }} | ${'2020-11-15T01:00:00.000Z'}
+ ${oct2020} | ${1} | ${{ utc: true }} | ${'2020-11-15T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString, numberOfMonths is $numberOfMonths, and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, numberOfMonths, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.nMonthsAfter(inputDate, numberOfMonths, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
+ });
+
+ describe('nMonthsBefore', () => {
+ // The previous month (February) has 28 days
+ const march2019 = '2019-03-15T00:00:00.000Z';
+ // Except in 2020, it had 29 days
+ const march2020 = '2020-03-15T00:00:00.000Z';
+ // The previous month (April) has 30 days
+ const may2020 = '2020-05-15T00:00:00.000Z';
+ // The previous month (May) has 31 days
+ const june2020 = '2020-06-15T00:00:00.000Z';
+ // November 1, 2020 was the day Daylight Saving Time ended in 2020 (in the US)
+ const nov2020 = '2020-11-15T00:00:00.000Z';
+
+ it.each`
+ inputAsString | numberOfMonths | options | expectedAsString
+ ${march2019} | ${1} | ${undefined} | ${'2019-02-15T01:00:00.000Z'}
+ ${march2020} | ${1} | ${undefined} | ${'2020-02-15T01:00:00.000Z'}
+ ${may2020} | ${1} | ${undefined} | ${'2020-04-15T00:00:00.000Z'}
+ ${june2020} | ${1} | ${undefined} | ${'2020-05-15T00:00:00.000Z'}
+ ${june2020} | ${12} | ${undefined} | ${'2019-06-15T00:00:00.000Z'}
+ ${june2020} | ${-1} | ${undefined} | ${'2020-07-15T00:00:00.000Z'}
+ ${june2020} | ${0} | ${undefined} | ${june2020}
+ ${june2020} | ${0.9} | ${undefined} | ${'2020-05-15T00:00:00.000Z'}
+ ${nov2020} | ${1} | ${undefined} | ${'2020-10-14T23:00:00.000Z'}
+ ${nov2020} | ${1} | ${{}} | ${'2020-10-14T23:00:00.000Z'}
+ ${nov2020} | ${1} | ${{ utc: false }} | ${'2020-10-14T23:00:00.000Z'}
+ ${nov2020} | ${1} | ${{ utc: true }} | ${'2020-10-15T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString, numberOfMonths is $numberOfMonths, and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, numberOfMonths, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.nMonthsBefore(inputDate, numberOfMonths, options);
+
+ expect(actual.toISOString()).toBe(expectedAsString);
+ },
+ );
+ });
});
describe('approximateDuration', () => {
@@ -843,7 +951,7 @@ describe('format24HourTimeStringFromInt', () => {
});
});
-describe('getOverlappingDaysInPeriods', () => {
+describe('getOverlapDateInPeriods', () => {
const start = new Date(2021, 0, 11);
const end = new Date(2021, 0, 13);
@@ -851,14 +959,15 @@ describe('getOverlappingDaysInPeriods', () => {
const givenPeriodLeft = new Date(2021, 0, 11);
const givenPeriodRight = new Date(2021, 0, 14);
- it('returns an overlap object that contains the amount of days overlapping, start date of overlap and end date of overlap', () => {
+ it('returns an overlap object that contains the amount of days overlapping, the amount of hours overlapping, start date of overlap and end date of overlap', () => {
expect(
- datetimeUtility.getOverlappingDaysInPeriods(
+ datetimeUtility.getOverlapDateInPeriods(
{ start, end },
{ start: givenPeriodLeft, end: givenPeriodRight },
),
).toEqual({
daysOverlap: 2,
+ hoursOverlap: 48,
overlapStartDate: givenPeriodLeft.getTime(),
overlapEndDate: end.getTime(),
});
@@ -871,7 +980,7 @@ describe('getOverlappingDaysInPeriods', () => {
it('returns an overlap object that contains a 0 value for days overlapping', () => {
expect(
- datetimeUtility.getOverlappingDaysInPeriods(
+ datetimeUtility.getOverlapDateInPeriods(
{ start, end },
{ start: givenPeriodLeft, end: givenPeriodRight },
),
@@ -886,14 +995,54 @@ describe('getOverlappingDaysInPeriods', () => {
it('throws an exception when the left period contains an invalid date', () => {
expect(() =>
- datetimeUtility.getOverlappingDaysInPeriods({ start, end }, { start: startInvalid, end }),
+ datetimeUtility.getOverlapDateInPeriods({ start, end }, { start: startInvalid, end }),
).toThrow(error);
});
it('throws an exception when the right period contains an invalid date', () => {
expect(() =>
- datetimeUtility.getOverlappingDaysInPeriods({ start, end }, { start, end: endInvalid }),
+ datetimeUtility.getOverlapDateInPeriods({ start, end }, { start, end: endInvalid }),
).toThrow(error);
});
});
});
+
+describe('isToday', () => {
+ const today = new Date();
+ it.each`
+ date | expected | negation
+ ${today} | ${true} | ${'is'}
+ ${new Date('2021-01-21T12:00:00.000Z')} | ${false} | ${'is NOT'}
+ `('returns $expected as $date $negation today', ({ date, expected }) => {
+ expect(datetimeUtility.isToday(date)).toBe(expected);
+ });
+});
+
+describe('getStartOfDay', () => {
+ beforeEach(() => {
+ timezoneMock.register('US/Eastern');
+ });
+
+ afterEach(() => {
+ timezoneMock.unregister();
+ });
+
+ it.each`
+ inputAsString | options | expectedAsString
+ ${'2021-01-29T18:08:23.014Z'} | ${undefined} | ${'2021-01-29T05:00:00.000Z'}
+ ${'2021-01-29T13:08:23.014-05:00'} | ${undefined} | ${'2021-01-29T05:00:00.000Z'}
+ ${'2021-01-30T03:08:23.014+09:00'} | ${undefined} | ${'2021-01-29T05:00:00.000Z'}
+ ${'2021-01-28T18:08:23.014-10:00'} | ${undefined} | ${'2021-01-28T05:00:00.000Z'}
+ ${'2021-01-28T18:08:23.014-10:00'} | ${{}} | ${'2021-01-28T05:00:00.000Z'}
+ ${'2021-01-28T18:08:23.014-10:00'} | ${{ utc: false }} | ${'2021-01-28T05:00:00.000Z'}
+ ${'2021-01-28T18:08:23.014-10:00'} | ${{ utc: true }} | ${'2021-01-29T00:00:00.000Z'}
+ `(
+ 'when the provided date is $inputAsString and the options parameter is $options, returns $expectedAsString',
+ ({ inputAsString, options, expectedAsString }) => {
+ const inputDate = new Date(inputAsString);
+ const actual = datetimeUtility.getStartOfDay(inputDate, options);
+
+ expect(actual.toISOString()).toEqual(expectedAsString);
+ },
+ );
+});
diff --git a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
index 26b942c3567..0ca70e0a77e 100644
--- a/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
+++ b/spec/frontend/lib/utils/unit_format/formatter_factory_spec.js
@@ -36,6 +36,27 @@ describe('unit_format/formatter_factory', () => {
expect(formatNumber(10 ** 7, undefined, 9)).toBe('1.00e+7');
expect(formatNumber(10 ** 7, undefined, 10)).toBe('10,000,000');
});
+
+ describe('formats with a different locale', () => {
+ let originalLang;
+
+ beforeAll(() => {
+ originalLang = document.documentElement.lang;
+ document.documentElement.lang = 'es';
+ });
+
+ afterAll(() => {
+ document.documentElement.lang = originalLang;
+ });
+
+ it('formats a using the correct thousands separator', () => {
+ expect(formatNumber(1000000)).toBe('1.000.000');
+ });
+
+ it('formats a using the correct decimal separator', () => {
+ expect(formatNumber(12.345)).toBe('12,345');
+ });
+ });
});
describe('suffixFormatter', () => {
diff --git a/spec/frontend/logs/components/log_advanced_filters_spec.js b/spec/frontend/logs/components/log_advanced_filters_spec.js
index dfa8913a301..1fc8d943215 100644
--- a/spec/frontend/logs/components/log_advanced_filters_spec.js
+++ b/spec/frontend/logs/components/log_advanced_filters_spec.js
@@ -4,9 +4,8 @@ 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';
-import { mockPods, mockSearch } from '../mock_data';
-
import LogAdvancedFilters from '~/logs/components/log_advanced_filters.vue';
+import { mockPods, mockSearch } from '../mock_data';
const module = 'environmentLogs';
diff --git a/spec/frontend/logs/components/log_simple_filters_spec.js b/spec/frontend/logs/components/log_simple_filters_spec.js
index 5bd42fd7dbc..5d2b22da3b5 100644
--- a/spec/frontend/logs/components/log_simple_filters_spec.js
+++ b/spec/frontend/logs/components/log_simple_filters_spec.js
@@ -1,9 +1,8 @@
import { GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { createStore } from '~/logs/stores';
-import { mockPods, mockPodName } from '../mock_data';
-
import LogSimpleFilters from '~/logs/components/log_simple_filters.vue';
+import { mockPods, mockPodName } from '../mock_data';
const module = 'environmentLogs';
diff --git a/spec/frontend/logs/stores/actions_spec.js b/spec/frontend/logs/stores/actions_spec.js
index bc58f1e677f..471b8603a94 100644
--- a/spec/frontend/logs/stores/actions_spec.js
+++ b/spec/frontend/logs/stores/actions_spec.js
@@ -19,6 +19,7 @@ import { defaultTimeRange } from '~/vue_shared/constants';
import axios from '~/lib/utils/axios_utils';
import { deprecatedCreateFlash as flash } from '~/flash';
+import { TOKEN_TYPE_POD_NAME } from '~/logs/constants';
import {
mockPodName,
mockEnvironmentsEndpoint,
@@ -34,7 +35,6 @@ import {
mockManagedApps,
mockManagedAppsEndpoint,
} from '../mock_data';
-import { TOKEN_TYPE_POD_NAME } from '~/logs/constants';
jest.mock('~/flash');
jest.mock('~/lib/utils/datetime_range');
diff --git a/spec/frontend/groups/members/components/app_spec.js b/spec/frontend/members/components/app_spec.js
index 9847dacbec8..8fac0f5c5e6 100644
--- a/spec/frontend/groups/members/components/app_spec.js
+++ b/spec/frontend/members/components/app_spec.js
@@ -2,13 +2,13 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import { GlAlert } from '@gitlab/ui';
-import App from '~/groups/members/components/app.vue';
+import MembersApp from '~/members/components/app.vue';
import FilterSortContainer from '~/members/components/filter_sort/filter_sort_container.vue';
import * as commonUtils from '~/lib/utils/common_utils';
import { RECEIVE_MEMBER_ROLE_ERROR, HIDE_ERROR } from '~/members/store/mutation_types';
import mutations from '~/members/store/mutations';
-describe('GroupMembersApp', () => {
+describe('MembersApp', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -25,7 +25,7 @@ describe('GroupMembersApp', () => {
mutations,
});
- wrapper = shallowMount(App, {
+ wrapper = shallowMount(MembersApp, {
localVue,
store,
...options,
@@ -48,7 +48,7 @@ describe('GroupMembersApp', () => {
it('renders and scrolls to error alert', async () => {
createComponent({ showError: false, errorMessage: '' });
- store.commit(RECEIVE_MEMBER_ROLE_ERROR);
+ store.commit(RECEIVE_MEMBER_ROLE_ERROR, { error: new Error('Network Error') });
await nextTick();
diff --git a/spec/frontend/members/components/avatars/group_avatar_spec.js b/spec/frontend/members/components/avatars/group_avatar_spec.js
index 658bb9462b0..2720181aeb8 100644
--- a/spec/frontend/members/components/avatars/group_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/group_avatar_spec.js
@@ -1,8 +1,8 @@
import { mount, createWrapper } from '@vue/test-utils';
import { getByText as getByTextHelper } from '@testing-library/dom';
import { GlAvatarLink } from '@gitlab/ui';
-import { group as member } from '../../mock_data';
import GroupAvatar from '~/members/components/avatars/group_avatar.vue';
+import { group as member } from '../../mock_data';
describe('MemberList', () => {
let wrapper;
diff --git a/spec/frontend/members/components/avatars/invite_avatar_spec.js b/spec/frontend/members/components/avatars/invite_avatar_spec.js
index 13ee727528b..8ebfdf31127 100644
--- a/spec/frontend/members/components/avatars/invite_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/invite_avatar_spec.js
@@ -1,7 +1,7 @@
import { mount, createWrapper } from '@vue/test-utils';
import { getByText as getByTextHelper } from '@testing-library/dom';
-import { invite as member } from '../../mock_data';
import InviteAvatar from '~/members/components/avatars/invite_avatar.vue';
+import { invite as member } from '../../mock_data';
describe('MemberList', () => {
let wrapper;
diff --git a/spec/frontend/members/components/avatars/user_avatar_spec.js b/spec/frontend/members/components/avatars/user_avatar_spec.js
index 411ec1a54de..d6305652616 100644
--- a/spec/frontend/members/components/avatars/user_avatar_spec.js
+++ b/spec/frontend/members/components/avatars/user_avatar_spec.js
@@ -1,8 +1,8 @@
import { mount, createWrapper } from '@vue/test-utils';
import { within } from '@testing-library/dom';
import { GlAvatarLink, GlBadge } from '@gitlab/ui';
-import { member as memberMock, orphanedMember } from '../../mock_data';
import UserAvatar from '~/members/components/avatars/user_avatar.vue';
+import { member as memberMock, orphanedMember } from '../../mock_data';
describe('UserAvatar', () => {
let wrapper;
diff --git a/spec/frontend/members/components/table/member_action_buttons_spec.js b/spec/frontend/members/components/table/member_action_buttons_spec.js
index b7a6df3d054..064c8403423 100644
--- a/spec/frontend/members/components/table/member_action_buttons_spec.js
+++ b/spec/frontend/members/components/table/member_action_buttons_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
import { MEMBER_TYPES } from '~/members/constants';
-import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
import GroupActionButtons from '~/members/components/action_buttons/group_action_buttons.vue';
import InviteActionButtons from '~/members/components/action_buttons/invite_action_buttons.vue';
import AccessRequestActionButtons from '~/members/components/action_buttons/access_request_action_buttons.vue';
+import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
describe('MemberActionButtons', () => {
let wrapper;
diff --git a/spec/frontend/members/components/table/member_avatar_spec.js b/spec/frontend/members/components/table/member_avatar_spec.js
index 4341dfbbaf9..a6f9f157532 100644
--- a/spec/frontend/members/components/table/member_avatar_spec.js
+++ b/spec/frontend/members/components/table/member_avatar_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { MEMBER_TYPES } from '~/members/constants';
-import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
import MemberAvatar from '~/members/components/table/member_avatar.vue';
import UserAvatar from '~/members/components/avatars/user_avatar.vue';
import GroupAvatar from '~/members/components/avatars/group_avatar.vue';
import InviteAvatar from '~/members/components/avatars/invite_avatar.vue';
+import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
describe('MemberList', () => {
let wrapper;
diff --git a/spec/frontend/members/components/table/members_table_cell_spec.js b/spec/frontend/members/components/table/members_table_cell_spec.js
index 117c9255c00..69795a4d670 100644
--- a/spec/frontend/members/components/table/members_table_cell_spec.js
+++ b/spec/frontend/members/components/table/members_table_cell_spec.js
@@ -1,8 +1,15 @@
import { mount, createLocalVue } from '@vue/test-utils';
import Vuex from 'vuex';
import { MEMBER_TYPES } from '~/members/constants';
-import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
import MembersTableCell from '~/members/components/table/members_table_cell.vue';
+import {
+ member as memberMock,
+ directMember,
+ inheritedMember,
+ group,
+ invite,
+ accessRequest,
+} from '../../mock_data';
describe('MembersTableCell', () => {
const WrappedComponent = {
@@ -31,7 +38,7 @@ describe('MembersTableCell', () => {
const localVue = createLocalVue();
localVue.use(Vuex);
- localVue.component('wrapped-component', WrappedComponent);
+ localVue.component('WrappedComponent', WrappedComponent);
const createStore = (state = {}) => {
return new Vuex.Store({
@@ -75,19 +82,12 @@ describe('MembersTableCell', () => {
const createComponentWithDirectMember = (member = {}) => {
createComponent({
- member: {
- ...memberMock,
- source: {
- ...memberMock.source,
- id: 1,
- },
- ...member,
- },
+ member: { ...directMember, ...member },
});
};
const createComponentWithInheritedMember = (member = {}) => {
createComponent({
- member: { ...memberMock, ...member },
+ member: { ...inheritedMember, ...member },
});
};
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index dbaccde069c..729b65f37aa 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -15,7 +15,7 @@ import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
import * as initUserPopovers from '~/user_popovers';
-import { member as memberMock, invite, accessRequest } from '../../mock_data';
+import { member as memberMock, directMember, invite, accessRequest } from '../../mock_data';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -74,11 +74,6 @@ describe('MembersTable', () => {
});
describe('fields', () => {
- const directMember = {
- ...memberMock,
- source: { ...memberMock.source, id: 1 },
- };
-
const memberCanUpdate = {
...directMember,
canUpdate: true,
@@ -154,7 +149,7 @@ describe('MembersTable', () => {
expect(findTableCellByMemberId('Actions', members[0].id).classes()).toStrictEqual([
'col-actions',
'gl-display-none!',
- 'gl-display-lg-table-cell!',
+ 'gl-lg-display-table-cell!',
]);
expect(findTableCellByMemberId('Actions', members[1].id).classes()).toStrictEqual([
'col-actions',
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index 96a388614f3..3cc52379026 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -6,6 +6,7 @@ import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import waitForPromises from 'helpers/wait_for_promises';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
+import { BV_DROPDOWN_SHOW } from '~/lib/utils/constants';
import { member } from '../../mock_data';
const localVue = createLocalVue();
@@ -67,7 +68,7 @@ describe('RoleDropdown', () => {
createComponent();
findDropdownToggle().trigger('click');
- wrapper.vm.$root.$on('bv::dropdown::shown', () => {
+ wrapper.vm.$root.$on(BV_DROPDOWN_SHOW, () => {
done();
});
});
diff --git a/spec/frontend/groups/members/index_spec.js b/spec/frontend/members/index_spec.js
index 5c717e53229..d73e3b03f07 100644
--- a/spec/frontend/groups/members/index_spec.js
+++ b/spec/frontend/members/index_spec.js
@@ -1,15 +1,15 @@
import { createWrapper } from '@vue/test-utils';
-import { initGroupMembersApp } from '~/groups/members';
-import GroupMembersApp from '~/groups/members/components/app.vue';
-import { membersJsonString, membersParsed } from './mock_data';
+import { initMembersApp } from '~/members/index';
+import MembersApp from '~/members/components/app.vue';
+import { membersJsonString, members } from './mock_data';
-describe('initGroupMembersApp', () => {
+describe('initMembersApp', () => {
let el;
let vm;
let wrapper;
const setup = () => {
- vm = initGroupMembersApp(el, {
+ vm = initMembersApp(el, {
tableFields: ['account'],
tableAttrs: { table: { 'data-qa-selector': 'members_list' } },
tableSortableFields: ['account'],
@@ -22,7 +22,7 @@ describe('initGroupMembersApp', () => {
beforeEach(() => {
el = document.createElement('div');
el.setAttribute('data-members', membersJsonString);
- el.setAttribute('data-group-id', '234');
+ el.setAttribute('data-source-id', '234');
el.setAttribute('data-can-manage-members', 'true');
el.setAttribute('data-member-path', '/groups/foo-bar/-/group_members/:id');
@@ -36,10 +36,10 @@ describe('initGroupMembersApp', () => {
wrapper = null;
});
- it('renders `GroupMembersApp`', () => {
+ it('renders `MembersApp`', () => {
setup();
- expect(wrapper.find(GroupMembersApp).exists()).toBe(true);
+ expect(wrapper.find(MembersApp).exists()).toBe(true);
});
it('sets `currentUserId` in Vuex store', () => {
@@ -57,7 +57,7 @@ describe('initGroupMembersApp', () => {
});
});
- it('parses and sets `data-group-id` as `sourceId` in Vuex store', () => {
+ it('parses and sets `data-source-id` as `sourceId` in Vuex store', () => {
setup();
expect(vm.$store.state.sourceId).toBe(234);
@@ -72,7 +72,7 @@ describe('initGroupMembersApp', () => {
it('parses and sets `members` in Vuex store', () => {
setup();
- expect(vm.$store.state.members).toEqual(membersParsed);
+ expect(vm.$store.state.members).toEqual(members);
});
it('sets `tableFields` in Vuex store', () => {
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index e668f2a1998..fa324ce1cf9 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -4,6 +4,7 @@ export const member = {
canRemove: false,
canOverride: false,
isOverridden: false,
+ isDirectMember: false,
accessLevel: { integerValue: 50, stringValue: 'Owner' },
source: {
id: 178,
@@ -69,3 +70,8 @@ export const accessRequest = {
};
export const members = [member];
+
+export const membersJsonString = JSON.stringify(members);
+
+export const directMember = { ...member, isDirectMember: true };
+export const inheritedMember = { ...member, isDirectMember: false };
diff --git a/spec/frontend/members/store/actions_spec.js b/spec/frontend/members/store/actions_spec.js
index 5424fee0750..b0f863e6163 100644
--- a/spec/frontend/members/store/actions_spec.js
+++ b/spec/frontend/members/store/actions_spec.js
@@ -57,15 +57,17 @@ describe('Vuex members actions', () => {
describe('unsuccessful request', () => {
it(`commits ${types.RECEIVE_MEMBER_ROLE_ERROR} mutation and throws error`, async () => {
- mock.onPut().networkError();
+ const error = new Error('Network Error');
+ mock.onPut().reply(() => Promise.reject(error));
await expect(
testAction(updateMemberRole, payload, state, [
{
type: types.RECEIVE_MEMBER_ROLE_ERROR,
+ payload: { error },
},
]),
- ).rejects.toThrowError(new Error('Network Error'));
+ ).rejects.toThrowError(error);
});
});
});
@@ -108,15 +110,17 @@ describe('Vuex members actions', () => {
describe('unsuccessful request', () => {
it(`commits ${types.RECEIVE_MEMBER_EXPIRATION_ERROR} mutation and throws error`, async () => {
- mock.onPut().networkError();
+ const error = new Error('Network Error');
+ mock.onPut().reply(() => Promise.reject(error));
await expect(
testAction(updateMemberExpiration, { memberId, expiresAt }, state, [
{
type: types.RECEIVE_MEMBER_EXPIRATION_ERROR,
+ payload: { error },
},
]),
- ).rejects.toThrowError(new Error('Network Error'));
+ ).rejects.toThrowError(error);
});
});
});
diff --git a/spec/frontend/members/store/mutations_spec.js b/spec/frontend/members/store/mutations_spec.js
index 488bfdf15fd..a56e6a72d9e 100644
--- a/spec/frontend/members/store/mutations_spec.js
+++ b/spec/frontend/members/store/mutations_spec.js
@@ -28,13 +28,33 @@ describe('Vuex members mutations', () => {
});
describe(types.RECEIVE_MEMBER_ROLE_ERROR, () => {
- it('shows error message', () => {
- mutations[types.RECEIVE_MEMBER_ROLE_ERROR](state);
+ describe('when error does not have a message', () => {
+ it('shows default error message', () => {
+ mutations[types.RECEIVE_MEMBER_ROLE_ERROR](state, {
+ error: new Error('Network Error'),
+ });
+
+ expect(state.showError).toBe(true);
+ expect(state.errorMessage).toBe(
+ "An error occurred while updating the member's role, please try again.",
+ );
+ });
+ });
+
+ describe('when error has a message', () => {
+ it('shows error message', () => {
+ const error = new Error('Request failed with status code 422');
+ const message =
+ 'User email "john.smith@gmail.com" does not match the allowed domain of example.com';
- expect(state.showError).toBe(true);
- expect(state.errorMessage).toBe(
- "An error occurred while updating the member's role, please try again.",
- );
+ error.response = {
+ data: { message },
+ };
+ mutations[types.RECEIVE_MEMBER_ROLE_ERROR](state, { error });
+
+ expect(state.showError).toBe(true);
+ expect(state.errorMessage).toBe(message);
+ });
});
});
@@ -52,13 +72,33 @@ describe('Vuex members mutations', () => {
});
describe(types.RECEIVE_MEMBER_EXPIRATION_ERROR, () => {
- it('shows error message', () => {
- mutations[types.RECEIVE_MEMBER_EXPIRATION_ERROR](state);
+ describe('when error does not have a message', () => {
+ it('shows default error message', () => {
+ mutations[types.RECEIVE_MEMBER_EXPIRATION_ERROR](state, {
+ error: new Error('Network Error'),
+ });
+
+ expect(state.showError).toBe(true);
+ expect(state.errorMessage).toBe(
+ "An error occurred while updating the member's expiration date, please try again.",
+ );
+ });
+ });
+
+ describe('when error has a message', () => {
+ it('shows error message', () => {
+ const error = new Error('Request failed with status code 422');
+ const message =
+ 'User email "john.smith@gmail.com" does not match the allowed domain of example.com';
- expect(state.showError).toBe(true);
- expect(state.errorMessage).toBe(
- "An error occurred while updating the member's expiration date, please try again.",
- );
+ error.response = {
+ data: { message },
+ };
+ mutations[types.RECEIVE_MEMBER_EXPIRATION_ERROR](state, { error });
+
+ expect(state.showError).toBe(true);
+ expect(state.errorMessage).toBe(message);
+ });
});
});
});
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index 7cd4e735b55..5c9484fe128 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -9,12 +9,20 @@ import {
canOverride,
parseSortParam,
buildSortHref,
+ parseDataAttributes,
+ groupLinkRequestFormatter,
} from '~/members/utils';
import { DEFAULT_SORT } from '~/members/constants';
-import { member as memberMock, group, invite } from './mock_data';
+import {
+ member as memberMock,
+ directMember,
+ inheritedMember,
+ group,
+ invite,
+ membersJsonString,
+ members,
+} from './mock_data';
-const DIRECT_MEMBER_ID = 178;
-const INHERITED_MEMBER_ID = 179;
const IS_CURRENT_USER_ID = 123;
const IS_NOT_CURRENT_USER_ID = 124;
const URL_HOST = 'https://localhost/';
@@ -57,11 +65,11 @@ describe('Members Utils', () => {
describe('isDirectMember', () => {
test.each`
- sourceId | expected
- ${DIRECT_MEMBER_ID} | ${true}
- ${INHERITED_MEMBER_ID} | ${false}
- `('returns $expected', ({ sourceId, expected }) => {
- expect(isDirectMember(memberMock, sourceId)).toBe(expected);
+ member | expected
+ ${directMember} | ${true}
+ ${inheritedMember} | ${false}
+ `('returns $expected', ({ member, expected }) => {
+ expect(isDirectMember(member)).toBe(expected);
});
});
@@ -76,18 +84,13 @@ describe('Members Utils', () => {
});
describe('canRemove', () => {
- const memberCanRemove = {
- ...memberMock,
- canRemove: true,
- };
-
test.each`
- member | sourceId | expected
- ${memberCanRemove} | ${DIRECT_MEMBER_ID} | ${true}
- ${memberCanRemove} | ${INHERITED_MEMBER_ID} | ${false}
- ${memberMock} | ${INHERITED_MEMBER_ID} | ${false}
- `('returns $expected', ({ member, sourceId, expected }) => {
- expect(canRemove(member, sourceId)).toBe(expected);
+ member | expected
+ ${{ ...directMember, canRemove: true }} | ${true}
+ ${{ ...inheritedMember, canRemove: true }} | ${false}
+ ${{ ...memberMock, canRemove: false }} | ${false}
+ `('returns $expected', ({ member, expected }) => {
+ expect(canRemove(member)).toBe(expected);
});
});
@@ -96,25 +99,20 @@ describe('Members Utils', () => {
member | expected
${invite} | ${true}
${{ ...invite, invite: { ...invite.invite, canResend: false } }} | ${false}
- `('returns $expected', ({ member, sourceId, expected }) => {
- expect(canResend(member, sourceId)).toBe(expected);
+ `('returns $expected', ({ member, expected }) => {
+ expect(canResend(member)).toBe(expected);
});
});
describe('canUpdate', () => {
- const memberCanUpdate = {
- ...memberMock,
- canUpdate: true,
- };
-
test.each`
- member | currentUserId | sourceId | expected
- ${memberCanUpdate} | ${IS_NOT_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${true}
- ${memberCanUpdate} | ${IS_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${false}
- ${memberCanUpdate} | ${IS_CURRENT_USER_ID} | ${INHERITED_MEMBER_ID} | ${false}
- ${memberMock} | ${IS_NOT_CURRENT_USER_ID} | ${DIRECT_MEMBER_ID} | ${false}
- `('returns $expected', ({ member, currentUserId, sourceId, expected }) => {
- expect(canUpdate(member, currentUserId, sourceId)).toBe(expected);
+ member | currentUserId | expected
+ ${{ ...directMember, canUpdate: true }} | ${IS_NOT_CURRENT_USER_ID} | ${true}
+ ${{ ...directMember, canUpdate: true }} | ${IS_CURRENT_USER_ID} | ${false}
+ ${{ ...inheritedMember, canUpdate: true }} | ${IS_CURRENT_USER_ID} | ${false}
+ ${{ ...directMember, canUpdate: false }} | ${IS_NOT_CURRENT_USER_ID} | ${false}
+ `('returns $expected', ({ member, currentUserId, expected }) => {
+ expect(canUpdate(member, currentUserId)).toBe(expected);
});
});
@@ -229,4 +227,38 @@ describe('Members Utils', () => {
});
});
});
+
+ describe('parseDataAttributes', () => {
+ let el;
+
+ beforeEach(() => {
+ el = document.createElement('div');
+ el.setAttribute('data-members', membersJsonString);
+ el.setAttribute('data-source-id', '234');
+ el.setAttribute('data-can-manage-members', 'true');
+ });
+
+ afterEach(() => {
+ el = null;
+ });
+
+ it('correctly parses the data attributes', () => {
+ expect(parseDataAttributes(el)).toEqual({
+ members,
+ sourceId: 234,
+ canManageMembers: true,
+ });
+ });
+ });
+
+ describe('groupLinkRequestFormatter', () => {
+ it('returns expected format', () => {
+ expect(
+ groupLinkRequestFormatter({
+ accessLevel: 50,
+ expires_at: '2020-10-16',
+ }),
+ ).toEqual({ group_link: { group_access: 50, expires_at: '2020-10-16' } });
+ });
+ });
});
diff --git a/spec/frontend/merge_request/components/status_box_spec.js b/spec/frontend/merge_request/components/status_box_spec.js
index e6b6512476b..e7623aed4b3 100644
--- a/spec/frontend/merge_request/components/status_box_spec.js
+++ b/spec/frontend/merge_request/components/status_box_spec.js
@@ -18,6 +18,12 @@ const testCases = [
icon: 'issue-open-m',
},
{
+ name: 'Open',
+ state: 'locked',
+ class: 'status-box-open',
+ icon: 'issue-open-m',
+ },
+ {
name: 'Closed',
state: 'closed',
class: 'status-box-mr-closed',
diff --git a/spec/frontend/milestones/milestone_combobox_spec.js b/spec/frontend/milestones/milestone_combobox_spec.js
index 8c519abe382..b0061189a48 100644
--- a/spec/frontend/milestones/milestone_combobox_spec.js
+++ b/spec/frontend/milestones/milestone_combobox_spec.js
@@ -6,8 +6,8 @@ import MockAdapter from 'axios-mock-adapter';
import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem } from '@gitlab/ui';
import { ENTER_KEY } from '~/lib/utils/keys';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
-import { projectMilestones, groupMilestones } from './mock_data';
import createStore from '~/milestones/stores/';
+import { projectMilestones, groupMilestones } from './mock_data';
const extraLinks = [
{ text: 'Create new', url: 'http://127.0.0.1:3000/h5bp/html5-boilerplate/-/milestones/new' },
diff --git a/spec/frontend/monitoring/components/charts/anomaly_spec.js b/spec/frontend/monitoring/components/charts/anomaly_spec.js
index dad3003d536..00e843722d6 100644
--- a/spec/frontend/monitoring/components/charts/anomaly_spec.js
+++ b/spec/frontend/monitoring/components/charts/anomaly_spec.js
@@ -3,9 +3,9 @@ import { TEST_HOST } from 'helpers/test_constants';
import Anomaly from '~/monitoring/components/charts/anomaly.vue';
import { colorValues } from '~/monitoring/constants';
+import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
import { anomalyDeploymentData, mockProjectDir } from '../../mock_data';
import { anomalyGraphData } from '../../graph_data';
-import MonitorTimeSeriesChart from '~/monitoring/components/charts/time_series.vue';
const mockProjectPath = `${TEST_HOST}${mockProjectDir}`;
diff --git a/spec/frontend/monitoring/components/charts/single_stat_spec.js b/spec/frontend/monitoring/components/charts/single_stat_spec.js
index 37712eb3012..144a4e0c968 100644
--- a/spec/frontend/monitoring/components/charts/single_stat_spec.js
+++ b/spec/frontend/monitoring/components/charts/single_stat_spec.js
@@ -27,8 +27,12 @@ describe('Single Stat Chart component', () => {
describe('computed', () => {
describe('statValue', () => {
- it('should interpolate the value and unit props', () => {
- expect(findChart().props('value')).toBe('1.00MB');
+ it('should display the correct value', () => {
+ expect(findChart().props('value')).toBe('1.00');
+ });
+
+ it('should display the correct value unit', () => {
+ expect(findChart().props('unit')).toBe('MB');
});
it('should change the value representation to a percentile one', () => {
@@ -36,7 +40,8 @@ describe('Single Stat Chart component', () => {
graphData: singleStatGraphData({ max_value: 120 }, { value: 91 }),
});
- expect(findChart().props('value')).toContain('75.83%');
+ expect(findChart().props('value')).toBe('75.83');
+ expect(findChart().props('unit')).toBe('%');
});
it('should display NaN for non numeric maxValue values', () => {
diff --git a/spec/frontend/monitoring/components/charts/time_series_spec.js b/spec/frontend/monitoring/components/charts/time_series_spec.js
index b7e1cb91987..89b7e82e84a 100644
--- a/spec/frontend/monitoring/components/charts/time_series_spec.js
+++ b/spec/frontend/monitoring/components/charts/time_series_spec.js
@@ -1,14 +1,14 @@
import { mount, shallowMount } from '@vue/test-utils';
-import { setTestTimeout } from 'helpers/timeout';
import timezoneMock from 'timezone-mock';
import { GlLink } from '@gitlab/ui';
-import { TEST_HOST } from 'helpers/test_constants';
import {
GlAreaChart,
GlLineChart,
GlChartSeriesLabel,
GlChartLegend,
} from '@gitlab/ui/dist/charts';
+import { TEST_HOST } from 'helpers/test_constants';
+import { setTestTimeout } from 'helpers/timeout';
import { shallowWrapperContainsSlotText } from 'helpers/vue_test_utils_helper';
import { panelTypes, chartHeight } from '~/monitoring/constants';
import TimeSeries from '~/monitoring/components/charts/time_series.vue';
diff --git a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
index 43d5937a3a1..7a2f4a3de9a 100644
--- a/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_actions_menu_spec.js
@@ -2,13 +2,13 @@ import { shallowMount } from '@vue/test-utils';
import { GlDropdownItem } 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';
+import { dashboardActionsMenuProps, dashboardGitResponse } from '../mock_data';
+import { setupAllDashboards, setupStoreWithData } from '../store_utils';
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
diff --git a/spec/frontend/monitoring/components/dashboard_header_spec.js b/spec/frontend/monitoring/components/dashboard_header_spec.js
index 32fd9c45e8d..e431b17a965 100644
--- a/spec/frontend/monitoring/components/dashboard_header_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_header_spec.js
@@ -7,6 +7,7 @@ import RefreshButton from '~/monitoring/components/refresh_button.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
import DashboardsDropdown from '~/monitoring/components/dashboards_dropdown.vue';
import ActionsMenu from '~/monitoring/components/dashboard_actions_menu.vue';
+import { redirectTo } from '~/lib/utils/url_utility';
import { setupAllDashboards, setupStoreWithDashboard, setupStoreWithData } from '../store_utils';
import {
environmentData,
@@ -14,7 +15,6 @@ import {
selfMonitoringDashboardGitResponse,
dashboardHeaderProps,
} from '../mock_data';
-import { redirectTo } from '~/lib/utils/url_utility';
const mockProjectPath = 'https://path/to/project';
diff --git a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
index 08c69701bd2..37939a11c4d 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_builder_spec.js
@@ -3,11 +3,10 @@ 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';
+import { metricsDashboardResponse } from '../fixture_data';
+import { mockTimeRange } from '../mock_data';
const mockPanel = metricsDashboardResponse.dashboard.panel_groups[0].panels[0];
diff --git a/spec/frontend/monitoring/components/dashboard_panel_spec.js b/spec/frontend/monitoring/components/dashboard_panel_spec.js
index f64e05d3a2c..b3257f66cc5 100644
--- a/spec/frontend/monitoring/components/dashboard_panel_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_panel_spec.js
@@ -1,28 +1,13 @@
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
-import { setTestTimeout } from 'helpers/timeout';
import { GlDropdownItem } from '@gitlab/ui';
+import { setTestTimeout } from 'helpers/timeout';
import invalidUrl from '~/lib/utils/invalid_url';
import axios from '~/lib/utils/axios_utils';
import AlertWidget from '~/monitoring/components/alert_widget.vue';
import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
-import {
- mockAlert,
- mockLogsHref,
- mockLogsPath,
- mockNamespace,
- mockNamespacedData,
- mockTimeRange,
-} from '../mock_data';
-import { dashboardProps, graphData, graphDataEmpty } from '../fixture_data';
-import {
- anomalyGraphData,
- singleStatGraphData,
- heatmapGraphData,
- barGraphData,
-} from '../graph_data';
import { panelTypes } from '~/monitoring/constants';
@@ -37,6 +22,21 @@ import MonitorStackedColumnChart from '~/monitoring/components/charts/stacked_co
import { createStore, monitoringDashboard } from '~/monitoring/stores';
import { createStore as createEmbedGroupStore } from '~/monitoring/stores/embed_group';
+import {
+ anomalyGraphData,
+ singleStatGraphData,
+ heatmapGraphData,
+ barGraphData,
+} from '../graph_data';
+import { dashboardProps, graphData, graphDataEmpty } from '../fixture_data';
+import {
+ mockAlert,
+ mockLogsHref,
+ mockLogsPath,
+ mockNamespace,
+ mockNamespacedData,
+ mockTimeRange,
+} from '../mock_data';
const mocks = {
$toast: {
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index db35f1cdde3..062a87c4091 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -16,6 +16,7 @@ import GraphGroup from '~/monitoring/components/graph_group.vue';
import LinksSection from '~/monitoring/components/links_section.vue';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
+import { deprecatedCreateFlash as createFlash } from '~/flash';
import {
setupAllDashboards,
setupStoreWithDashboard,
@@ -30,7 +31,6 @@ import {
metricsDashboardPanelCount,
dashboardProps,
} from '../fixture_data';
-import { deprecatedCreateFlash as createFlash } from '~/flash';
jest.mock('~/flash');
diff --git a/spec/frontend/monitoring/components/dashboard_url_time_spec.js b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
index c4630bde32f..40610e5f542 100644
--- a/spec/frontend/monitoring/components/dashboard_url_time_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_url_time_spec.js
@@ -9,13 +9,13 @@ import {
updateHistory,
} from '~/lib/utils/url_utility';
import axios from '~/lib/utils/axios_utils';
-import { mockProjectDir } from '../mock_data';
-import { dashboardProps } from '../fixture_data';
import Dashboard from '~/monitoring/components/dashboard.vue';
import DashboardHeader from '~/monitoring/components/dashboard_header.vue';
import { createStore } from '~/monitoring/stores';
import { defaultTimeRange } from '~/vue_shared/constants';
+import { dashboardProps } from '../fixture_data';
+import { mockProjectDir } from '../mock_data';
jest.mock('~/flash');
jest.mock('~/lib/utils/url_utility');
diff --git a/spec/frontend/monitoring/csv_export_spec.js b/spec/frontend/monitoring/csv_export_spec.js
index eb2a6e40243..42d19c21a7b 100644
--- a/spec/frontend/monitoring/csv_export_spec.js
+++ b/spec/frontend/monitoring/csv_export_spec.js
@@ -1,5 +1,5 @@
-import { timeSeriesGraphData } from './graph_data';
import { graphDataToCsv } from '~/monitoring/csv_export';
+import { timeSeriesGraphData } from './graph_data';
describe('monitoring export_csv', () => {
describe('graphDataToCsv', () => {
diff --git a/spec/frontend/monitoring/mock_data.js b/spec/frontend/monitoring/mock_data.js
index ca06c96c7d6..29a7c86491d 100644
--- a/spec/frontend/monitoring/mock_data.js
+++ b/spec/frontend/monitoring/mock_data.js
@@ -1,6 +1,6 @@
// The path below needs to be relative because we import the mock-data to karma
-import { TEST_HOST } from '../__helpers__/test_constants';
import invalidUrl from '~/lib/utils/invalid_url';
+import { TEST_HOST } from '../__helpers__/test_constants';
// This import path needs to be relative for now because this mock data is used in
// Karma specs too, where the helpers/test_constants alias can not be resolved
diff --git a/spec/frontend/monitoring/requests/index_spec.js b/spec/frontend/monitoring/requests/index_spec.js
index 078de5f15d1..b590fe749e0 100644
--- a/spec/frontend/monitoring/requests/index_spec.js
+++ b/spec/frontend/monitoring/requests/index_spec.js
@@ -3,8 +3,8 @@ import { backoffMockImplementation } from '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';
+import { metricsDashboardResponse } from '../fixture_data';
describe('monitoring metrics_requests', () => {
let mock;
diff --git a/spec/frontend/monitoring/store/utils_spec.js b/spec/frontend/monitoring/store/utils_spec.js
index 58bb87cb332..2221909583c 100644
--- a/spec/frontend/monitoring/store/utils_spec.js
+++ b/spec/frontend/monitoring/store/utils_spec.js
@@ -11,8 +11,8 @@ import {
normalizeCustomDashboardPath,
} from '~/monitoring/stores/utils';
import * as urlUtils from '~/lib/utils/url_utility';
-import { annotationsData } from '../mock_data';
import { NOT_IN_DB_PREFIX } from '~/monitoring/constants';
+import { annotationsData } from '../mock_data';
const projectPath = 'gitlab-org/gitlab-test';
diff --git a/spec/frontend/monitoring/store/variable_mapping_spec.js b/spec/frontend/monitoring/store/variable_mapping_spec.js
index de124b0313c..85e87f361b5 100644
--- a/spec/frontend/monitoring/store/variable_mapping_spec.js
+++ b/spec/frontend/monitoring/store/variable_mapping_spec.js
@@ -3,13 +3,13 @@ import {
mergeURLVariables,
optionsFromSeriesData,
} from '~/monitoring/stores/variable_mapping';
+import * as urlUtils from '~/lib/utils/url_utility';
import {
templatingVariablesExamples,
storeTextVariables,
storeCustomVariables,
storeMetricLabelValuesVariables,
} from '../mock_data';
-import * as urlUtils from '~/lib/utils/url_utility';
describe('Monitoring variable mapping', () => {
describe('parseTemplatingVariables', () => {
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 002c4f206cb..8394d1d0ab2 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -9,7 +9,6 @@ import CommentForm from '~/notes/components/comment_form.vue';
import * as constants from '~/notes/constants';
import eventHub from '~/notes/event_hub';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
jest.mock('autosize');
@@ -64,14 +63,6 @@ describe('issue_comment_form component', () => {
});
describe('user is logged in', () => {
- describe('avatar', () => {
- it('should render user avatar with link', () => {
- mountComponent({ mountFunction: mount });
-
- expect(wrapper.find(UserAvatarLink).attributes('href')).toBe(userDataMock.path);
- });
- });
-
describe('handleSave', () => {
it('should request to save note when note is entered', () => {
mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
diff --git a/spec/frontend/notes/components/discussion_actions_spec.js b/spec/frontend/notes/components/discussion_actions_spec.js
index 48e569720e9..03e5842bb0f 100644
--- a/spec/frontend/notes/components/discussion_actions_spec.js
+++ b/spec/frontend/notes/components/discussion_actions_spec.js
@@ -1,10 +1,10 @@
import { shallowMount, mount } from '@vue/test-utils';
-import { discussionMock } from '../mock_data';
import DiscussionActions from '~/notes/components/discussion_actions.vue';
import ReplyPlaceholder from '~/notes/components/discussion_reply_placeholder.vue';
import ResolveDiscussionButton from '~/notes/components/discussion_resolve_button.vue';
import ResolveWithIssueButton from '~/notes/components/discussion_resolve_with_issue_button.vue';
import createStore from '~/notes/stores';
+import { discussionMock } from '../mock_data';
// NOTE: clone mock_data so that it is not accidentally mutated
const createDiscussionMock = (props = {}) =>
diff --git a/spec/frontend/notes/components/discussion_counter_spec.js b/spec/frontend/notes/components/discussion_counter_spec.js
index ebf7d52f38b..ca1972c9f61 100644
--- a/spec/frontend/notes/components/discussion_counter_spec.js
+++ b/spec/frontend/notes/components/discussion_counter_spec.js
@@ -3,8 +3,8 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlButton } from '@gitlab/ui';
import notesModule from '~/notes/stores/modules';
import DiscussionCounter from '~/notes/components/discussion_counter.vue';
-import { noteableDataMock, discussionMock, notesDataMock, userDataMock } from '../mock_data';
import * as types from '~/notes/stores/mutation_types';
+import { noteableDataMock, discussionMock, notesDataMock, userDataMock } from '../mock_data';
describe('DiscussionCounter component', () => {
let store;
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index 3cfc1445cb8..7c3d747711c 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -1,11 +1,12 @@
import Vue from 'vue';
import { mount, createLocalVue, createWrapper } from '@vue/test-utils';
-import { TEST_HOST } from 'spec/test_constants';
import AxiosMockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'spec/test_constants';
import createStore from '~/notes/stores';
import noteActions from '~/notes/components/note_actions.vue';
-import { userDataMock } from '../mock_data';
import axios from '~/lib/utils/axios_utils';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+import { userDataMock } from '../mock_data';
describe('noteActions', () => {
let wrapper;
@@ -135,7 +136,7 @@ describe('noteActions', () => {
.then(() => {
const emitted = Object.keys(rootWrapper.emitted());
- expect(emitted).toEqual(['bv::hide::tooltip']);
+ expect(emitted).toEqual([BV_HIDE_TOOLTIP]);
done();
})
.catch(done.fail);
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index e64a75bede9..2ef99297540 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -4,9 +4,8 @@ import createStore from '~/notes/stores';
import NoteForm from '~/notes/components/note_form.vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { noteableDataMock, notesDataMock, discussionMock } from '../mock_data';
-
import { getDraft, updateDraft } from '~/lib/utils/autosave';
+import { noteableDataMock, notesDataMock, discussionMock } from '../mock_data';
jest.mock('~/lib/utils/autosave');
@@ -25,6 +24,8 @@ describe('issue_note_form component', () => {
});
};
+ const findCancelButton = () => wrapper.find('[data-testid="cancel"]');
+
beforeEach(() => {
getDraft.mockImplementation((key) => {
if (key === dummyAutosaveKey) {
@@ -160,8 +161,8 @@ describe('issue_note_form component', () => {
});
await nextTick();
- const cancelButton = wrapper.find('[data-testid="cancel"]');
- cancelButton.trigger('click');
+ const cancelButton = findCancelButton();
+ cancelButton.vm.$emit('click');
await nextTick();
expect(wrapper.emitted().cancelForm).toHaveLength(1);
@@ -177,7 +178,7 @@ describe('issue_note_form component', () => {
const textarea = wrapper.find('textarea');
textarea.setValue('Foo');
const saveButton = wrapper.find('.js-vue-issue-save');
- saveButton.trigger('click');
+ saveButton.vm.$emit('click');
expect(wrapper.vm.isSubmitting).toBe(true);
});
@@ -272,7 +273,7 @@ describe('issue_note_form component', () => {
await nextTick();
const cancelButton = wrapper.find('[data-testid="cancelBatchCommentsEnabled"]');
- cancelButton.trigger('click');
+ cancelButton.vm.$emit('click');
expect(wrapper.vm.cancelHandler).toHaveBeenCalledWith(true);
});
@@ -302,16 +303,16 @@ describe('issue_note_form component', () => {
expect(wrapper.find('.js-resolve-checkbox').exists()).toBe(false);
});
- it('hides actions for commits', () => {
+ it('hides actions for commits', async () => {
wrapper.setProps({ discussion: { for_commit: true } });
- return nextTick(() => {
- expect(wrapper.find('.note-form-actions').text()).not.toContain('Start a review');
- });
+ await nextTick();
+
+ expect(wrapper.find('.note-form-actions').text()).not.toContain('Start a review');
});
describe('on enter', () => {
- it('should start review or add to review when cmd+enter is pressed', () => {
+ it('should start review or add to review when cmd+enter is pressed', async () => {
const textarea = wrapper.find('textarea');
jest.spyOn(wrapper.vm, 'handleAddToReview');
@@ -319,9 +320,8 @@ describe('issue_note_form component', () => {
textarea.setValue('Foo');
textarea.trigger('keydown.enter', { metaKey: true });
- return nextTick(() => {
- expect(wrapper.vm.handleAddToReview).toHaveBeenCalled();
- });
+ await nextTick();
+ expect(wrapper.vm.handleAddToReview).toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index e495a4738e0..45fbba2524a 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -10,9 +10,9 @@ import createStore from '~/notes/stores';
import * as constants from '~/notes/constants';
import '~/behaviors/markdown/render_gfm';
// TODO: use generated fixture (https://gitlab.com/gitlab-org/gitlab-foss/issues/62491)
-import * as mockData from '../mock_data';
import * as urlUtility from '~/lib/utils/url_utility';
import OrderedLayout from '~/vue_shared/components/ordered_layout.vue';
+import * as mockData from '../mock_data';
jest.mock('~/user_popovers', () => jest.fn());
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index f0e6a0a68dd..468e5af6f1a 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -1,5 +1,5 @@
-import { TEST_HOST } from 'spec/test_constants';
import AxiosMockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'spec/test_constants';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import { deprecatedCreateFlash as Flash } from '~/flash';
@@ -9,7 +9,11 @@ import * as mutationTypes from '~/notes/stores/mutation_types';
import * as notesConstants from '~/notes/constants';
import createStore from '~/notes/stores';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
-import { resetStore } from '../helpers';
+import axios from '~/lib/utils/axios_utils';
+import * as utils from '~/notes/stores/utils';
+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';
import {
discussionMock,
notesDataMock,
@@ -18,11 +22,7 @@ import {
individualNote,
batchSuggestionsInfoMock,
} from '../mock_data';
-import axios from '~/lib/utils/axios_utils';
-import * as utils from '~/notes/stores/utils';
-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';
+import { resetStore } from '../helpers';
const TEST_ERROR_MESSAGE = 'Test error message';
jest.mock('~/flash');
@@ -291,9 +291,45 @@ describe('Actions Notes Store', () => {
[
{ type: 'updateOrCreateNotes', payload: discussionMock.notes },
{ type: 'startTaskList' },
+ { type: 'updateResolvableDiscussionsCounts' },
],
));
});
+
+ describe('paginated notes feature flag enabled', () => {
+ const lastFetchedAt = '12358';
+
+ beforeEach(() => {
+ window.gon = { features: { paginatedNotes: true } };
+
+ axiosMock.onGet(notesDataMock.notesPath).replyOnce(200, {
+ notes: discussionMock.notes,
+ more: false,
+ last_fetched_at: lastFetchedAt,
+ });
+ });
+
+ afterEach(() => {
+ window.gon = null;
+ });
+
+ it('should dispatch setFetchingState, setNotesFetchedState, setLoadingState, updateOrCreateNotes, startTaskList and commit SET_LAST_FETCHED_AT', () => {
+ return testAction(
+ actions.fetchData,
+ null,
+ { notesData: notesDataMock, isFetching: true },
+ [{ type: 'SET_LAST_FETCHED_AT', payload: lastFetchedAt }],
+ [
+ { type: 'setFetchingState', payload: false },
+ { type: 'setNotesFetchedState', payload: true },
+ { type: 'setLoadingState', payload: false },
+ { type: 'updateOrCreateNotes', payload: discussionMock.notes },
+ { type: 'startTaskList' },
+ { type: 'updateResolvableDiscussionsCounts' },
+ ],
+ );
+ });
+ });
});
describe('poll', () => {
@@ -1355,4 +1391,17 @@ describe('Actions Notes Store', () => {
);
});
});
+
+ describe('setFetchingState', () => {
+ it('commits SET_NOTES_FETCHING_STATE', (done) => {
+ testAction(
+ actions.setFetchingState,
+ true,
+ null,
+ [{ type: mutationTypes.SET_NOTES_FETCHING_STATE, payload: true }],
+ [],
+ done,
+ );
+ });
+ });
});
diff --git a/spec/frontend/notes/stores/mutation_spec.js b/spec/frontend/notes/stores/mutation_spec.js
index 66fc74525ad..c4301f63470 100644
--- a/spec/frontend/notes/stores/mutation_spec.js
+++ b/spec/frontend/notes/stores/mutation_spec.js
@@ -400,6 +400,19 @@ describe('Notes Store mutations', () => {
expect(state.discussions[0].notes[0].note).toEqual('Foo');
});
+ it('does not update existing note if it matches', () => {
+ const state = {
+ discussions: [{ ...individualNote, individual_note: false }],
+ };
+ jest.spyOn(state.discussions[0].notes, 'splice');
+
+ const updated = individualNote.notes[0];
+
+ mutations.UPDATE_NOTE(state, updated);
+
+ expect(state.discussions[0].notes.splice).not.toHaveBeenCalled();
+ });
+
it('transforms an individual note to discussion', () => {
const state = {
discussions: [individualNote],
diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js
new file mode 100644
index 00000000000..2fe8e9c677e
--- /dev/null
+++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js
@@ -0,0 +1,240 @@
+import axios from 'axios';
+import MockAdapter from 'axios-mock-adapter';
+import { shallowMount } from '@vue/test-utils';
+import { GlButtonGroup, GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import waitForPromises from 'helpers/wait_for_promises';
+import httpStatus from '~/lib/utils/http_status';
+import NotificationsDropdown from '~/notifications/components/notifications_dropdown.vue';
+import NotificationsDropdownItem from '~/notifications/components/notifications_dropdown_item.vue';
+
+const mockDropdownItems = ['global', 'watch', 'participating', 'mention', 'disabled'];
+const mockToastShow = jest.fn();
+
+describe('NotificationsDropdown', () => {
+ let wrapper;
+ let mockAxios;
+
+ function createComponent(injectedProperties = {}) {
+ return shallowMount(NotificationsDropdown, {
+ stubs: {
+ GlButtonGroup,
+ GlDropdown,
+ GlDropdownItem,
+ NotificationsDropdownItem,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ provide: {
+ dropdownItems: mockDropdownItems,
+ initialNotificationLevel: 'global',
+ ...injectedProperties,
+ },
+ mocks: {
+ $toast: {
+ show: mockToastShow,
+ },
+ },
+ });
+ }
+
+ const findButtonGroup = () => wrapper.find(GlButtonGroup);
+ const findButton = () => wrapper.find(GlButton);
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findByTestId = (testId) => wrapper.find(`[data-testid="${testId}"]`);
+ const findAllNotificationsDropdownItems = () => wrapper.findAll(NotificationsDropdownItem);
+ const findDropdownItemAt = (index) =>
+ findAllNotificationsDropdownItems().at(index).find(GlDropdownItem);
+
+ const clickDropdownItemAt = async (index) => {
+ const dropdownItem = findDropdownItemAt(index);
+ dropdownItem.vm.$emit('click');
+
+ await waitForPromises();
+ };
+
+ beforeEach(() => {
+ gon.api_version = 'v4';
+ mockAxios = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ mockAxios.restore();
+ });
+
+ describe('template', () => {
+ describe('when notification level is "custom"', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialNotificationLevel: 'custom',
+ });
+ });
+
+ it('renders a button group', () => {
+ expect(findButtonGroup().exists()).toBe(true);
+ });
+
+ it('shows the button text when showLabel is true', () => {
+ wrapper = createComponent({
+ initialNotificationLevel: 'custom',
+ showLabel: true,
+ });
+
+ expect(findButton().text()).toBe('Custom');
+ });
+
+ it("doesn't show the button text when showLabel is false", () => {
+ wrapper = createComponent({
+ initialNotificationLevel: 'custom',
+ showLabel: false,
+ });
+
+ expect(findButton().text()).toBe('');
+ });
+ });
+
+ describe('when notification level is not "custom"', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialNotificationLevel: 'global',
+ });
+ });
+
+ it('does not render a button group', () => {
+ expect(findButtonGroup().exists()).toBe(false);
+ });
+
+ it('shows the button text when showLabel is true', () => {
+ wrapper = createComponent({
+ showLabel: true,
+ });
+
+ expect(findDropdown().props('text')).toBe('Global');
+ });
+
+ it("doesn't show the button text when showLabel is false", () => {
+ wrapper = createComponent({
+ showLabel: false,
+ });
+
+ expect(findDropdown().props('text')).toBe(null);
+ });
+ });
+
+ describe('button tooltip', () => {
+ const tooltipTitlePrefix = 'Notification setting';
+ it.each`
+ level | title
+ ${'global'} | ${'Global'}
+ ${'watch'} | ${'Watch'}
+ ${'participating'} | ${'Participate'}
+ ${'mention'} | ${'On mention'}
+ ${'disabled'} | ${'Disabled'}
+ ${'custom'} | ${'Custom'}
+ `(`renders "${tooltipTitlePrefix} - $title" for "$level" level`, ({ level, title }) => {
+ wrapper = createComponent({
+ initialNotificationLevel: level,
+ });
+
+ const tooltipElement = findByTestId('notificationButton');
+ const tooltip = getBinding(tooltipElement.element, 'gl-tooltip');
+
+ expect(tooltip.value.title).toBe(`${tooltipTitlePrefix} - ${title}`);
+ });
+ });
+
+ describe('button icon', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ initialNotificationLevel: 'disabled',
+ });
+ });
+
+ it('renders the "notifications-off" icon when notification level is "disabled"', () => {
+ expect(findDropdown().props('icon')).toBe('notifications-off');
+ });
+
+ it('renders the "notifications" icon when notification level is not "disabled"', () => {
+ wrapper = createComponent();
+
+ expect(findDropdown().props('icon')).toBe('notifications');
+ });
+ });
+
+ describe('dropdown items', () => {
+ it.each`
+ dropdownIndex | level | title | description
+ ${0} | ${'global'} | ${'Global'} | ${'Use your global notification setting'}
+ ${1} | ${'watch'} | ${'Watch'} | ${'You will receive notifications for any activity'}
+ ${2} | ${'participating'} | ${'Participate'} | ${'You will only receive notifications for threads you have participated in'}
+ ${3} | ${'mention'} | ${'On mention'} | ${'You will receive notifications only for comments in which you were @mentioned'}
+ ${4} | ${'disabled'} | ${'Disabled'} | ${'You will not get any notifications via email'}
+ ${5} | ${'custom'} | ${'Custom'} | ${'You will only receive notifications for the events you choose'}
+ `('displays "$title" and "$description"', ({ dropdownIndex, title, description }) => {
+ wrapper = createComponent();
+
+ expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('title')).toBe(title);
+ expect(findAllNotificationsDropdownItems().at(dropdownIndex).props('description')).toBe(
+ description,
+ );
+ });
+ });
+ });
+
+ describe('when selecting an item', () => {
+ beforeEach(() => {
+ jest.spyOn(axios, 'put');
+ });
+
+ it.each`
+ projectId | groupId | endpointUrl | endpointType | condition
+ ${1} | ${null} | ${'/api/v4/projects/1/notification_settings'} | ${'project notifications'} | ${'a projectId is given'}
+ ${null} | ${1} | ${'/api/v4/groups/1/notification_settings'} | ${'group notifications'} | ${'a groupId is given'}
+ ${null} | ${null} | ${'/api/v4/notification_settings'} | ${'global notifications'} | ${'when neither projectId nor groupId are given'}
+ `(
+ 'calls the $endpointType endpoint when $condition',
+ async ({ projectId, groupId, endpointUrl }) => {
+ wrapper = createComponent({
+ projectId,
+ groupId,
+ });
+
+ await clickDropdownItemAt(1);
+
+ expect(axios.put).toHaveBeenCalledWith(endpointUrl, {
+ level: 'watch',
+ });
+ },
+ );
+
+ it('updates the selectedNotificationLevel and marks the item with a checkmark', async () => {
+ mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {});
+ wrapper = createComponent();
+
+ const dropdownItem = findDropdownItemAt(1);
+
+ await clickDropdownItemAt(1);
+
+ expect(wrapper.vm.selectedNotificationLevel).toBe('watch');
+ expect(dropdownItem.props('isChecked')).toBe(true);
+ });
+
+ it("won't update the selectedNotificationLevel and shows a toast message when the request fails and ", async () => {
+ mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
+ wrapper = createComponent();
+
+ await clickDropdownItemAt(1);
+
+ expect(wrapper.vm.selectedNotificationLevel).toBe('global');
+ expect(
+ mockToastShow,
+ ).toHaveBeenCalledWith(
+ 'An error occured while updating the notification settings. Please try again.',
+ { type: 'error' },
+ );
+ });
+ });
+});
diff --git a/spec/frontend/onboarding_issues/index_spec.js b/spec/frontend/onboarding_issues/index_spec.js
deleted file mode 100644
index d476ba1cf5a..00000000000
--- a/spec/frontend/onboarding_issues/index_spec.js
+++ /dev/null
@@ -1,137 +0,0 @@
-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 Tracking from '~/tracking';
-
-describe('Onboarding Issues Popovers', () => {
- const COOKIE_NAME = 'onboarding_issues_settings';
- const getCookieValue = () => JSON.parse(getCookie(COOKIE_NAME));
-
- beforeEach(() => {
- jest.spyOn($.fn, 'popover');
- });
-
- afterEach(() => {
- $.fn.popover.mockRestore();
- document.getElementsByTagName('html')[0].innerHTML = '';
- removeCookie(COOKIE_NAME);
- });
-
- const setupShowLearnGitLabIssuesPopoverTest = ({
- currentPath = 'group/learn-gitlab',
- isIssuesBoardsLinkShown = true,
- isCookieSet = true,
- cookieValue = true,
- } = {}) => {
- setWindowLocation(`http://example.com/${currentPath}`);
-
- if (isIssuesBoardsLinkShown) {
- const elem = document.createElement('a');
- elem.setAttribute('data-qa-selector', 'issue_boards_link');
- document.body.appendChild(elem);
- }
-
- if (isCookieSet) {
- setCookie(COOKIE_NAME, { previous: true, 'issues#index': cookieValue });
- }
-
- showLearnGitLabIssuesPopover();
- };
-
- describe('showLearnGitLabIssuesPopover', () => {
- describe('when on another project', () => {
- beforeEach(() => {
- setupShowLearnGitLabIssuesPopoverTest({
- currentPath: 'group/another-project',
- });
- });
-
- it('does not show a popover', () => {
- expect($.fn.popover).not.toHaveBeenCalled();
- });
- });
-
- describe('when the issues boards link is not shown', () => {
- beforeEach(() => {
- setupShowLearnGitLabIssuesPopoverTest({
- isIssuesBoardsLinkShown: false,
- });
- });
-
- it('does not show a popover', () => {
- expect($.fn.popover).not.toHaveBeenCalled();
- });
- });
-
- describe('when the cookie is not set', () => {
- beforeEach(() => {
- setupShowLearnGitLabIssuesPopoverTest({
- isCookieSet: false,
- });
- });
-
- it('does not show a popover', () => {
- expect($.fn.popover).not.toHaveBeenCalled();
- });
- });
-
- describe('when the cookie value is false', () => {
- beforeEach(() => {
- setupShowLearnGitLabIssuesPopoverTest({
- cookieValue: false,
- });
- });
-
- it('does not show a popover', () => {
- expect($.fn.popover).not.toHaveBeenCalled();
- });
- });
-
- describe('with all the right conditions', () => {
- beforeEach(() => {
- setupShowLearnGitLabIssuesPopoverTest();
- });
-
- it('shows a popover', () => {
- expect($.fn.popover).toHaveBeenCalled();
- });
-
- it('does not change the cookie value', () => {
- expect(getCookieValue()['issues#index']).toBe(true);
- });
-
- it('disables the previous popover', () => {
- expect(getCookieValue().previous).toBe(false);
- });
-
- describe('when clicking the issues boards link', () => {
- beforeEach(() => {
- document.querySelector('a[data-qa-selector="issue_boards_link"]').click();
- });
-
- it('deletes the cookie', () => {
- expect(getCookie(COOKIE_NAME)).toBe(undefined);
- });
- });
-
- describe('when dismissing the popover', () => {
- beforeEach(() => {
- jest.spyOn(Tracking, 'event');
- document.querySelector('.learn-gitlab.popover .close').click();
- });
-
- it('deletes the cookie', () => {
- expect(getCookie(COOKIE_NAME)).toBe(undefined);
- });
-
- it('sends a tracking event', () => {
- expect(Tracking.event).toHaveBeenCalledWith(
- 'Growth::Conversion::Experiment::OnboardingIssues',
- 'dismiss_popover',
- );
- });
- });
- });
- });
-});
diff --git a/spec/frontend/packages/details/store/getters_spec.js b/spec/frontend/packages/details/store/getters_spec.js
index b8c2138e7f5..e5b130cd958 100644
--- a/spec/frontend/packages/details/store/getters_spec.js
+++ b/spec/frontend/packages/details/store/getters_spec.js
@@ -17,6 +17,7 @@ import {
composerPackageInclude,
groupExists,
} from '~/packages/details/store/getters';
+import { NpmManager } from '~/packages/details/constants';
import {
conanPackage,
npmPackage,
@@ -32,7 +33,6 @@ import {
registryUrl,
pypiSetupCommandStr,
} from '../mock_data';
-import { NpmManager } from '~/packages/details/constants';
describe('Getters PackageDetails Store', () => {
let state;
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
deleted file mode 100644
index ed77f25916f..00000000000
--- a/spec/frontend/packages/list/components/__snapshots__/packages_filter_spec.js.snap
+++ /dev/null
@@ -1,14 +0,0 @@
-// 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
index b2df1ac5ab6..3f17731584c 100644
--- 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
@@ -6,517 +6,60 @@ exports[`packages_list_app renders 1`] = `
packagehelpurl="foo"
/>
- <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=""
- 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=""
- 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=""
- 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="Generic"
- 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=""
- 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 Generic 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=""
- 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"
+ <package-search-stub />
+
+ <div>
+ <section
+ class="row empty-state text-center"
+ >
+ <div
+ class="col-12"
>
- <template>
- <div>
- <section
- class="row empty-state text-center"
- >
- <div
- class="col-12"
- >
- <div
- class="svg-250 svg-content"
- >
- <img
- alt=""
- 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"
+ <div
+ class="svg-250 svg-content"
+ >
+ <img
+ alt=""
+ class="gl-max-w-full"
+ src="helpSvg"
+ />
+ </div>
+ </div>
+
+ <div
+ class="col-12"
>
- <template>
- <div>
- <section
- class="row empty-state text-center"
+ <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"
>
- <div
- class="col-12"
- >
- <div
- class="svg-250 svg-content"
- >
- <img
- alt=""
- 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>
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+
<div>
- <section
- class="row empty-state text-center"
- >
- <div
- class="col-12"
- >
- <div
- class="svg-250 svg-content"
- >
- <img
- alt=""
- 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="gl-display-flex gl-align-self-center gl-py-2 gl-flex-grow-1 gl-justify-content-end"
- >
- <package-filter-stub
- class="gl-mr-2"
- />
-
- <package-sort-stub />
+ </div>
</div>
- </template>
- </b-tabs-stub>
+ </section>
+ </div>
</div>
`;
diff --git a/spec/frontend/packages/list/components/packages_filter_spec.js b/spec/frontend/packages/list/components/packages_filter_spec.js
deleted file mode 100644
index b186b5f5e48..00000000000
--- a/spec/frontend/packages/list/components/packages_filter_spec.js
+++ /dev/null
@@ -1,50 +0,0 @@
-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
index 217096f822a..36dd73666ea 100644
--- a/spec/frontend/packages/list/components/packages_list_app_spec.js
+++ b/spec/frontend/packages/list/components/packages_list_app_spec.js
@@ -1,9 +1,10 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlEmptyState, GlTab, GlTabs, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import * as commonUtils from '~/lib/utils/common_utils';
import createFlash from '~/flash';
import PackageListApp from '~/packages/list/components/packages_list_app.vue';
+import PackageSearch from '~/packages/list/components/package_search.vue';
import { SHOW_DELETE_SUCCESS_ALERT } from '~/packages/shared/constants';
import { DELETE_PACKAGE_SUCCESS_MESSAGE } from '~/packages/list/constants';
@@ -26,9 +27,9 @@ describe('packages_list_app', () => {
const emptyListHelpUrl = 'helpUrl';
const findEmptyState = () => wrapper.find(GlEmptyState);
const findListComponent = () => wrapper.find(PackageList);
- const findTabComponent = (index = 0) => wrapper.findAll(GlTab).at(index);
+ const findPackageSearch = () => wrapper.find(PackageSearch);
- const createStore = (filterQuery = '') => {
+ const createStore = (filter = []) => {
store = new Vuex.Store({
state: {
isLoading: false,
@@ -38,7 +39,7 @@ describe('packages_list_app', () => {
emptyListHelpUrl,
packageHelpUrl: 'foo',
},
- filterQuery,
+ filter,
},
});
store.dispatch = jest.fn();
@@ -52,8 +53,6 @@ describe('packages_list_app', () => {
GlEmptyState,
GlLoadingIcon,
PackageList,
- GlTab,
- GlTabs,
GlSprintf,
GlLink,
},
@@ -122,27 +121,9 @@ describe('packages_list_app', () => {
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');
+ createStore([{ type: 'something' }]);
mountComponent();
});
@@ -154,12 +135,28 @@ describe('packages_list_app', () => {
});
});
+ describe('Package Search', () => {
+ it('exists', () => {
+ mountComponent();
+
+ expect(findPackageSearch().exists()).toBe(true);
+ });
+
+ it.each(['sort:changed', 'filter:changed'])('on %p fetches data from the store', (event) => {
+ mountComponent();
+
+ findPackageSearch().vm.$emit(event);
+
+ expect(store.dispatch).toHaveBeenCalledWith('requestPackagesList');
+ });
+ });
+
describe('delete alert handling', () => {
const { location } = window.location;
const search = `?${SHOW_DELETE_SUCCESS_ALERT}=true`;
beforeEach(() => {
- createStore('foo');
+ createStore();
jest.spyOn(commonUtils, 'historyReplaceState').mockImplementation(() => {});
delete window.location;
window.location = {
diff --git a/spec/frontend/packages/list/components/packages_search_spec.js b/spec/frontend/packages/list/components/packages_search_spec.js
new file mode 100644
index 00000000000..23a8d3fb871
--- /dev/null
+++ b/spec/frontend/packages/list/components/packages_search_spec.js
@@ -0,0 +1,145 @@
+import Vuex from 'vuex';
+import { GlSorting, GlSortingItem, GlFilteredSearch } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import component from '~/packages/list/components/package_search.vue';
+import PackageTypeToken from '~/packages/list/components/tokens/package_type_token.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Package Search', () => {
+ let wrapper;
+ let store;
+ let sorting;
+ let sortingItems;
+
+ const findPackageListSorting = () => wrapper.find(GlSorting);
+ const findSortingItems = () => wrapper.findAll(GlSortingItem);
+ const findFilteredSearch = () => wrapper.find(GlFilteredSearch);
+
+ const createStore = (isGroupPage) => {
+ const state = {
+ config: {
+ isGroupPage,
+ },
+ sorting: {
+ orderBy: 'version',
+ sort: 'desc',
+ },
+ filter: [],
+ };
+ store = new Vuex.Store({
+ state,
+ });
+ store.dispatch = jest.fn();
+ };
+
+ const mountComponent = (isGroupPage = false) => {
+ createStore(isGroupPage);
+
+ wrapper = shallowMount(component, {
+ localVue,
+ store,
+ stubs: {
+ GlSortingItem,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('searching', () => {
+ it('has a filtered-search component', () => {
+ mountComponent();
+
+ expect(findFilteredSearch().exists()).toBe(true);
+ });
+
+ it('binds the correct props to filtered-search', () => {
+ mountComponent();
+
+ expect(findFilteredSearch().props()).toMatchObject({
+ value: [],
+ placeholder: 'Filter results',
+ availableTokens: wrapper.vm.tokens,
+ });
+ });
+
+ it('updates vuex when value changes', () => {
+ mountComponent();
+
+ findFilteredSearch().vm.$emit('input', ['foo']);
+
+ expect(store.dispatch).toHaveBeenCalledWith('setFilter', ['foo']);
+ });
+
+ it('emits filter:changed on submit event', () => {
+ mountComponent();
+
+ findFilteredSearch().vm.$emit('submit');
+ expect(wrapper.emitted('filter:changed')).toEqual([[]]);
+ });
+
+ it('emits filter:changed on clear event and reset vuex', () => {
+ mountComponent();
+
+ findFilteredSearch().vm.$emit('clear');
+
+ expect(store.dispatch).toHaveBeenCalledWith('setFilter', []);
+ expect(wrapper.emitted('filter:changed')).toEqual([[]]);
+ });
+
+ it('has a PackageTypeToken token', () => {
+ mountComponent();
+
+ expect(findFilteredSearch().props('availableTokens')).toEqual(
+ expect.arrayContaining([
+ expect.objectContaining({ token: PackageTypeToken, type: 'type', icon: 'package' }),
+ ]),
+ );
+ });
+ });
+
+ describe('sorting', () => {
+ 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/components/packages_sort_spec.js b/spec/frontend/packages/list/components/packages_sort_spec.js
deleted file mode 100644
index d15ad9bd542..00000000000
--- a/spec/frontend/packages/list/components/packages_sort_spec.js
+++ /dev/null
@@ -1,90 +0,0 @@
-import Vuex from 'vuex';
-import { GlSorting, GlSortingItem } 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 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/components/tokens/package_type_token_spec.js b/spec/frontend/packages/list/components/tokens/package_type_token_spec.js
new file mode 100644
index 00000000000..a654f431266
--- /dev/null
+++ b/spec/frontend/packages/list/components/tokens/package_type_token_spec.js
@@ -0,0 +1,48 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlFilteredSearchToken, GlFilteredSearchSuggestion } from '@gitlab/ui';
+import component from '~/packages/list/components/tokens/package_type_token.vue';
+import { PACKAGE_TYPES } from '~/packages/list/constants';
+
+describe('packages_filter', () => {
+ let wrapper;
+
+ const findFilteredSearchToken = () => wrapper.find(GlFilteredSearchToken);
+ const findFilteredSearchSuggestions = () => wrapper.findAll(GlFilteredSearchSuggestion);
+
+ const mountComponent = ({ attrs, listeners } = {}) => {
+ wrapper = shallowMount(component, {
+ attrs,
+ listeners,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('it binds all of his attrs to filtered search token', () => {
+ mountComponent({ attrs: { foo: 'bar' } });
+
+ expect(findFilteredSearchToken().attributes('foo')).toBe('bar');
+ });
+
+ it('it binds all of his events to filtered search token', () => {
+ const clickListener = jest.fn();
+ mountComponent({ listeners: { click: clickListener } });
+
+ findFilteredSearchToken().vm.$emit('click');
+
+ expect(clickListener).toHaveBeenCalled();
+ });
+
+ it.each(PACKAGE_TYPES.map((p, index) => [p, index]))(
+ 'displays a suggestion for %p',
+ (packageType, index) => {
+ mountComponent();
+ const item = findFilteredSearchSuggestions().at(index);
+ expect(item.text()).toBe(packageType.title);
+ expect(item.props('value')).toBe(packageType.type);
+ },
+ );
+});
diff --git a/spec/frontend/packages/list/stores/actions_spec.js b/spec/frontend/packages/list/stores/actions_spec.js
index 05e1fe57cae..d33851e6ab3 100644
--- a/spec/frontend/packages/list/stores/actions_spec.js
+++ b/spec/frontend/packages/list/stores/actions_spec.js
@@ -30,11 +30,13 @@ describe('Actions Package list store', () => {
sort: 'asc',
orderBy: 'version',
};
+
+ const filter = [];
it('should fetch the project packages list when isGroupPage is false', (done) => {
testAction(
actions.requestPackagesList,
undefined,
- { config: { isGroupPage: false, resourceId: 1 }, sorting },
+ { config: { isGroupPage: false, resourceId: 1 }, sorting, filter },
[],
[
{ type: 'setLoading', payload: true },
@@ -54,7 +56,7 @@ describe('Actions Package list store', () => {
testAction(
actions.requestPackagesList,
undefined,
- { config: { isGroupPage: true, resourceId: 2 }, sorting },
+ { config: { isGroupPage: true, resourceId: 2 }, sorting, filter },
[],
[
{ type: 'setLoading', payload: true },
@@ -70,7 +72,7 @@ describe('Actions Package list store', () => {
);
});
- it('should fetch packages of a certain type when selectedType is present', (done) => {
+ it('should fetch packages of a certain type when a filter with a type is present', (done) => {
const packageType = 'maven';
testAction(
@@ -79,7 +81,7 @@ describe('Actions Package list store', () => {
{
config: { isGroupPage: false, resourceId: 1 },
sorting,
- selectedType: { type: packageType },
+ filter: [{ type: 'type', value: { data: 'maven' } }],
},
[],
[
@@ -107,7 +109,7 @@ describe('Actions Package list store', () => {
testAction(
actions.requestPackagesList,
undefined,
- { config: { isGroupPage: false, resourceId: 2 }, sorting },
+ { config: { isGroupPage: false, resourceId: 2 }, sorting, filter },
[],
[
{ type: 'setLoading', payload: true },
diff --git a/spec/frontend/packages/list/stores/mutations_spec.js b/spec/frontend/packages/list/stores/mutations_spec.js
index 0d424a0c011..743de595eb5 100644
--- a/spec/frontend/packages/list/stores/mutations_spec.js
+++ b/spec/frontend/packages/list/stores/mutations_spec.js
@@ -78,17 +78,10 @@ describe('Mutations Registry Store', () => {
});
});
- 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');
+ expect(mockState.filter).toEqual('foo');
});
});
});
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
index 5faae5690db..4a75deebcf9 100644
--- 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
@@ -6,7 +6,7 @@ exports[`packages_list_row renders 1`] = `
data-qa-selector="package_row"
>
<div
- class="gl-display-flex gl-align-items-center gl-py-5"
+ class="gl-display-flex gl-align-items-center gl-py-3"
>
<!---->
@@ -14,7 +14,7 @@ exports[`packages_list_row renders 1`] = `
class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-fill-1"
>
<div
- class="gl-display-flex gl-flex-direction-column gl-justify-content-space-between gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
+ class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
>
<div
class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
@@ -40,7 +40,7 @@ exports[`packages_list_row renders 1`] = `
</div>
<div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-mt-1 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
+ class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-fill-1"
>
<div
class="gl-display-flex"
@@ -85,7 +85,7 @@ exports[`packages_list_row renders 1`] = `
</div>
<div
- class="gl-display-flex gl-align-items-center gl-mt-1 gl-min-h-6"
+ class="gl-display-flex gl-align-items-center gl-min-h-6"
>
<span>
<gl-sprintf-stub
@@ -97,7 +97,7 @@ exports[`packages_list_row renders 1`] = `
</div>
<div
- class="gl-w-9 gl-display-none gl-display-sm-flex gl-justify-content-end gl-pr-1"
+ class="gl-w-9 gl-display-none gl-sm-display-flex gl-justify-content-end gl-pr-1"
>
<gl-button-stub
aria-label="Remove package"
diff --git a/spec/frontend/packages/shared/components/packages_list_loader_spec.js b/spec/frontend/packages/shared/components/packages_list_loader_spec.js
index 115a3a7095d..4ff01068f92 100644
--- a/spec/frontend/packages/shared/components/packages_list_loader_spec.js
+++ b/spec/frontend/packages/shared/components/packages_list_loader_spec.js
@@ -30,7 +30,7 @@ describe('PackagesListLoader', () => {
it('has the correct classes', () => {
expect(findDesktopShapes().classes()).toEqual([
'gl-display-none',
- 'gl-display-sm-flex',
+ 'gl-sm-display-flex',
'gl-flex-direction-column',
]);
});
@@ -44,7 +44,7 @@ describe('PackagesListLoader', () => {
it('has the correct classes', () => {
expect(findMobileShapes().classes()).toEqual([
'gl-flex-direction-column',
- 'gl-display-sm-none',
+ 'gl-sm-display-none',
]);
});
});
diff --git a/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
new file mode 100644
index 00000000000..8a1d5044106
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/components/group_settings_app_spec.js
@@ -0,0 +1,99 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlSprintf, GlLink } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import component from '~/packages_and_registries/settings/group/components/group_settings_app.vue';
+import SettingsBlock from '~/vue_shared/components/settings/settings_block.vue';
+import {
+ PACKAGE_SETTINGS_HEADER,
+ PACKAGE_SETTINGS_DESCRIPTION,
+ PACKAGES_DOCS_PATH,
+} from '~/packages_and_registries/settings/group/constants';
+
+import getGroupPackagesSettingsQuery from '~/packages_and_registries/settings/group/graphql/queries/get_group_packages_settings.query.graphql';
+import { groupPackageSettingsMock } from '../mock_data';
+
+const localVue = createLocalVue();
+
+describe('Group Settings App', () => {
+ let wrapper;
+ let apolloProvider;
+
+ const defaultProvide = {
+ defaultExpanded: false,
+ groupPath: 'foo_group_path',
+ };
+
+ const mountComponent = ({
+ provide = defaultProvide,
+ resolver = jest.fn().mockResolvedValue(groupPackageSettingsMock),
+ } = {}) => {
+ localVue.use(VueApollo);
+
+ const requestHandlers = [[getGroupPackagesSettingsQuery, resolver]];
+
+ apolloProvider = createMockApollo(requestHandlers);
+
+ wrapper = shallowMount(component, {
+ localVue,
+ apolloProvider,
+ provide,
+ stubs: {
+ GlSprintf,
+ SettingsBlock,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findSettingsBlock = () => wrapper.find(SettingsBlock);
+ const findDescription = () => wrapper.find('[data-testid="description"');
+ const findLink = () => wrapper.find(GlLink);
+
+ it('renders a settings block', () => {
+ mountComponent();
+
+ expect(findSettingsBlock().exists()).toBe(true);
+ });
+
+ it('passes the correct props to settings block', () => {
+ mountComponent();
+
+ expect(findSettingsBlock().props('defaultExpanded')).toBe(false);
+ });
+
+ it('has the correct header text', () => {
+ mountComponent();
+
+ expect(wrapper.text()).toContain(PACKAGE_SETTINGS_HEADER);
+ });
+
+ it('has the correct description text', () => {
+ mountComponent();
+
+ expect(findDescription().text()).toMatchInterpolatedText(PACKAGE_SETTINGS_DESCRIPTION);
+ });
+
+ it('has the correct link', () => {
+ mountComponent();
+
+ expect(findLink().attributes()).toMatchObject({
+ href: PACKAGES_DOCS_PATH,
+ target: '_blank',
+ });
+ expect(findLink().text()).toBe('More Information');
+ });
+
+ it('calls the graphql API with the proper variables', () => {
+ const resolver = jest.fn().mockResolvedValue(groupPackageSettingsMock);
+ mountComponent({ resolver });
+
+ expect(resolver).toHaveBeenCalledWith({
+ fullPath: defaultProvide.groupPath,
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/settings/group/mock_data.js b/spec/frontend/packages_and_registries/settings/group/mock_data.js
new file mode 100644
index 00000000000..6be43cb4aea
--- /dev/null
+++ b/spec/frontend/packages_and_registries/settings/group/mock_data.js
@@ -0,0 +1,12 @@
+export const groupPackageSettingsMock = {
+ data: {
+ group: {
+ packageSettings: {
+ mavenDuplicatesAllowed: true,
+ mavenDuplicateExceptionRegex: '',
+ __typename: 'PackageSettings',
+ },
+ __typename: 'Group',
+ },
+ },
+};
diff --git a/spec/frontend/pages/projects/edit/mount_search_settings_spec.js b/spec/frontend/pages/projects/edit/mount_search_settings_spec.js
deleted file mode 100644
index b48809b3d00..00000000000
--- a/spec/frontend/pages/projects/edit/mount_search_settings_spec.js
+++ /dev/null
@@ -1,25 +0,0 @@
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import initSearch from '~/search_settings';
-import mountSearchSettings from '~/pages/projects/edit/mount_search_settings';
-
-jest.mock('~/search_settings');
-
-describe('pages/projects/edit/mount_search_settings', () => {
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('initializes search settings when js-search-settings-app is available', async () => {
- setHTMLFixture('<div class="js-search-settings-app"></div>');
-
- await mountSearchSettings();
-
- expect(initSearch).toHaveBeenCalled();
- });
-
- it('does not initialize search settings when js-search-settings-app is unavailable', async () => {
- await mountSearchSettings();
-
- expect(initSearch).not.toHaveBeenCalled();
- });
-});
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index 4a60c7fd509..c9420285bad 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -6,8 +6,8 @@ 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 httpStatusCodes from '~/lib/utils/http_status';
+import { codeCoverageMockData, sortedDataByDates } from './mock_data';
describe('Code Coverage', () => {
let wrapper;
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 9aee6ec7ace..689649063c0 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
@@ -175,7 +175,7 @@ describe('Settings Panel', () => {
wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PRIVATE });
expect(findRepositoryFeatureProjectRow().props().helpText).toBe(
- 'View and edit files in this project',
+ 'View and edit files in this project.',
);
});
@@ -183,7 +183,7 @@ describe('Settings Panel', () => {
wrapper = overrideCurrentSettings({ visibilityLevel: visibilityOptions.PUBLIC });
expect(findRepositoryFeatureProjectRow().props().helpText).toBe(
- 'View and edit files in this project. Non-project members will only have read access',
+ 'View and edit files in this project. Non-project members will only have read access.',
);
});
});
@@ -400,7 +400,7 @@ describe('Settings Panel', () => {
const link = message.find('a');
expect(message.text()).toContain(
- 'LFS objects from this repository are still available to forks',
+ 'LFS objects from this repository are available to forks.',
);
expect(link.text()).toBe('How do I remove them?');
expect(link.attributes('href')).toBe(
@@ -530,7 +530,7 @@ describe('Settings Panel', () => {
it('should contain help text', () => {
expect(wrapper.find({ ref: 'metrics-visibility-settings' }).props().helpText).toBe(
- 'With Metrics Dashboard you can visualize this project performance metrics',
+ "Visualize the project's performance metrics.",
);
});
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
index aae25a3aa6d..76c56bd2815 100644
--- a/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
+++ b/spec/frontend/pipeline_editor/components/commit/commit_form_spec.js
@@ -5,7 +5,7 @@ import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
import { mockCommitMessage, mockDefaultBranch } from '../../mock_data';
-describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
+describe('Pipeline Editor | Commit Form', () => {
let wrapper;
const createComponent = ({ props = {} } = {}, mountFn = shallowMount) => {
@@ -21,8 +21,8 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
});
};
- const findCommitTextarea = () => wrapper.find(GlFormTextarea);
- const findBranchInput = () => wrapper.find(GlFormInput);
+ const findCommitTextarea = () => wrapper.findComponent(GlFormTextarea);
+ const findBranchInput = () => wrapper.findComponent(GlFormInput);
const findNewMrCheckbox = () => wrapper.find('[data-testid="new-mr-checkbox"]');
const findSubmitBtn = () => wrapper.find('[type="submit"]');
const findCancelBtn = () => wrapper.find('[type="reset"]');
diff --git a/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
new file mode 100644
index 00000000000..269c0cb525e
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/commit/commit_section_spec.js
@@ -0,0 +1,223 @@
+import { mount } from '@vue/test-utils';
+import { GlFormTextarea, GlFormInput, GlLoadingIcon } from '@gitlab/ui';
+import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
+import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
+import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
+import commitCreate from '~/pipeline_editor/graphql/mutations/commit_ci_file.mutation.graphql';
+
+import { COMMIT_SUCCESS } from '~/pipeline_editor/constants';
+import {
+ mockCiConfigPath,
+ mockCiYml,
+ mockCommitSha,
+ mockCommitNextSha,
+ mockCommitMessage,
+ mockDefaultBranch,
+ mockProjectFullPath,
+ mockNewMergeRequestPath,
+} from '../../mock_data';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ redirectTo: jest.fn(),
+ refreshCurrentPage: jest.fn(),
+ objectToQuery: jest.requireActual('~/lib/utils/url_utility').objectToQuery,
+ mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams,
+}));
+
+const mockVariables = {
+ projectPath: mockProjectFullPath,
+ startBranch: mockDefaultBranch,
+ message: mockCommitMessage,
+ filePath: mockCiConfigPath,
+ content: mockCiYml,
+ lastCommitId: mockCommitSha,
+};
+
+const mockProvide = {
+ ciConfigPath: mockCiConfigPath,
+ defaultBranch: mockDefaultBranch,
+ projectFullPath: mockProjectFullPath,
+ newMergeRequestPath: mockNewMergeRequestPath,
+};
+
+describe('Pipeline Editor | Commit section', () => {
+ let wrapper;
+ let mockMutate;
+
+ const defaultProps = { ciFileContent: mockCiYml };
+
+ const createComponent = ({ props = {}, options = {}, provide = {} } = {}) => {
+ mockMutate = jest.fn().mockResolvedValue({
+ data: {
+ commitCreate: {
+ errors: [],
+ commit: {
+ sha: mockCommitNextSha,
+ },
+ },
+ },
+ });
+
+ wrapper = mount(CommitSection, {
+ propsData: { ...defaultProps, ...props },
+ provide: { ...mockProvide, ...provide },
+ data() {
+ return {
+ commitSha: mockCommitSha,
+ };
+ },
+ mocks: {
+ $apollo: {
+ mutate: mockMutate,
+ },
+ },
+ attachTo: document.body,
+ ...options,
+ });
+ };
+
+ const findCommitForm = () => wrapper.findComponent(CommitForm);
+ const findCommitBtnLoadingIcon = () =>
+ wrapper.find('[type="submit"]').findComponent(GlLoadingIcon);
+
+ const submitCommit = async ({
+ message = mockCommitMessage,
+ branch = mockDefaultBranch,
+ openMergeRequest = false,
+ } = {}) => {
+ await findCommitForm().findComponent(GlFormTextarea).setValue(message);
+ await findCommitForm().findComponent(GlFormInput).setValue(branch);
+ if (openMergeRequest) {
+ await findCommitForm().find('[data-testid="new-mr-checkbox"]').setChecked(openMergeRequest);
+ }
+ await findCommitForm().find('[type="submit"]').trigger('click');
+ // Simulate the write to local cache that occurs after a commit
+ await wrapper.setData({ commitSha: mockCommitNextSha });
+ };
+
+ const cancelCommitForm = async () => {
+ const findCancelBtn = () => wrapper.find('[type="reset"]');
+ await findCancelBtn().trigger('click');
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ mockMutate.mockReset();
+
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('when the user commits changes to the current branch', () => {
+ beforeEach(async () => {
+ await submitCommit();
+ });
+
+ it('calls the mutation with the default branch', () => {
+ expect(mockMutate).toHaveBeenCalledTimes(1);
+ expect(mockMutate).toHaveBeenCalledWith({
+ mutation: commitCreate,
+ update: expect.any(Function),
+ variables: {
+ ...mockVariables,
+ branch: mockDefaultBranch,
+ },
+ });
+ });
+
+ it('emits an event to communicate the commit was successful', () => {
+ expect(wrapper.emitted('commit')).toHaveLength(1);
+ expect(wrapper.emitted('commit')[0]).toEqual([{ type: COMMIT_SUCCESS }]);
+ });
+
+ it('shows no saving state', () => {
+ expect(findCommitBtnLoadingIcon().exists()).toBe(false);
+ });
+
+ it('a second commit submits the latest sha, keeping the form updated', async () => {
+ await submitCommit();
+
+ expect(mockMutate).toHaveBeenCalledTimes(2);
+ expect(mockMutate).toHaveBeenCalledWith({
+ mutation: commitCreate,
+ update: expect.any(Function),
+ variables: {
+ ...mockVariables,
+ lastCommitId: mockCommitNextSha,
+ branch: mockDefaultBranch,
+ },
+ });
+ });
+ });
+
+ describe('when the user commits changes to a new branch', () => {
+ const newBranch = 'new-branch';
+
+ beforeEach(async () => {
+ await submitCommit({
+ branch: newBranch,
+ });
+ });
+
+ it('calls the mutation with the new branch', () => {
+ expect(mockMutate).toHaveBeenCalledWith({
+ mutation: commitCreate,
+ update: expect.any(Function),
+ variables: {
+ ...mockVariables,
+ branch: newBranch,
+ },
+ });
+ });
+ });
+
+ describe('when the user commits changes to open a new merge request', () => {
+ const newBranch = 'new-branch';
+
+ beforeEach(async () => {
+ await submitCommit({
+ branch: newBranch,
+ openMergeRequest: true,
+ });
+ });
+
+ it('redirects to the merge request page with source and target branches', () => {
+ const branchesQuery = objectToQuery({
+ 'merge_request[source_branch]': newBranch,
+ 'merge_request[target_branch]': mockDefaultBranch,
+ });
+
+ expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`);
+ });
+ });
+
+ describe('when the commit is ocurring', () => {
+ it('shows a saving state', async () => {
+ mockMutate.mockImplementationOnce(() => {
+ expect(findCommitBtnLoadingIcon().exists()).toBe(true);
+ return Promise.resolve();
+ });
+
+ await submitCommit({
+ message: mockCommitMessage,
+ branch: mockDefaultBranch,
+ openMergeRequest: false,
+ });
+ });
+ });
+
+ describe('when the commit form is cancelled', () => {
+ beforeEach(async () => {
+ createComponent();
+ });
+
+ it('emits an event so that it cab be reseted', async () => {
+ await cancelCommitForm();
+
+ expect(wrapper.emitted('resetContent')).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js b/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js
new file mode 100644
index 00000000000..df15a6c8e7f
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/header/pipeline_editor_header_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
+import ValidationSegment from '~/pipeline_editor/components/header/validation_segment.vue';
+
+import { mockLintResponse } from '../../mock_data';
+
+describe('Pipeline editor header', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(PipelineEditorHeader, {
+ props: {
+ ciConfigData: mockLintResponse,
+ isCiConfigDataLoading: false,
+ },
+ });
+ };
+
+ const findValidationSegment = () => wrapper.findComponent(ValidationSegment);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+ it('renders the validation segment', () => {
+ expect(findValidationSegment().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/info/validation_segment_spec.js b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
index 8a991d82018..c5b88e4b904 100644
--- a/spec/frontend/pipeline_editor/components/info/validation_segment_spec.js
+++ b/spec/frontend/pipeline_editor/components/header/validation_segment_spec.js
@@ -3,7 +3,9 @@ import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { sprintf } from '~/locale';
-import ValidationSegment, { i18n } from '~/pipeline_editor/components/info/validation_segment.vue';
+import ValidationSegment, {
+ i18n,
+} from '~/pipeline_editor/components/header/validation_segment.vue';
import { CI_CONFIG_STATUS_INVALID } from '~/pipeline_editor/constants';
import { mockYmlHelpPagePath, mergeUnwrappedCiConfig } from '../../mock_data';
@@ -29,6 +31,11 @@ describe('~/pipeline_editor/components/info/validation_segment.vue', () => {
const findLearnMoreLink = () => wrapper.findByTestId('learnMoreLink');
const findValidationMsg = () => wrapper.findByTestId('validationMsg');
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
it('shows the loading state', () => {
createComponent({ loading: true });
diff --git a/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
new file mode 100644
index 00000000000..275e2987f38
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -0,0 +1,129 @@
+import { nextTick } from 'vue';
+import { shallowMount, mount } from '@vue/test-utils';
+import { GlLoadingIcon } from '@gitlab/ui';
+import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
+import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
+import CiLint from '~/pipeline_editor/components/lint/ci_lint.vue';
+
+import { mockLintResponse, mockCiYml } from '../mock_data';
+
+describe('Pipeline editor tabs component', () => {
+ let wrapper;
+ const MockTextEditor = {
+ template: '<div />',
+ };
+ const mockProvide = {
+ glFeatures: {
+ ciConfigVisualizationTab: true,
+ },
+ };
+
+ const createComponent = ({ props = {}, provide = {}, mountFn = shallowMount } = {}) => {
+ wrapper = mountFn(PipelineEditorTabs, {
+ propsData: {
+ ciConfigData: mockLintResponse,
+ ciFileContent: mockCiYml,
+ isCiConfigDataLoading: false,
+ ...props,
+ },
+ provide: { ...mockProvide, ...provide },
+ stubs: {
+ TextEditor: MockTextEditor,
+ },
+ });
+ };
+
+ const findEditorTab = () => wrapper.find('[data-testid="editor-tab"]');
+ const findLintTab = () => wrapper.find('[data-testid="lint-tab"]');
+ const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]');
+ const findCiLint = () => wrapper.findComponent(CiLint);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findPipelineGraph = () => wrapper.findComponent(PipelineGraph);
+ const findTextEditor = () => wrapper.findComponent(MockTextEditor);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('tabs', () => {
+ describe('editor tab', () => {
+ it('displays editor only after the tab is mounted', async () => {
+ createComponent({ mountFn: mount });
+
+ expect(findTextEditor().exists()).toBe(false);
+
+ await nextTick();
+
+ expect(findTextEditor().exists()).toBe(true);
+ expect(findEditorTab().exists()).toBe(true);
+ });
+ });
+
+ describe('visualization tab', () => {
+ describe('with feature flag on', () => {
+ describe('while loading', () => {
+ beforeEach(() => {
+ createComponent({ props: { isCiConfigDataLoading: true } });
+ });
+
+ it('displays a loading icon if the lint query is loading', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
+ });
+ describe('after loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('display the tab and visualization', () => {
+ expect(findVisualizationTab().exists()).toBe(true);
+ expect(findPipelineGraph().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('with feature flag off', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: { ciConfigVisualizationTab: false },
+ },
+ });
+ });
+
+ it('does not display the tab or component', () => {
+ expect(findVisualizationTab().exists()).toBe(false);
+ expect(findPipelineGraph().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('lint tab', () => {
+ describe('while loading', () => {
+ beforeEach(() => {
+ createComponent({ props: { isCiConfigDataLoading: true } });
+ });
+
+ it('displays a loading icon if the lint query is loading', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not display the lint component', () => {
+ expect(findCiLint().exists()).toBe(false);
+ });
+ });
+ describe('after loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('display the tab and the lint component', () => {
+ expect(findLintTab().exists()).toBe(true);
+ expect(findCiLint().exists()).toBe(true);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_editor/components/text_editor_spec.js b/spec/frontend/pipeline_editor/components/text_editor_spec.js
index 9221d64c44b..9bb4bb6c210 100644
--- a/spec/frontend/pipeline_editor/components/text_editor_spec.js
+++ b/spec/frontend/pipeline_editor/components/text_editor_spec.js
@@ -1,4 +1,7 @@
import { shallowMount } from '@vue/test-utils';
+
+import { EDITOR_READY_EVENT } from '~/editor/constants';
+import TextEditor from '~/pipeline_editor/components/text_editor.vue';
import {
mockCiConfigPath,
mockCiYml,
@@ -7,9 +10,7 @@ import {
mockProjectNamespace,
} from '../mock_data';
-import TextEditor from '~/pipeline_editor/components/text_editor.vue';
-
-describe('~/pipeline_editor/components/text_editor.vue', () => {
+describe('Pipeline Editor | Text editor component', () => {
let wrapper;
let editorReadyListener;
@@ -20,7 +21,7 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
template: '<div/>',
props: ['value', 'fileName'],
mounted() {
- this.$emit('editor-ready');
+ this.$emit(EDITOR_READY_EVENT);
},
methods: {
getEditor: () => ({
@@ -35,16 +36,19 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
provide: {
projectPath: mockProjectPath,
projectNamespace: mockProjectNamespace,
- },
- propsData: {
ciConfigPath: mockCiConfigPath,
- commitSha: mockCommitSha,
},
attrs: {
value: mockCiYml,
},
+ // Simulate graphQL client query result
+ data() {
+ return {
+ commitSha: mockCommitSha,
+ };
+ },
listeners: {
- 'editor-ready': editorReadyListener,
+ [EDITOR_READY_EVENT]: editorReadyListener,
},
stubs: {
EditorLite: MockEditorLite,
@@ -53,41 +57,64 @@ describe('~/pipeline_editor/components/text_editor.vue', () => {
});
};
- const findEditor = () => wrapper.find(MockEditorLite);
+ const findEditor = () => wrapper.findComponent(MockEditorLite);
- beforeEach(() => {
- editorReadyListener = jest.fn();
- mockUse = jest.fn();
- mockRegisterCiSchema = jest.fn();
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
- createComponent();
+ mockUse.mockClear();
+ mockRegisterCiSchema.mockClear();
});
- it('contains an editor', () => {
- expect(findEditor().exists()).toBe(true);
- });
+ describe('template', () => {
+ beforeEach(() => {
+ editorReadyListener = jest.fn();
+ mockUse = jest.fn();
+ mockRegisterCiSchema = jest.fn();
- it('editor contains the value provided', () => {
- expect(findEditor().props('value')).toBe(mockCiYml);
- });
+ createComponent();
+ });
- it('editor is configured for the CI config path', () => {
- expect(findEditor().props('fileName')).toBe(mockCiConfigPath);
- });
+ it('contains an editor', () => {
+ expect(findEditor().exists()).toBe(true);
+ });
- it('editor is configured with syntax highligting', async () => {
- expect(mockUse).toHaveBeenCalledTimes(1);
- expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
- expect(mockRegisterCiSchema).toHaveBeenCalledWith({
- projectNamespace: mockProjectNamespace,
- projectPath: mockProjectPath,
- ref: mockCommitSha,
+ it('editor contains the value provided', () => {
+ expect(findEditor().props('value')).toBe(mockCiYml);
+ });
+
+ it('editor is configured for the CI config path', () => {
+ expect(findEditor().props('fileName')).toBe(mockCiConfigPath);
+ });
+
+ it('bubbles up events', () => {
+ findEditor().vm.$emit(EDITOR_READY_EVENT);
+
+ expect(editorReadyListener).toHaveBeenCalled();
});
});
- it('bubbles up events', () => {
- findEditor().vm.$emit('editor-ready');
+ describe('register CI schema', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ // Since the editor will have already mounted, the event will have fired.
+ // To ensure we properly test this, we clear the mock and re-remit the event.
+ mockRegisterCiSchema.mockClear();
+ mockUse.mockClear();
- expect(editorReadyListener).toHaveBeenCalled();
+ findEditor().vm.$emit(EDITOR_READY_EVENT);
+ });
+
+ it('configures editor with syntax highlight', async () => {
+ expect(mockUse).toHaveBeenCalledTimes(1);
+ expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
+ expect(mockRegisterCiSchema).toHaveBeenCalledWith({
+ projectNamespace: mockProjectNamespace,
+ projectPath: mockProjectPath,
+ ref: mockCommitSha,
+ });
+ });
});
});
diff --git a/spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js b/spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
new file mode 100644
index 00000000000..44fda2812d8
--- /dev/null
+++ b/spec/frontend/pipeline_editor/components/ui/confirm_unsaved_changes_dialog_spec.js
@@ -0,0 +1,42 @@
+import { shallowMount } from '@vue/test-utils';
+import ConfirmDialog from '~/pipeline_editor/components/ui/confirm_unsaved_changes_dialog.vue';
+
+describe('pipeline_editor/components/ui/confirm_unsaved_changes_dialog', () => {
+ let beforeUnloadEvent;
+ let setDialogContent;
+ let wrapper;
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(ConfirmDialog, {
+ propsData,
+ });
+ };
+
+ beforeEach(() => {
+ beforeUnloadEvent = new Event('beforeunload');
+ jest.spyOn(beforeUnloadEvent, 'preventDefault');
+ setDialogContent = jest.spyOn(beforeUnloadEvent, 'returnValue', 'set');
+ });
+
+ afterEach(() => {
+ beforeUnloadEvent.preventDefault.mockRestore();
+ setDialogContent.mockRestore();
+ wrapper.destroy();
+ });
+
+ it('shows confirmation dialog when there are unsaved changes', () => {
+ createComponent({ hasUnsavedChanges: true });
+ window.dispatchEvent(beforeUnloadEvent);
+
+ expect(beforeUnloadEvent.preventDefault).toHaveBeenCalled();
+ expect(setDialogContent).toHaveBeenCalledWith('');
+ });
+
+ it('does not show confirmation dialog when there are no unsaved changes', () => {
+ createComponent({ hasUnsavedChanges: false });
+ window.dispatchEvent(beforeUnloadEvent);
+
+ expect(beforeUnloadEvent.preventDefault).not.toHaveBeenCalled();
+ expect(setDialogContent).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js
index 3e008527415..bee9f53814e 100644
--- a/spec/frontend/pipeline_editor/graphql/resolvers_spec.js
+++ b/spec/frontend/pipeline_editor/graphql/resolvers_spec.js
@@ -1,5 +1,8 @@
import MockAdapter from 'axios-mock-adapter';
import Api from '~/api';
+import httpStatus from '~/lib/utils/http_status';
+import axios from '~/lib/utils/axios_utils';
+import { resolvers } from '~/pipeline_editor/graphql/resolvers';
import {
mockCiConfigPath,
mockCiYml,
@@ -7,9 +10,6 @@ import {
mockLintResponse,
mockProjectFullPath,
} from '../mock_data';
-import httpStatus from '~/lib/utils/http_status';
-import axios from '~/lib/utils/axios_utils';
-import { resolvers } from '~/pipeline_editor/graphql/resolvers';
jest.mock('~/api', () => {
return {
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
index d6b90900600..7ffdf1815ba 100644
--- a/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/pipeline_editor/pipeline_editor_app_spec.js
@@ -1,119 +1,71 @@
-import { nextTick } from 'vue';
-import { mount, shallowMount, createLocalVue } from '@vue/test-utils';
-import { GlAlert, GlButton, GlFormInput, GlFormTextarea, GlLoadingIcon, GlTabs } from '@gitlab/ui';
-import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import { GlAlert, GlButton, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
+import TextEditor from '~/pipeline_editor/components/text_editor.vue';
import httpStatusCodes from '~/lib/utils/http_status';
-import { objectToQuery, redirectTo, refreshCurrentPage } from '~/lib/utils/url_utility';
+
+import { COMMIT_SUCCESS, COMMIT_FAILURE, LOAD_FAILURE_UNKNOWN } from '~/pipeline_editor/constants';
+import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
+import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
+import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
+import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
import {
mockCiConfigPath,
mockCiConfigQueryResponse,
mockCiYml,
- mockCommitSha,
- mockCommitNextSha,
- mockCommitMessage,
mockDefaultBranch,
- mockProjectPath,
mockProjectFullPath,
- mockProjectNamespace,
- mockNewMergeRequestPath,
} from './mock_data';
-import CommitForm from '~/pipeline_editor/components/commit/commit_form.vue';
-import getCiConfigData from '~/pipeline_editor/graphql/queries/ci_config.graphql';
-import EditorTab from '~/pipeline_editor/components/ui/editor_tab.vue';
-import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
-import PipelineEditorApp from '~/pipeline_editor/pipeline_editor_app.vue';
-import TextEditor from '~/pipeline_editor/components/text_editor.vue';
-
const localVue = createLocalVue();
localVue.use(VueApollo);
-jest.mock('~/lib/utils/url_utility', () => ({
- redirectTo: jest.fn(),
- refreshCurrentPage: jest.fn(),
- objectToQuery: jest.requireActual('~/lib/utils/url_utility').objectToQuery,
- mergeUrlParams: jest.requireActual('~/lib/utils/url_utility').mergeUrlParams,
-}));
-
const MockEditorLite = {
template: '<div/>',
};
const mockProvide = {
+ ciConfigPath: mockCiConfigPath,
+ defaultBranch: mockDefaultBranch,
projectFullPath: mockProjectFullPath,
- projectPath: mockProjectPath,
- projectNamespace: mockProjectNamespace,
- glFeatures: {
- ciConfigVisualizationTab: true,
- },
};
-describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
+describe('Pipeline editor app component', () => {
let wrapper;
let mockApollo;
let mockBlobContentData;
let mockCiConfigData;
- let mockMutate;
-
- const createComponent = ({
- props = {},
- blobLoading = false,
- lintLoading = false,
- options = {},
- mountFn = shallowMount,
- provide = mockProvide,
- } = {}) => {
- mockMutate = jest.fn().mockResolvedValue({
- data: {
- commitCreate: {
- errors: [],
- commit: {
- sha: mockCommitNextSha,
- },
- },
- },
- });
- wrapper = mountFn(PipelineEditorApp, {
- propsData: {
- ciConfigPath: mockCiConfigPath,
- commitSha: mockCommitSha,
- defaultBranch: mockDefaultBranch,
- newMergeRequestPath: mockNewMergeRequestPath,
- ...props,
- },
- provide,
+ const createComponent = ({ blobLoading = false, options = {} } = {}) => {
+ wrapper = shallowMount(PipelineEditorApp, {
+ provide: mockProvide,
stubs: {
GlTabs,
GlButton,
CommitForm,
EditorLite: MockEditorLite,
- TextEditor,
},
mocks: {
$apollo: {
queries: {
- content: {
+ initialCiFileContent: {
loading: blobLoading,
},
ciConfigData: {
- loading: lintLoading,
+ loading: false,
},
},
- mutate: mockMutate,
},
},
- // attachTo is required for input/submit events
- attachTo: mountFn === mount ? document.body : null,
...options,
});
};
- const createComponentWithApollo = ({ props = {}, mountFn = shallowMount } = {}) => {
+ const createComponentWithApollo = ({ props = {} } = {}) => {
const handlers = [[getCiConfigData, mockCiConfigData]];
const resolvers = {
Query: {
@@ -134,18 +86,13 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
apolloProvider: mockApollo,
};
- createComponent({ props, options }, mountFn);
+ createComponent({ props, options });
};
- const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
- const findAlert = () => wrapper.find(GlAlert);
- const findTabAt = (i) => wrapper.findAll(EditorTab).at(i);
- const findVisualizationTab = () => wrapper.find('[data-testid="visualization-tab"]');
- const findTextEditor = () => wrapper.find(TextEditor);
- const findEditorLite = () => wrapper.find(MockEditorLite);
- const findCommitForm = () => wrapper.find(CommitForm);
- const findPipelineGraph = () => wrapper.find(PipelineGraph);
- const findCommitBtnLoadingIcon = () => wrapper.find('[type="submit"]').find(GlLoadingIcon);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findEditorHome = () => wrapper.findComponent(PipelineEditorHome);
+ const findTextEditor = () => wrapper.findComponent(TextEditor);
beforeEach(() => {
mockBlobContentData = jest.fn();
@@ -155,9 +102,6 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
afterEach(() => {
mockBlobContentData.mockReset();
mockCiConfigData.mockReset();
- refreshCurrentPage.mockReset();
- redirectTo.mockReset();
- mockMutate.mockReset();
wrapper.destroy();
wrapper = null;
@@ -170,245 +114,6 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
expect(findTextEditor().exists()).toBe(false);
});
- describe('tabs', () => {
- describe('editor tab', () => {
- it('displays editor only after the tab is mounted', async () => {
- createComponent({ mountFn: mount });
-
- expect(findTabAt(0).find(TextEditor).exists()).toBe(false);
-
- await nextTick();
-
- expect(findTabAt(0).find(TextEditor).exists()).toBe(true);
- });
- });
-
- describe('visualization tab', () => {
- describe('with feature flag on', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('display the tab', () => {
- expect(findVisualizationTab().exists()).toBe(true);
- });
-
- it('displays a loading icon if the lint query is loading', () => {
- createComponent({ lintLoading: true });
-
- expect(findLoadingIcon().exists()).toBe(true);
- expect(findPipelineGraph().exists()).toBe(false);
- });
- });
-
- describe('with feature flag off', () => {
- beforeEach(() => {
- createComponent({
- provide: {
- ...mockProvide,
- glFeatures: { ciConfigVisualizationTab: false },
- },
- });
- });
-
- it('does not display the tab', () => {
- expect(findVisualizationTab().exists()).toBe(false);
- });
- });
- });
- });
-
- describe('when data is set', () => {
- beforeEach(async () => {
- createComponent({ mountFn: mount });
-
- wrapper.setData({
- content: mockCiYml,
- contentModel: mockCiYml,
- });
-
- await waitForPromises();
- });
-
- it('displays content after the query loads', () => {
- expect(findLoadingIcon().exists()).toBe(false);
-
- expect(findEditorLite().attributes('value')).toBe(mockCiYml);
- expect(findEditorLite().attributes('file-name')).toBe(mockCiConfigPath);
- });
-
- it('configures text editor', () => {
- expect(findTextEditor().props('commitSha')).toBe(mockCommitSha);
- });
-
- describe('commit form', () => {
- const mockVariables = {
- content: mockCiYml,
- filePath: mockCiConfigPath,
- lastCommitId: mockCommitSha,
- message: mockCommitMessage,
- projectPath: mockProjectFullPath,
- startBranch: mockDefaultBranch,
- };
-
- const findInForm = (selector) => findCommitForm().find(selector);
-
- const submitCommit = async ({
- message = mockCommitMessage,
- branch = mockDefaultBranch,
- openMergeRequest = false,
- } = {}) => {
- await findInForm(GlFormTextarea).setValue(message);
- await findInForm(GlFormInput).setValue(branch);
- if (openMergeRequest) {
- await findInForm('[data-testid="new-mr-checkbox"]').setChecked(openMergeRequest);
- }
- await findInForm('[type="submit"]').trigger('click');
- };
-
- const cancelCommitForm = async () => {
- const findCancelBtn = () => wrapper.find('[type="reset"]');
- await findCancelBtn().trigger('click');
- };
-
- describe('when the user commits changes to the current branch', () => {
- beforeEach(async () => {
- await submitCommit();
- });
-
- it('calls the mutation with the default branch', () => {
- expect(mockMutate).toHaveBeenCalledWith({
- mutation: expect.any(Object),
- variables: {
- ...mockVariables,
- branch: mockDefaultBranch,
- },
- });
- });
-
- it('displays an alert to indicate success', () => {
- expect(findAlert().text()).toMatchInterpolatedText(
- 'Your changes have been successfully committed.',
- );
- });
-
- it('shows no saving state', () => {
- expect(findCommitBtnLoadingIcon().exists()).toBe(false);
- });
-
- it('a second commit submits the latest sha, keeping the form updated', async () => {
- await submitCommit();
-
- expect(mockMutate).toHaveBeenCalledTimes(2);
- expect(mockMutate).toHaveBeenLastCalledWith({
- mutation: expect.any(Object),
- variables: {
- ...mockVariables,
- lastCommitId: mockCommitNextSha,
- branch: mockDefaultBranch,
- },
- });
- });
- });
-
- describe('when the user commits changes to a new branch', () => {
- const newBranch = 'new-branch';
-
- beforeEach(async () => {
- await submitCommit({
- branch: newBranch,
- });
- });
-
- it('calls the mutation with the new branch', () => {
- expect(mockMutate).toHaveBeenCalledWith({
- mutation: expect.any(Object),
- variables: {
- ...mockVariables,
- branch: newBranch,
- },
- });
- });
- });
-
- describe('when the user commits changes to open a new merge request', () => {
- const newBranch = 'new-branch';
-
- beforeEach(async () => {
- await submitCommit({
- branch: newBranch,
- openMergeRequest: true,
- });
- });
-
- it('redirects to the merge request page with source and target branches', () => {
- const branchesQuery = objectToQuery({
- 'merge_request[source_branch]': newBranch,
- 'merge_request[target_branch]': mockDefaultBranch,
- });
-
- expect(redirectTo).toHaveBeenCalledWith(`${mockNewMergeRequestPath}?${branchesQuery}`);
- });
- });
-
- describe('when the commit is ocurring', () => {
- it('shows a saving state', async () => {
- await mockMutate.mockImplementationOnce(() => {
- expect(findCommitBtnLoadingIcon().exists()).toBe(true);
- return Promise.resolve();
- });
-
- await submitCommit({
- message: mockCommitMessage,
- branch: mockDefaultBranch,
- openMergeRequest: false,
- });
- });
- });
-
- describe('when the commit fails', () => {
- it('shows an error message', async () => {
- mockMutate.mockRejectedValueOnce(new Error('commit failed'));
-
- await submitCommit();
-
- await waitForPromises();
-
- expect(findAlert().text()).toMatchInterpolatedText(
- 'The GitLab CI configuration could not be updated. commit failed',
- );
- });
-
- it('shows an unkown error', async () => {
- mockMutate.mockRejectedValueOnce();
-
- await submitCommit();
-
- await waitForPromises();
-
- expect(findAlert().text()).toMatchInterpolatedText(
- 'The GitLab CI configuration could not be updated.',
- );
- });
- });
-
- describe('when the commit form is cancelled', () => {
- const otherContent = 'other content';
-
- beforeEach(async () => {
- findTextEditor().vm.$emit('input', otherContent);
- await nextTick();
- });
-
- it('content is restored after cancel is called', async () => {
- await cancelCommitForm();
-
- expect(findEditorLite().attributes('value')).toBe(mockCiYml);
- });
- });
- });
- });
-
describe('when queries are called', () => {
beforeEach(() => {
mockBlobContentData.mockResolvedValue(mockCiYml);
@@ -422,14 +127,12 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
await waitForPromises();
});
- it('shows editor and commit form', () => {
- expect(findEditorLite().exists()).toBe(true);
- expect(findTextEditor().exists()).toBe(true);
+ it('shows pipeline editor home component', () => {
+ expect(findEditorHome().exists()).toBe(true);
});
- it('no error is shown when data is set', async () => {
+ it('no error is shown when data is set', () => {
expect(findAlert().exists()).toBe(false);
- expect(findEditorLite().attributes('value')).toBe(mockCiYml);
});
it('ci config query is called with correct variables', async () => {
@@ -445,10 +148,10 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
});
describe('when no file exists', () => {
- const expectedAlertMsg =
+ const noFileAlertMsg =
'There is no .gitlab-ci.yml file in this repository, please add one and visit the Pipeline Editor again.';
- it('shows a 404 error message and does not show editor or commit form', async () => {
+ it('shows a 404 error message and does not show editor home component', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.NOT_FOUND,
@@ -458,12 +161,11 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
await waitForPromises();
- expect(findAlert().text()).toBe(expectedAlertMsg);
- expect(findEditorLite().exists()).toBe(false);
- expect(findTextEditor().exists()).toBe(false);
+ expect(findAlert().text()).toBe(noFileAlertMsg);
+ expect(findEditorHome().exists()).toBe(false);
});
- it('shows a 400 error message and does not show editor or commit form', async () => {
+ it('shows a 400 error message and does not show editor home component', async () => {
mockBlobContentData.mockRejectedValueOnce({
response: {
status: httpStatusCodes.BAD_REQUEST,
@@ -473,9 +175,8 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
await waitForPromises();
- expect(findAlert().text()).toBe(expectedAlertMsg);
- expect(findEditorLite().exists()).toBe(false);
- expect(findTextEditor().exists()).toBe(false);
+ expect(findAlert().text()).toBe(noFileAlertMsg);
+ expect(findEditorHome().exists()).toBe(false);
});
it('shows a unkown error message', async () => {
@@ -483,9 +184,60 @@ describe('~/pipeline_editor/pipeline_editor_app.vue', () => {
createComponentWithApollo();
await waitForPromises();
- expect(findAlert().text()).toBe('The CI configuration was not loaded, please try again.');
- expect(findEditorLite().exists()).toBe(true);
- expect(findTextEditor().exists()).toBe(true);
+ expect(findAlert().text()).toBe(wrapper.vm.$options.errorTexts[LOAD_FAILURE_UNKNOWN]);
+ expect(findEditorHome().exists()).toBe(true);
+ });
+ });
+
+ describe('when the user commits', () => {
+ const updateFailureMessage = 'The GitLab CI configuration could not be updated.';
+
+ describe('and the commit mutation succeeds', () => {
+ beforeEach(() => {
+ createComponent();
+
+ findEditorHome().vm.$emit('commit', { type: COMMIT_SUCCESS });
+ });
+
+ it('shows a confirmation message', () => {
+ expect(findAlert().text()).toBe(wrapper.vm.$options.successTexts[COMMIT_SUCCESS]);
+ });
+ });
+ describe('and the commit mutation fails', () => {
+ const commitFailedReasons = ['Commit failed'];
+
+ beforeEach(() => {
+ createComponent();
+
+ findEditorHome().vm.$emit('showError', {
+ type: COMMIT_FAILURE,
+ reasons: commitFailedReasons,
+ });
+ });
+
+ it('shows an error message', () => {
+ expect(findAlert().text()).toMatchInterpolatedText(
+ `${updateFailureMessage} ${commitFailedReasons[0]}`,
+ );
+ });
+ });
+ describe('when an unknown error occurs', () => {
+ const unknownReasons = ['Commit failed'];
+
+ beforeEach(() => {
+ createComponent();
+
+ findEditorHome().vm.$emit('showError', {
+ type: COMMIT_FAILURE,
+ reasons: unknownReasons,
+ });
+ });
+
+ it('shows an error message', () => {
+ expect(findAlert().text()).toMatchInterpolatedText(
+ `${updateFailureMessage} ${unknownReasons[0]}`,
+ );
+ });
});
});
});
diff --git a/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
new file mode 100644
index 00000000000..6ef1b3cdd1c
--- /dev/null
+++ b/spec/frontend/pipeline_editor/pipeline_editor_home_spec.js
@@ -0,0 +1,50 @@
+import { shallowMount } from '@vue/test-utils';
+
+import PipelineEditorHome from '~/pipeline_editor/pipeline_editor_home.vue';
+import PipelineEditorTabs from '~/pipeline_editor/components/pipeline_editor_tabs.vue';
+import CommitSection from '~/pipeline_editor/components/commit/commit_section.vue';
+import PipelineEditorHeader from '~/pipeline_editor/components/header/pipeline_editor_header.vue';
+
+import { mockLintResponse, mockCiYml } from './mock_data';
+
+describe('Pipeline editor home wrapper', () => {
+ let wrapper;
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(PipelineEditorHome, {
+ propsData: {
+ ciConfigData: mockLintResponse,
+ ciFileContent: mockCiYml,
+ isCiConfigDataLoading: false,
+ ...props,
+ },
+ });
+ };
+
+ const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorTabs);
+ const findPipelineEditorTabs = () => wrapper.findComponent(CommitSection);
+ const findCommitSection = () => wrapper.findComponent(PipelineEditorHeader);
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('renders', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows the pipeline editor header', () => {
+ expect(findPipelineEditorHeader().exists()).toBe(true);
+ });
+
+ it('shows the pipeline editor tabs', () => {
+ expect(findPipelineEditorTabs().exists()).toBe(true);
+ });
+
+ it('shows the commit section', () => {
+ expect(findCommitSection().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
index 421ad9f4939..3f1cf67127e 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
@@ -5,6 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import httpStatusCodes from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
+import { redirectTo } from '~/lib/utils/url_utility';
import {
mockBranches,
mockTags,
@@ -13,7 +14,6 @@ import {
mockProjectId,
mockError,
} from '../mock_data';
-import { redirectTo } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
@@ -34,6 +34,7 @@ describe('Pipeline New Form', () => {
const findForm = () => wrapper.find(GlForm);
const findDropdown = () => wrapper.find(GlDropdown);
const findDropdownItems = () => wrapper.findAll(GlDropdownItem);
+ const findSubmitButton = () => wrapper.find('[data-testid="run_pipeline_button"]');
const findVariableRows = () => wrapper.findAll('[data-testid="ci-variable-row"]');
const findRemoveIcons = () => wrapper.findAll('[data-testid="remove-ci-variable-row"]');
const findKeyInputs = () => wrapper.findAll('[data-testid="pipeline-form-ci-variable-key"]');
@@ -155,6 +156,18 @@ describe('Pipeline New Form', () => {
await waitForPromises();
});
+
+ it('disables the submit button immediately after submitting', async () => {
+ createComponent();
+
+ expect(findSubmitButton().props('disabled')).toBe(false);
+
+ findForm().vm.$emit('submit', dummySubmitEvent);
+ await waitForPromises();
+
+ expect(findSubmitButton().props('disabled')).toBe(true);
+ });
+
it('creates pipeline with full ref and variables', async () => {
createComponent();
@@ -167,6 +180,7 @@ describe('Pipeline New Form', () => {
expect(getExpectedPostParams().ref).toEqual(wrapper.vm.$data.refValue.fullName);
expect(redirectTo).toHaveBeenCalledWith(`${pipelinesPath}/${postResponse.id}`);
});
+
it('creates a pipeline with short ref and variables', async () => {
// query params are used
createComponent('', mockParams);
@@ -225,42 +239,29 @@ describe('Pipeline New Form', () => {
});
});
- describe('when feature flag new_pipeline_form_prefilled_vars is enabled', () => {
- let origGon;
-
+ describe('when yml defines a variable', () => {
const mockYmlKey = 'yml_var';
const mockYmlValue = 'yml_var_val';
const mockYmlDesc = 'A var from yml.';
- beforeAll(() => {
- origGon = window.gon;
- window.gon = { features: { newPipelineFormPrefilledVars: true } };
- });
-
- afterAll(() => {
- window.gon = origGon;
- });
-
- describe('loading state', () => {
- it('loading icon is shown when content is requested and hidden when received', async () => {
- createComponent('', mockParams, mount);
+ it('loading icon is shown when content is requested and hidden when received', async () => {
+ createComponent('', mockParams, mount);
- mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
- [mockYmlKey]: {
- value: mockYmlValue,
- description: mockYmlDesc,
- },
- });
+ mock.onGet(configVariablesPath).reply(httpStatusCodes.OK, {
+ [mockYmlKey]: {
+ value: mockYmlValue,
+ description: mockYmlDesc,
+ },
+ });
- expect(findLoadingIcon().exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
- await waitForPromises();
+ await waitForPromises();
- expect(findLoadingIcon().exists()).toBe(false);
- });
+ expect(findLoadingIcon().exists()).toBe(false);
});
- describe('when yml defines a variable with description', () => {
+ describe('with description', () => {
beforeEach(async () => {
createComponent('', mockParams, mount);
@@ -302,7 +303,7 @@ describe('Pipeline New Form', () => {
});
});
- describe('when yml defines a variable without description', () => {
+ describe('without description', () => {
beforeEach(async () => {
createComponent('', mockParams, mount);
@@ -325,31 +326,55 @@ describe('Pipeline New Form', () => {
describe('Form errors and warnings', () => {
beforeEach(() => {
createComponent();
+ });
- mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError);
+ describe('when the error response can be handled', () => {
+ beforeEach(async () => {
+ mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError);
- findForm().vm.$emit('submit', dummySubmitEvent);
+ findForm().vm.$emit('submit', dummySubmitEvent);
- return waitForPromises();
- });
+ await waitForPromises();
+ });
- it('shows both error and warning', () => {
- expect(findErrorAlert().exists()).toBe(true);
- expect(findWarningAlert().exists()).toBe(true);
- });
+ it('shows both error and warning', () => {
+ expect(findErrorAlert().exists()).toBe(true);
+ expect(findWarningAlert().exists()).toBe(true);
+ });
- it('shows the correct error', () => {
- expect(findErrorAlert().text()).toBe(mockError.errors[0]);
- });
+ it('shows the correct error', () => {
+ expect(findErrorAlert().text()).toBe(mockError.errors[0]);
+ });
- it('shows the correct warning title', () => {
- const { length } = mockError.warnings;
+ it('shows the correct warning title', () => {
+ const { length } = mockError.warnings;
- expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`);
+ expect(findWarningAlertSummary().attributes('message')).toBe(`${length} warnings found:`);
+ });
+
+ it('shows the correct amount of warnings', () => {
+ expect(findWarnings()).toHaveLength(mockError.warnings.length);
+ });
+
+ it('re-enables the submit button', () => {
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ });
});
- it('shows the correct amount of warnings', () => {
- expect(findWarnings()).toHaveLength(mockError.warnings.length);
+ describe('when the error response cannot be handled', () => {
+ beforeEach(async () => {
+ mock
+ .onPost(pipelinesPath)
+ .reply(httpStatusCodes.INTERNAL_SERVER_ERROR, 'something went wrong');
+
+ findForm().vm.$emit('submit', dummySubmitEvent);
+
+ await waitForPromises();
+ });
+
+ it('re-enables the submit button', () => {
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ });
});
});
});
diff --git a/spec/frontend/pipelines/blank_state_spec.js b/spec/frontend/pipelines/blank_state_spec.js
index c09d9232569..dae24efd041 100644
--- a/spec/frontend/pipelines/blank_state_spec.js
+++ b/spec/frontend/pipelines/blank_state_spec.js
@@ -1,25 +1,20 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import component from '~/pipelines/components/pipelines_list/blank_state.vue';
+import { mount } from '@vue/test-utils';
+import { getByText } from '@testing-library/dom';
+import BlankState from '~/pipelines/components/pipelines_list/blank_state.vue';
describe('Pipelines Blank State', () => {
- let vm;
- let Component;
-
- beforeEach(() => {
- Component = Vue.extend(component);
-
- vm = mountComponent(Component, {
+ const wrapper = mount(BlankState, {
+ propsData: {
svgPath: 'foo',
message: 'Blank State',
- });
+ },
});
it('should render svg', () => {
- expect(vm.$el.querySelector('.svg-content img').getAttribute('src')).toEqual('foo');
+ expect(wrapper.find('.svg-content img').attributes('src')).toEqual('foo');
});
it('should render message', () => {
- expect(vm.$el.querySelector('h4').textContent.trim()).toEqual('Blank State');
+ expect(getByText(wrapper.element, /Blank State/i)).toBeTruthy();
});
});
diff --git a/spec/frontend/pipelines/graph/graph_component_legacy_spec.js b/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
index 840b1f8baf5..1bde9cae7d2 100644
--- a/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_legacy_spec.js
@@ -6,9 +6,9 @@ import PipelineStore from '~/pipelines/stores/pipeline_store';
import GraphComponentLegacy from '~/pipelines/components/graph/graph_component_legacy.vue';
import StageColumnComponentLegacy from '~/pipelines/components/graph/stage_column_component_legacy.vue';
import LinkedPipelinesColumnLegacy from '~/pipelines/components/graph/linked_pipelines_column_legacy.vue';
+import PipelinesMediator from '~/pipelines/pipeline_details_mediator';
import graphJSON from './mock_data_legacy';
import linkedPipelineJSON from './linked_pipelines_mock_data';
-import PipelinesMediator from '~/pipelines/pipeline_details_mediator';
describe('graph component', () => {
let store;
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index cfc3b7af282..e629396e699 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -22,6 +22,13 @@ describe('graph component', () => {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
};
+ const defaultData = {
+ measurements: {
+ width: 800,
+ height: 800,
+ },
+ };
+
const createComponent = ({
data = {},
mountFn = shallowMount,
@@ -34,7 +41,10 @@ describe('graph component', () => {
...props,
},
data() {
- return { ...data };
+ return {
+ ...defaultData,
+ ...data,
+ };
},
provide: {
dataMethod: GRAPHQL,
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index fb005d628a9..fcae4406950 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -2,8 +2,9 @@ import { mount } from '@vue/test-utils';
import { GlButton, GlLoadingIcon } from '@gitlab/ui';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
-import mockData from './linked_pipelines_mock_data';
import { UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+import mockData from './linked_pipelines_mock_data';
const mockPipeline = mockData.triggered[0];
const validTriggeredPipelineId = mockPipeline.project.id;
@@ -212,11 +213,11 @@ describe('Linked pipeline', () => {
expect(wrapper.emitted().pipelineClicked).toBeTruthy();
});
- it('should emit `bv::hide::tooltip` to close the tooltip', () => {
+ it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, () => {
jest.spyOn(wrapper.vm.$root, '$emit');
findButton().trigger('click');
- expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual(['bv::hide::tooltip']);
+ expect(wrapper.vm.$root.$emit.mock.calls[0]).toEqual([BV_HIDE_TOOLTIP]);
});
it('should emit downstreamHovered with job name on mouseover', () => {
diff --git a/spec/frontend/pipelines/graph/stage_column_component_spec.js b/spec/frontend/pipelines/graph/stage_column_component_spec.js
index 202e25ccda3..16dc70a63a5 100644
--- a/spec/frontend/pipelines/graph/stage_column_component_spec.js
+++ b/spec/frontend/pipelines/graph/stage_column_component_spec.js
@@ -68,6 +68,10 @@ describe('stage column component', () => {
it('should render the provided groups', () => {
expect(findAllStageColumnGroups().length).toBe(mockGroups.length);
});
+
+ it('should emit updateMeasurements event on mount', () => {
+ expect(wrapper.emitted().updateMeasurements).toHaveLength(1);
+ });
});
describe('when job notifies action is complete', () => {
diff --git a/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
new file mode 100644
index 00000000000..cf2b66dea5f
--- /dev/null
+++ b/spec/frontend/pipelines/graph_shared/__snapshots__/links_inner_spec.js.snap
@@ -0,0 +1,23 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Links Inner component with a large number of needs matches snapshot and has expected path 1`] = `
+"<div class=\\"gl-display-flex gl-relative\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
+ <path d=\\"M202,118L42,118C72,118,72,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M202,118L52,118C82,118,82,148,112,148\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M222,138L62,138C92,138,92,158,122,158\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M212,128L72,128C102,128,102,168,132,168\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ <path d=\\"M232,148L82,148C112,148,112,178,142,178\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ </svg> </div>"
+`;
+
+exports[`Links Inner component with a parallel need matches snapshot and has expected path 1`] = `
+"<div class=\\"gl-display-flex gl-relative\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
+ <path d=\\"M192,108L22,108C52,108,52,118,82,118\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ </svg> </div>"
+`;
+
+exports[`Links Inner component with one need matches snapshot and has expected path 1`] = `
+"<div class=\\"gl-display-flex gl-relative\\"><svg id=\\"link-svg\\" viewBox=\\"0,0,1019,445\\" width=\\"1019px\\" height=\\"445px\\" class=\\"gl-absolute gl-pointer-events-none\\">
+ <path d=\\"M202,118L42,118C72,118,72,138,102,138\\" stroke-width=\\"2\\" class=\\"gl-fill-transparent gl-transition-duration-slow gl-transition-timing-function-ease gl-stroke-gray-200\\"></path>
+ </svg> </div>"
+`;
diff --git a/spec/frontend/pipelines/graph_shared/links_inner_spec.js b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
new file mode 100644
index 00000000000..6cabe2bc8a7
--- /dev/null
+++ b/spec/frontend/pipelines/graph_shared/links_inner_spec.js
@@ -0,0 +1,197 @@
+import { shallowMount } from '@vue/test-utils';
+import { setHTMLFixture } from 'helpers/fixtures';
+import LinksInner from '~/pipelines/components/graph_shared/links_inner.vue';
+import { createJobsHash } from '~/pipelines/utils';
+import {
+ jobRect,
+ largePipelineData,
+ parallelNeedData,
+ pipelineData,
+ pipelineDataWithNoNeeds,
+ rootRect,
+} from '../pipeline_graph/mock_data';
+
+describe('Links Inner component', () => {
+ const containerId = 'pipeline-graph-container';
+ const defaultProps = {
+ containerId,
+ containerMeasurements: { width: 1019, height: 445 },
+ pipelineId: 1,
+ pipelineData: [],
+ };
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = shallowMount(LinksInner, {
+ propsData: { ...defaultProps, ...props },
+ });
+ };
+
+ const findLinkSvg = () => wrapper.find('#link-svg');
+ const findAllLinksPath = () => findLinkSvg().findAll('path');
+
+ // We create fixture so that each job has an empty div that represent
+ // the JobPill in the DOM. Each `JobPill` would have different coordinates,
+ // so we increment their coordinates on each iteration to simulat different positions.
+ const setFixtures = ({ stages }) => {
+ const jobs = createJobsHash(stages);
+ const arrayOfJobs = Object.keys(jobs);
+
+ const linksHtmlElements = arrayOfJobs.map((job) => {
+ return `<div id=${job}-${defaultProps.pipelineId} />`;
+ });
+
+ setHTMLFixture(`<div id="${containerId}">${linksHtmlElements.join(' ')}</div>`);
+
+ // We are mocking the clientRect data of each job and the container ID.
+ jest
+ .spyOn(document.getElementById(containerId), 'getBoundingClientRect')
+ .mockImplementation(() => rootRect);
+
+ arrayOfJobs.forEach((job, index) => {
+ jest
+ .spyOn(
+ document.getElementById(`${job}-${defaultProps.pipelineId}`),
+ 'getBoundingClientRect',
+ )
+ .mockImplementation(() => {
+ const newValue = 10 * index;
+ const { left, right, top, bottom, x, y } = jobRect;
+ return {
+ ...jobRect,
+ left: left + newValue,
+ right: right + newValue,
+ top: top + newValue,
+ bottom: bottom + newValue,
+ x: x + newValue,
+ y: y + newValue,
+ };
+ });
+ });
+ };
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('basic SVG creation', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders an SVG of the right size', () => {
+ expect(findLinkSvg().exists()).toBe(true);
+ expect(findLinkSvg().attributes('width')).toBe(
+ `${defaultProps.containerMeasurements.width}px`,
+ );
+ expect(findLinkSvg().attributes('height')).toBe(
+ `${defaultProps.containerMeasurements.height}px`,
+ );
+ });
+ });
+
+ describe('no pipeline data', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the component', () => {
+ expect(findLinkSvg().exists()).toBe(true);
+ expect(findAllLinksPath()).toHaveLength(0);
+ });
+ });
+
+ describe('pipeline data with no needs', () => {
+ beforeEach(() => {
+ createComponent({ pipelineData: pipelineDataWithNoNeeds.stages });
+ });
+
+ it('renders no links', () => {
+ expect(findLinkSvg().exists()).toBe(true);
+ expect(findAllLinksPath()).toHaveLength(0);
+ });
+ });
+
+ describe('with one need', () => {
+ beforeEach(() => {
+ setFixtures(pipelineData);
+ createComponent({ pipelineData: pipelineData.stages });
+ });
+
+ it('renders one link', () => {
+ expect(findAllLinksPath()).toHaveLength(1);
+ });
+
+ it('path does not contain NaN values', () => {
+ expect(wrapper.html()).not.toContain('NaN');
+ });
+
+ it('matches snapshot and has expected path', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('with a parallel need', () => {
+ beforeEach(() => {
+ setFixtures(parallelNeedData);
+ createComponent({ pipelineData: parallelNeedData.stages });
+ });
+
+ it('renders only one link for all the same parallel jobs', () => {
+ expect(findAllLinksPath()).toHaveLength(1);
+ });
+
+ it('path does not contain NaN values', () => {
+ expect(wrapper.html()).not.toContain('NaN');
+ });
+
+ it('matches snapshot and has expected path', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('with a large number of needs', () => {
+ beforeEach(() => {
+ setFixtures(largePipelineData);
+ createComponent({ pipelineData: largePipelineData.stages });
+ });
+
+ it('renders the correct number of links', () => {
+ expect(findAllLinksPath()).toHaveLength(5);
+ });
+
+ it('path does not contain NaN values', () => {
+ expect(wrapper.html()).not.toContain('NaN');
+ });
+
+ it('matches snapshot and has expected path', () => {
+ expect(wrapper.html()).toMatchSnapshot();
+ });
+ });
+
+ describe('interactions', () => {
+ beforeEach(() => {
+ setFixtures(largePipelineData);
+ createComponent({ pipelineData: largePipelineData.stages });
+ });
+
+ it('highlight needs on hover', async () => {
+ const firstLink = findAllLinksPath().at(0);
+
+ const defaultColorClass = 'gl-stroke-gray-200';
+ const hoverColorClass = 'gl-stroke-blue-400';
+
+ expect(firstLink.classes(defaultColorClass)).toBe(true);
+ expect(firstLink.classes(hoverColorClass)).toBe(false);
+
+ // Because there is a watcher, we need to set the props after the component
+ // has mounted.
+ await wrapper.setProps({ highlightedJob: 'test_1' });
+
+ expect(firstLink.classes(defaultColorClass)).toBe(false);
+ expect(firstLink.classes(hoverColorClass)).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/pipelines/shared/links_layer_spec.js b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
index 9ef5233dbce..9ef5233dbce 100644
--- a/spec/frontend/pipelines/shared/links_layer_spec.js
+++ b/spec/frontend/pipelines/graph_shared/links_layer_spec.js
diff --git a/spec/frontend/pipelines/header_component_spec.js b/spec/frontend/pipelines/header_component_spec.js
index 03e385e3cc8..5b4cd7b71fe 100644
--- a/spec/frontend/pipelines/header_component_spec.js
+++ b/spec/frontend/pipelines/header_component_spec.js
@@ -1,15 +1,15 @@
import { shallowMount } from '@vue/test-utils';
import { GlModal, GlLoadingIcon } from '@gitlab/ui';
+import HeaderComponent from '~/pipelines/components/header_component.vue';
+import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
+import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
+import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
import {
mockCancelledPipelineHeader,
mockFailedPipelineHeader,
mockRunningPipelineHeader,
mockSuccessfulPipelineHeader,
} from './mock_data';
-import HeaderComponent from '~/pipelines/components/header_component.vue';
-import deletePipelineMutation from '~/pipelines/graphql/mutations/delete_pipeline.mutation.graphql';
-import retryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
-import cancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
describe('Pipeline details header', () => {
let wrapper;
diff --git a/spec/frontend/pipelines/legacy_header_component_spec.js b/spec/frontend/pipelines/legacy_header_component_spec.js
deleted file mode 100644
index fb7feb8898a..00000000000
--- a/spec/frontend/pipelines/legacy_header_component_spec.js
+++ /dev/null
@@ -1,116 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlModal } from '@gitlab/ui';
-import LegacyHeaderComponent from '~/pipelines/components/legacy_header_component.vue';
-import CiHeader from '~/vue_shared/components/header_ci_component.vue';
-import eventHub from '~/pipelines/event_hub';
-
-describe('Pipeline details header', () => {
- let wrapper;
- let glModalDirective;
-
- const threeWeeksAgo = new Date();
- threeWeeksAgo.setDate(threeWeeksAgo.getDate() - 21);
-
- const findDeleteModal = () => wrapper.find(GlModal);
-
- const defaultProps = {
- pipeline: {
- details: {
- status: {
- group: 'failed',
- icon: 'status_failed',
- label: 'failed',
- text: 'failed',
- details_path: 'path',
- },
- },
- id: 123,
- created_at: threeWeeksAgo.toISOString(),
- user: {
- web_url: 'path',
- name: 'Foo',
- username: 'foobar',
- email: 'foo@bar.com',
- avatar_url: 'link',
- },
- retry_path: 'retry',
- cancel_path: 'cancel',
- delete_path: 'delete',
- },
- isLoading: false,
- };
-
- const createComponent = (props = {}) => {
- glModalDirective = jest.fn();
-
- wrapper = shallowMount(LegacyHeaderComponent, {
- propsData: {
- ...props,
- },
- directives: {
- glModal: {
- bind(el, { value }) {
- glModalDirective(value);
- },
- },
- },
- });
- };
-
- beforeEach(() => {
- jest.spyOn(eventHub, '$emit');
-
- createComponent(defaultProps);
- });
-
- afterEach(() => {
- eventHub.$off();
-
- wrapper.destroy();
- wrapper = null;
- });
-
- it('should render provided pipeline info', () => {
- expect(wrapper.find(CiHeader).props()).toMatchObject({
- status: defaultProps.pipeline.details.status,
- itemId: defaultProps.pipeline.id,
- time: defaultProps.pipeline.created_at,
- user: defaultProps.pipeline.user,
- });
- });
-
- describe('action buttons', () => {
- it('should not trigger eventHub when nothing happens', () => {
- expect(eventHub.$emit).not.toHaveBeenCalled();
- });
-
- it('should call postAction when retry button action is clicked', () => {
- 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('[data-testid="cancelPipeline"]').vm.$emit('click');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('headerPostAction', 'cancel');
- });
-
- it('does not show delete modal', () => {
- expect(findDeleteModal()).not.toBeVisible();
- });
-
- describe('when delete button action is clicked', () => {
- it('displays delete modal', () => {
- expect(findDeleteModal().props('modalId')).toBe(wrapper.vm.$options.DELETE_MODAL_ID);
- expect(glModalDirective).toHaveBeenCalledWith(wrapper.vm.$options.DELETE_MODAL_ID);
- });
-
- it('should call delete when modal is submitted', () => {
- findDeleteModal().vm.$emit('ok');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('headerDeleteAction', 'delete');
- });
- });
- });
-});
diff --git a/spec/frontend/pipelines/pipeline_graph/mock_data.js b/spec/frontend/pipelines/pipeline_graph/mock_data.js
index 7d1a7a79c7f..339aac9f349 100644
--- a/spec/frontend/pipelines/pipeline_graph/mock_data.js
+++ b/spec/frontend/pipelines/pipeline_graph/mock_data.js
@@ -1,5 +1,3 @@
-import { createUniqueLinkId } from '~/pipelines/components/graph_shared/drawing_utils';
-
export const yamlString = `stages:
- empty
- build
@@ -41,10 +39,28 @@ deploy_a:
script: echo hello
`;
-const jobId1 = createUniqueLinkId('build', 'build_1');
-const jobId2 = createUniqueLinkId('test', 'test_1');
-const jobId3 = createUniqueLinkId('test', 'test_2');
-const jobId4 = createUniqueLinkId('deploy', 'deploy_1');
+export const pipelineDataWithNoNeeds = {
+ stages: [
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_1',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ ],
+ },
+ {
+ name: 'test',
+ groups: [
+ {
+ name: 'test_1',
+ jobs: [{ script: 'yarn test', stage: 'test' }],
+ },
+ ],
+ },
+ ],
+};
export const pipelineData = {
stages: [
@@ -54,7 +70,6 @@ export const pipelineData = {
{
name: 'build_1',
jobs: [{ script: 'echo hello', stage: 'build' }],
- id: jobId1,
},
],
},
@@ -64,12 +79,10 @@ export const pipelineData = {
{
name: 'test_1',
jobs: [{ script: 'yarn test', stage: 'test' }],
- id: jobId2,
},
{
name: 'test_2',
jobs: [{ script: 'yarn karma', stage: 'test' }],
- id: jobId3,
},
],
},
@@ -79,7 +92,86 @@ export const pipelineData = {
{
name: 'deploy_1',
jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['test_1'] }],
- id: jobId4,
+ },
+ ],
+ },
+ ],
+};
+
+export const parallelNeedData = {
+ stages: [
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_1',
+ parallel: 3,
+ jobs: [
+ { script: 'echo hello', stage: 'build', name: 'build_1 1/3' },
+ { script: 'echo hello', stage: 'build', name: 'build_1 2/3' },
+ { script: 'echo hello', stage: 'build', name: 'build_1 3/3' },
+ ],
+ },
+ ],
+ },
+ {
+ name: 'test',
+ groups: [
+ {
+ name: 'test_1',
+ jobs: [{ script: 'yarn test', stage: 'test', needs: ['build_1'] }],
+ },
+ ],
+ },
+ ],
+};
+
+export const largePipelineData = {
+ stages: [
+ {
+ name: 'build',
+ groups: [
+ {
+ name: 'build_1',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ {
+ name: 'build_2',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ {
+ name: 'build_3',
+ jobs: [{ script: 'echo hello', stage: 'build' }],
+ },
+ ],
+ },
+ {
+ name: 'test',
+ groups: [
+ {
+ name: 'test_1',
+ jobs: [{ script: 'yarn test', stage: 'test', needs: ['build_2'] }],
+ },
+ {
+ name: 'test_2',
+ jobs: [{ script: 'yarn karma', stage: 'test', needs: ['build_2'] }],
+ },
+ ],
+ },
+ {
+ name: 'deploy',
+ groups: [
+ {
+ name: 'deploy_1',
+ jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['test_1'] }],
+ },
+ {
+ name: 'deploy_2',
+ jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['build_3'] }],
+ },
+ {
+ name: 'deploy_3',
+ jobs: [{ script: 'yarn magick', stage: 'deploy', needs: ['test_2'] }],
},
],
},
@@ -94,9 +186,30 @@ export const singleStageData = {
{
name: 'build_1',
jobs: [{ script: 'echo hello', stage: 'build' }],
- id: jobId1,
},
],
},
],
};
+
+export const rootRect = {
+ bottom: 463,
+ height: 271,
+ left: 236,
+ right: 1252,
+ top: 192,
+ width: 1016,
+ x: 236,
+ y: 192,
+};
+
+export const jobRect = {
+ bottom: 312,
+ height: 24,
+ left: 308,
+ right: 428,
+ top: 288,
+ width: 120,
+ x: 308,
+ y: 288,
+};
diff --git a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
index b6b0a964383..5462045c5c4 100644
--- a/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
+++ b/spec/frontend/pipelines/pipeline_graph/pipeline_graph_spec.js
@@ -1,11 +1,11 @@
import { shallowMount } from '@vue/test-utils';
import { GlAlert } from '@gitlab/ui';
-import { pipelineData, singleStageData } from './mock_data';
import { CI_CONFIG_STATUS_INVALID, CI_CONFIG_STATUS_VALID } from '~/pipeline_editor/constants';
import { DRAW_FAILURE, EMPTY_PIPELINE_DATA, INVALID_CI_CONFIG } from '~/pipelines/constants';
import PipelineGraph from '~/pipelines/components/pipeline_graph/pipeline_graph.vue';
import StagePill from '~/pipelines/components/pipeline_graph/stage_pill.vue';
import JobPill from '~/pipelines/components/pipeline_graph/job_pill.vue';
+import { pipelineData, singleStageData } from './mock_data';
describe('pipeline graph component', () => {
const defaultProps = { pipelineData };
diff --git a/spec/frontend/pipelines/pipeline_url_spec.js b/spec/frontend/pipelines/pipeline_url_spec.js
index 47315bd42e6..5266127ca60 100644
--- a/spec/frontend/pipelines/pipeline_url_spec.js
+++ b/spec/frontend/pipelines/pipeline_url_spec.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
-import { trimText } from 'helpers/text_helper';
import { shallowMount } from '@vue/test-utils';
+import { trimText } from 'helpers/text_helper';
import PipelineUrlComponent from '~/pipelines/components/pipelines_list/pipeline_url.vue';
$.fn.popover = () => {};
@@ -17,6 +17,7 @@ describe('Pipeline Url Component', () => {
const findStuckTag = () => wrapper.find('[data-testid="pipeline-url-stuck"]');
const findDetachedTag = () => wrapper.find('[data-testid="pipeline-url-detached"]');
const findForkTag = () => wrapper.find('[data-testid="pipeline-url-fork"]');
+ const findTrainTag = () => wrapper.find('[data-testid="pipeline-url-train"]');
const defaultProps = {
pipeline: {
@@ -141,6 +142,7 @@ describe('Pipeline Url Component', () => {
expect(findScheduledTag().exists()).toBe(true);
expect(findScheduledTag().text()).toContain('Scheduled');
});
+
it('should render the fork badge when the pipeline was run in a fork', () => {
createComponent({
pipeline: {
@@ -152,4 +154,28 @@ describe('Pipeline Url Component', () => {
expect(findForkTag().exists()).toBe(true);
expect(findForkTag().text()).toBe('fork');
});
+
+ it('should render the train badge when the pipeline is a merge train pipeline', () => {
+ createComponent({
+ pipeline: {
+ flags: {
+ merge_train_pipeline: true,
+ },
+ },
+ });
+
+ expect(findTrainTag().text()).toContain('train');
+ });
+
+ it('should not render the train badge when the pipeline is not a merge train pipeline', () => {
+ createComponent({
+ pipeline: {
+ flags: {
+ merge_train_pipeline: false,
+ },
+ },
+ });
+
+ expect(findTrainTag().exists()).toBe(false);
+ });
});
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
index 69c1b7ce43d..ec01fe7c324 100644
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -1,25 +1,29 @@
-import { shallowMount } from '@vue/test-utils';
+import { shallowMount, mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { TEST_HOST } from 'spec/test_constants';
-import { GlButton } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
+import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import PipelinesActions from '~/pipelines/components/pipelines_list/pipelines_actions.vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
+jest.mock('~/flash');
+
describe('Pipelines Actions dropdown', () => {
let wrapper;
let mock;
- const createComponent = (actions = []) => {
- wrapper = shallowMount(PipelinesActions, {
+ const createComponent = (props, mountFn = shallowMount) => {
+ wrapper = mountFn(PipelinesActions, {
propsData: {
- actions,
+ ...props,
},
});
};
- const findAllDropdownItems = () => wrapper.findAll(GlButton);
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAll(GlDropdownItem);
const findAllCountdowns = () => wrapper.findAll(GlCountdown);
beforeEach(() => {
@@ -47,7 +51,7 @@ describe('Pipelines Actions dropdown', () => {
];
beforeEach(() => {
- createComponent(mockActions);
+ createComponent({ actions: mockActions });
});
it('renders a dropdown with the provided actions', () => {
@@ -59,16 +63,33 @@ describe('Pipelines Actions dropdown', () => {
});
describe('on click', () => {
- it('makes a request and toggles the loading state', () => {
+ beforeEach(() => {
+ createComponent({ actions: mockActions }, mount);
+ });
+
+ it('makes a request and toggles the loading state', async () => {
mock.onPost(mockActions.path).reply(200);
- wrapper.find(GlButton).vm.$emit('click');
+ findAllDropdownItems().at(0).vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+ expect(findDropdown().props('loading')).toBe(true);
+
+ await waitForPromises();
+ expect(findDropdown().props('loading')).toBe(false);
+ });
+
+ it('makes a failed request and toggles the loading state', async () => {
+ mock.onPost(mockActions.path).reply(500);
+
+ findAllDropdownItems().at(0).vm.$emit('click');
- expect(wrapper.vm.isLoading).toBe(true);
+ await wrapper.vm.$nextTick();
+ expect(findDropdown().props('loading')).toBe(true);
- return waitForPromises().then(() => {
- expect(wrapper.vm.isLoading).toBe(false);
- });
+ await waitForPromises();
+ expect(findDropdown().props('loading')).toBe(false);
+ expect(createFlash).toHaveBeenCalledTimes(1);
});
});
});
@@ -89,10 +110,10 @@ describe('Pipelines Actions dropdown', () => {
beforeEach(() => {
jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
- createComponent([scheduledJobAction, expiredJobAction]);
+ createComponent({ actions: [scheduledJobAction, expiredJobAction] });
});
- it('makes post request after confirming', () => {
+ it('makes post request after confirming', async () => {
mock.onPost(scheduledJobAction.path).reply(200);
jest.spyOn(window, 'confirm').mockReturnValue(true);
@@ -100,19 +121,22 @@ describe('Pipelines Actions dropdown', () => {
expect(window.confirm).toHaveBeenCalled();
- return waitForPromises().then(() => {
- expect(mock.history.post.length).toBe(1);
- });
+ await waitForPromises();
+
+ expect(mock.history.post).toHaveLength(1);
});
- it('does not make post request if confirmation is cancelled', () => {
+ it('does not make post request if confirmation is cancelled', async () => {
mock.onPost(scheduledJobAction.path).reply(200);
jest.spyOn(window, 'confirm').mockReturnValue(false);
findAllDropdownItems().at(0).vm.$emit('click');
expect(window.confirm).toHaveBeenCalled();
- expect(mock.history.post.length).toBe(0);
+
+ await waitForPromises();
+
+ expect(mock.history.post).toHaveLength(0);
});
it('displays the remaining time in the dropdown', () => {
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 5d82669b0b8..cbad5b4c1e6 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -1,8 +1,8 @@
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import waitForPromises from 'helpers/wait_for_promises';
import { GlFilteredSearch, GlButton, GlLoadingIcon } from '@gitlab/ui';
+import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import axios from '~/lib/utils/axios_utils';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
@@ -15,9 +15,9 @@ import PipelinesTableComponent from '~/pipelines/components/pipelines_list/pipel
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 { deprecatedCreateFlash as createFlash } from '~/flash';
+import { pipelineWithStages, stageReply, users, mockSearch, branches } from './mock_data';
jest.mock('~/flash');
@@ -66,10 +66,10 @@ describe('Pipelines', () => {
const findRunPipelineButton = () => findByTestId('run-pipeline-button');
const findCiLintButton = () => findByTestId('ci-lint-button');
const findCleanCacheButton = () => findByTestId('clear-cache-button');
+ const findStagesDropdown = () => findByTestId('mini-pipeline-graph-dropdown-toggle');
const findEmptyState = () => wrapper.find(EmptyState);
const findBlankState = () => wrapper.find(BlankState);
- const findStagesDropdown = () => wrapper.find('.js-builds-dropdown-button');
const findTablePagination = () => wrapper.find(TablePagination);
diff --git a/spec/frontend/pipelines/pipelines_table_row_spec.js b/spec/frontend/pipelines/pipelines_table_row_spec.js
index 9cdd24b2ab5..660651547fc 100644
--- a/spec/frontend/pipelines/pipelines_table_row_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_row_spec.js
@@ -155,7 +155,9 @@ describe('Pipelines Table Row', () => {
it('should render an icon for each stage', () => {
expect(
- wrapper.findAll('.table-section:nth-child(4) .js-builds-dropdown-button').length,
+ wrapper.findAll(
+ '.table-section:nth-child(4) [data-testid="mini-pipeline-graph-dropdown-toggle"]',
+ ).length,
).toEqual(pipeline.details.stages.length);
});
});
diff --git a/spec/frontend/pipelines/stage_spec.js b/spec/frontend/pipelines/stage_spec.js
index e4782a1dab1..8c914705d97 100644
--- a/spec/frontend/pipelines/stage_spec.js
+++ b/spec/frontend/pipelines/stage_spec.js
@@ -1,4 +1,6 @@
import 'bootstrap/js/dist/dropdown';
+import $ from 'jquery';
+import { GlDropdown } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
@@ -9,6 +11,7 @@ import { stageReply } from './mock_data';
describe('Pipelines stage component', () => {
let wrapper;
let mock;
+ let glFeatures;
const defaultProps = {
stage: {
@@ -22,8 +25,6 @@ describe('Pipelines stage component', () => {
updateDropdown: false,
};
- const isDropdownOpen = () => wrapper.classes('show');
-
const createComponent = (props = {}) => {
wrapper = mount(StageComponent, {
attachTo: document.body,
@@ -31,110 +32,265 @@ describe('Pipelines stage component', () => {
...defaultProps,
...props,
},
+ provide: {
+ glFeatures,
+ },
});
};
beforeEach(() => {
mock = new MockAdapter(axios);
+ jest.spyOn(eventHub, '$emit');
+ glFeatures = {};
});
afterEach(() => {
wrapper.destroy();
wrapper = null;
+ eventHub.$emit.mockRestore();
mock.restore();
});
- describe('default', () => {
- beforeEach(() => {
- createComponent();
+ describe('when ci_mini_pipeline_gl_dropdown feature flag is disabled', () => {
+ const isDropdownOpen = () => wrapper.classes('show');
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should render a dropdown with the status icon', () => {
+ expect(wrapper.attributes('class')).toEqual('dropdown');
+ expect(wrapper.find('svg').exists()).toBe(true);
+ expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown');
+ });
});
- it('should render a dropdown with the status icon', () => {
- expect(wrapper.attributes('class')).toEqual('dropdown');
- expect(wrapper.find('svg').exists()).toBe(true);
- expect(wrapper.find('button').attributes('data-toggle')).toEqual('dropdown');
+ describe('with successful request', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+ createComponent();
+ });
+
+ it('should render the received data and emit `clickedDropdown` event', async () => {
+ wrapper.find('button').trigger('click');
+
+ await axios.waitForAll();
+ expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
+ stageReply.latest_statuses[0].name,
+ );
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
+ });
});
- });
- describe('with successful request', () => {
- beforeEach(() => {
- mock.onGet('path.json').reply(200, stageReply);
+ it('when request fails should close the dropdown', async () => {
+ mock.onGet('path.json').reply(500);
createComponent();
- });
+ wrapper.find({ ref: 'dropdown' }).trigger('click');
- it('should render the received data and emit `clickedDropdown` event', async () => {
- jest.spyOn(eventHub, '$emit');
- wrapper.find('button').trigger('click');
+ expect(isDropdownOpen()).toBe(true);
+ wrapper.find('button').trigger('click');
await axios.waitForAll();
- expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
- stageReply.latest_statuses[0].name,
- );
- expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
+ expect(isDropdownOpen()).toBe(false);
});
- });
- it('when request fails should close the dropdown', async () => {
- mock.onGet('path.json').reply(500);
- createComponent();
- wrapper.find({ ref: 'dropdown' }).trigger('click');
- expect(isDropdownOpen()).toBe(true);
+ describe('update endpoint correctly', () => {
+ beforeEach(() => {
+ const copyStage = { ...stageReply };
+ copyStage.latest_statuses[0].name = 'this is the updated content';
+ mock.onGet('bar.json').reply(200, copyStage);
+ createComponent({
+ stage: {
+ status: {
+ group: 'running',
+ icon: 'status_running',
+ title: 'running',
+ },
+ dropdown_path: 'bar.json',
+ },
+ });
+ return axios.waitForAll();
+ });
+
+ it('should update the stage to request the new endpoint provided', async () => {
+ wrapper.find('button').trigger('click');
+ await axios.waitForAll();
+
+ expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
+ 'this is the updated content',
+ );
+ });
+ });
+
+ describe('pipelineActionRequestComplete', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ });
+
+ const clickCiAction = async () => {
+ wrapper.find('button').trigger('click');
+ await axios.waitForAll();
+
+ wrapper.find('.js-ci-action').trigger('click');
+ await axios.waitForAll();
+ };
+
+ describe('within pipeline table', () => {
+ it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
+ createComponent({ type: 'PIPELINES_TABLE' });
+
+ await clickCiAction();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ });
+ });
+
+ describe('in MR widget', () => {
+ beforeEach(() => {
+ jest.spyOn($.fn, 'dropdown');
+ });
- wrapper.find('button').trigger('click');
- await axios.waitForAll();
+ it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
+ createComponent();
- expect(isDropdownOpen()).toBe(false);
+ await clickCiAction();
+
+ expect($.fn.dropdown).toHaveBeenCalledWith('toggle');
+ });
+ });
+ });
});
- describe('update endpoint correctly', () => {
+ describe('when ci_mini_pipeline_gl_dropdown feature flag is enabled', () => {
+ const findDropdown = () => wrapper.find(GlDropdown);
+ const findDropdownToggle = () => wrapper.find('button.gl-dropdown-toggle');
+ const findDropdownMenu = () =>
+ wrapper.find('[data-testid="mini-pipeline-graph-dropdown-menu-list"]');
+ const findCiActionBtn = () => wrapper.find('.js-ci-action');
+
+ const openGlDropdown = () => {
+ findDropdownToggle().trigger('click');
+ return new Promise((resolve) => {
+ wrapper.vm.$root.$on('bv::dropdown::show', resolve);
+ });
+ };
+
beforeEach(() => {
- const copyStage = { ...stageReply };
- copyStage.latest_statuses[0].name = 'this is the updated content';
- mock.onGet('bar.json').reply(200, copyStage);
- createComponent({
- stage: {
- status: {
- group: 'running',
- icon: 'status_running',
- title: 'running',
- },
- dropdown_path: 'bar.json',
- },
+ glFeatures = { ciMiniPipelineGlDropdown: true };
+ });
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should render a dropdown with the status icon', () => {
+ expect(findDropdown().exists()).toBe(true);
+ expect(findDropdownToggle().classes('gl-dropdown-toggle')).toEqual(true);
+ expect(wrapper.find('[data-testid="status_success_borderless-icon"]').exists()).toBe(true);
});
- return axios.waitForAll();
});
- it('should update the stage to request the new endpoint provided', async () => {
- wrapper.find('button').trigger('click');
+ describe('with successful request', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+ createComponent();
+ });
+
+ it('should render the received data and emit `clickedDropdown` event', async () => {
+ await openGlDropdown();
+ await axios.waitForAll();
+
+ expect(findDropdownMenu().text()).toContain(stageReply.latest_statuses[0].name);
+ expect(eventHub.$emit).toHaveBeenCalledWith('clickedDropdown');
+ });
+ });
+
+ it('when request fails should close the dropdown', async () => {
+ mock.onGet('path.json').reply(500);
+
+ createComponent();
+
+ await openGlDropdown();
await axios.waitForAll();
- expect(wrapper.find('.js-builds-dropdown-container ul').text()).toContain(
- 'this is the updated content',
- );
+ expect(findDropdown().classes('show')).toBe(false);
});
- });
- describe('pipelineActionRequestComplete', () => {
- beforeEach(() => {
- mock.onGet('path.json').reply(200, stageReply);
- mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ describe('update endpoint correctly', () => {
+ beforeEach(async () => {
+ const copyStage = { ...stageReply };
+ copyStage.latest_statuses[0].name = 'this is the updated content';
+ mock.onGet('bar.json').reply(200, copyStage);
+ createComponent({
+ stage: {
+ status: {
+ group: 'running',
+ icon: 'status_running',
+ title: 'running',
+ },
+ dropdown_path: 'bar.json',
+ },
+ });
+ await axios.waitForAll();
+ });
- createComponent({ type: 'PIPELINES_TABLE' });
+ it('should update the stage to request the new endpoint provided', async () => {
+ await openGlDropdown();
+ await axios.waitForAll();
+
+ expect(findDropdownMenu().text()).toContain('this is the updated content');
+ });
});
- describe('within pipeline table', () => {
- it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
- jest.spyOn(eventHub, '$emit');
+ describe('pipelineActionRequestComplete', () => {
+ beforeEach(() => {
+ mock.onGet('path.json').reply(200, stageReply);
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ });
- wrapper.find('button').trigger('click');
+ const clickCiAction = async () => {
+ await openGlDropdown();
await axios.waitForAll();
- wrapper.find('.js-ci-action').trigger('click');
+ findCiActionBtn().trigger('click');
await axios.waitForAll();
+ };
+
+ describe('within pipeline table', () => {
+ beforeEach(() => {
+ createComponent({ type: 'PIPELINES_TABLE' });
+ });
+
+ it('emits `refreshPipelinesTable` event when `pipelineActionRequestComplete` is triggered', async () => {
+ await clickCiAction();
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ });
+ });
+
+ describe('in MR widget', () => {
+ beforeEach(() => {
+ jest.spyOn($.fn, 'dropdown');
+ createComponent();
+ });
+
+ it('closes the dropdown when `pipelineActionRequestComplete` is triggered', async () => {
+ const hidden = jest.fn();
+
+ wrapper.vm.$root.$on('bv::dropdown::hide', hidden);
+
+ expect(hidden).toHaveBeenCalledTimes(0);
+
+ await clickCiAction();
- expect(eventHub.$emit).toHaveBeenCalledWith('refreshPipelinesTable');
+ expect(hidden).toHaveBeenCalledTimes(1);
+ });
});
});
});
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index f7ff36c0a46..970430b3adf 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -16,7 +16,7 @@ describe('Actions TestReports Store', () => {
const testReports = getJSONFixture('pipelines/test_report.json');
const summary = { total_count: 1 };
- const suiteEndpoint = `${TEST_HOST}/tests/:suite_name.json`;
+ const suiteEndpoint = `${TEST_HOST}/tests/suite.json`;
const summaryEndpoint = `${TEST_HOST}/test_reports/summary.json`;
const defaultState = {
suiteEndpoint,
@@ -69,9 +69,8 @@ describe('Actions TestReports Store', () => {
beforeEach(() => {
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 } })
+ .onGet(suiteEndpoint, { params: { build_ids: buildIds } })
.replyOnce(200, testReports.test_suites[0], {});
});
diff --git a/spec/frontend/pipelines/test_reports/test_case_details_spec.js b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
index bfb8b43778d..42765ca6a32 100644
--- a/spec/frontend/pipelines/test_reports/test_case_details_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_case_details_spec.js
@@ -11,12 +11,17 @@ describe('Test case details', () => {
classname: 'spec.test_spec',
name: 'Test#something cool',
formattedTime: '10.04ms',
+ recent_failures: {
+ count: 2,
+ base_branch: 'master',
+ },
system_output: 'Line 42 is broken',
};
const findModal = () => wrapper.find(GlModal);
const findName = () => wrapper.find('[data-testid="test-case-name"]');
const findDuration = () => wrapper.find('[data-testid="test-case-duration"]');
+ const findRecentFailures = () => wrapper.find('[data-testid="test-case-recent-failures"]');
const findSystemOutput = () => wrapper.find('[data-testid="test-case-trace"]');
const createComponent = (testCase = {}) => {
@@ -56,6 +61,36 @@ describe('Test case details', () => {
});
});
+ describe('when test case has recent failures', () => {
+ describe('has only 1 recent failure', () => {
+ it('renders the recent failure', () => {
+ createComponent({ recent_failures: { ...defaultTestCase.recent_failures, count: 1 } });
+
+ expect(findRecentFailures().text()).toContain(
+ `Failed 1 time in ${defaultTestCase.recent_failures.base_branch} in the last 14 days`,
+ );
+ });
+ });
+
+ describe('has more than 1 recent failure', () => {
+ it('renders the recent failures', () => {
+ createComponent();
+
+ expect(findRecentFailures().text()).toContain(
+ `Failed ${defaultTestCase.recent_failures.count} times in ${defaultTestCase.recent_failures.base_branch} in the last 14 days`,
+ );
+ });
+ });
+ });
+
+ describe('when test case does not have recent failures', () => {
+ it('does not render the recent failures', () => {
+ createComponent({ recent_failures: null });
+
+ expect(findRecentFailures().exists()).toBe(false);
+ });
+ });
+
describe('when test case has system output', () => {
it('renders the test case system output', () => {
createComponent();
diff --git a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
index b8fd056610b..57545aec50f 100644
--- a/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
+++ b/spec/frontend/pipelines/test_reports/test_suite_table_spec.js
@@ -1,7 +1,7 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
-import { getJSONFixture } from 'helpers/fixtures';
import { GlButton, GlFriendlyWrap, GlPagination } from '@gitlab/ui';
+import { getJSONFixture } from 'helpers/fixtures';
import SuiteTable from '~/pipelines/components/test_reports/test_suite_table.vue';
import * as getters from '~/pipelines/stores/test_reports/getters';
import { TestStatus } from '~/pipelines/constants';
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 371ba5a4f9b..7ddbbb3b005 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 { GlFilteredSearchToken, GlFilteredSearchSuggestion, GlLoadingIcon } from '@gitlab/ui';
-import { stubComponent } from 'helpers/stub_component';
import { shallowMount } from '@vue/test-utils';
+import { stubComponent } from 'helpers/stub_component';
import Api from '~/api';
import PipelineTriggerAuthorToken from '~/pipelines/components/pipelines_list/tokens/pipeline_trigger_author_token.vue';
import { users } from '../mock_data';
diff --git a/spec/frontend/profile/account/components/delete_account_modal_spec.js b/spec/frontend/profile/account/components/delete_account_modal_spec.js
index 63e27473979..82d2ab2d18a 100644
--- a/spec/frontend/profile/account/components/delete_account_modal_spec.js
+++ b/spec/frontend/profile/account/components/delete_account_modal_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
-import { TEST_HOST } from 'helpers/test_constants';
import { merge } from 'lodash';
import { mount } from '@vue/test-utils';
+import { TEST_HOST } from 'helpers/test_constants';
import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue';
const GlModalStub = {
diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
index 9fa7d658405..6f816006ae3 100644
--- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import Vuex from 'vuex';
import { shallowMount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
Vue.use(Vuex);
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index 1c37b82fed3..e197ca8eace 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -1,13 +1,14 @@
import MockAdapter from 'axios-mock-adapter';
import { shallowMount, mount, createWrapper } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { GlModal, GlForm, GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import { within } from '@testing-library/dom';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
import eventHub from '~/projects/commit/event_hub';
import CommitFormModal from '~/projects/commit/components/form_modal.vue';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
import createStore from '~/projects/commit/store';
+import { BV_SHOW_MODAL } from '~/lib/utils/constants';
import mockData from '../mock_data';
describe('CommitFormModal', () => {
@@ -64,7 +65,7 @@ describe('CommitFormModal', () => {
wrapper.vm.show();
- expect(rootEmit).toHaveBeenCalledWith('bv::show::modal', mockData.modalPropsData.modalId);
+ expect(rootEmit).toHaveBeenCalledWith(BV_SHOW_MODAL, mockData.modalPropsData.modalId);
});
it('Clears the modal state once modal is hidden', () => {
diff --git a/spec/frontend/projects/commit/store/actions_spec.js b/spec/frontend/projects/commit/store/actions_spec.js
index ec528d4ee88..e10e8fab440 100644
--- a/spec/frontend/projects/commit/store/actions_spec.js
+++ b/spec/frontend/projects/commit/store/actions_spec.js
@@ -5,8 +5,8 @@ import createFlash from '~/flash';
import getInitialState from '~/projects/commit/store/state';
import * as actions from '~/projects/commit/store/actions';
import * as types from '~/projects/commit/store/mutation_types';
-import mockData from '../mock_data';
import { PROJECT_BRANCHES_ERROR } from '~/projects/commit/constants';
+import mockData from '../mock_data';
jest.mock('~/flash.js');
diff --git a/spec/frontend/projects/commit_box/info/load_branches_spec.js b/spec/frontend/projects/commit_box/info/load_branches_spec.js
index ebd4ee45dab..8100200cbdd 100644
--- a/spec/frontend/projects/commit_box/info/load_branches_spec.js
+++ b/spec/frontend/projects/commit_box/info/load_branches_spec.js
@@ -1,6 +1,6 @@
import axios from 'axios';
-import waitForPromises from 'helpers/wait_for_promises';
import MockAdapter from 'axios-mock-adapter';
+import waitForPromises from 'helpers/wait_for_promises';
import { loadBranches } from '~/projects/commit_box/info/load_branches';
const mockCommitPath = '/commit/abcd/branches';
diff --git a/spec/frontend/projects/compare/components/app_spec.js b/spec/frontend/projects/compare/components/app_spec.js
new file mode 100644
index 00000000000..e265055ef1b
--- /dev/null
+++ b/spec/frontend/projects/compare/components/app_spec.js
@@ -0,0 +1,116 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import CompareApp from '~/projects/compare/components/app.vue';
+import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+
+const projectCompareIndexPath = 'some/path';
+const refsProjectPath = 'some/refs/path';
+const paramsFrom = 'master';
+const paramsTo = 'master';
+
+describe('CompareApp component', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(CompareApp, {
+ propsData: {
+ projectCompareIndexPath,
+ refsProjectPath,
+ paramsFrom,
+ paramsTo,
+ projectMergeRequestPath: '',
+ createMrPath: '',
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders component with prop', () => {
+ expect(wrapper.props()).toEqual(
+ expect.objectContaining({
+ projectCompareIndexPath,
+ refsProjectPath,
+ paramsFrom,
+ paramsTo,
+ }),
+ );
+ });
+
+ it('contains the correct form attributes', () => {
+ expect(wrapper.attributes('action')).toBe(projectCompareIndexPath);
+ expect(wrapper.attributes('method')).toBe('POST');
+ });
+
+ it('has input with csrf token', () => {
+ expect(wrapper.find('input[name="authenticity_token"]').attributes('value')).toBe(
+ 'mock-csrf-token',
+ );
+ });
+
+ it('has ellipsis', () => {
+ expect(wrapper.find('[data-testid="ellipsis"]').exists()).toBe(true);
+ });
+
+ it('render Source and Target BranchDropdown components', () => {
+ const branchDropdowns = wrapper.findAll(RevisionDropdown);
+
+ expect(branchDropdowns.length).toBe(2);
+ expect(branchDropdowns.at(0).props('revisionText')).toBe('Source');
+ expect(branchDropdowns.at(1).props('revisionText')).toBe('Target');
+ });
+
+ describe('compare button', () => {
+ const findCompareButton = () => wrapper.find(GlButton);
+
+ it('renders button', () => {
+ expect(findCompareButton().exists()).toBe(true);
+ });
+
+ it('submits form', () => {
+ findCompareButton().vm.$emit('click');
+ expect(wrapper.find('form').element.submit).toHaveBeenCalled();
+ });
+
+ it('has compare text', () => {
+ expect(findCompareButton().text()).toBe('Compare');
+ });
+ });
+
+ describe('merge request buttons', () => {
+ const findProjectMrButton = () => wrapper.find('[data-testid="projectMrButton"]');
+ const findCreateMrButton = () => wrapper.find('[data-testid="createMrButton"]');
+
+ it('does not have merge request buttons', () => {
+ createComponent();
+ expect(findProjectMrButton().exists()).toBe(false);
+ expect(findCreateMrButton().exists()).toBe(false);
+ });
+
+ it('has "View open merge request" button', () => {
+ createComponent({
+ projectMergeRequestPath: 'some/project/merge/request/path',
+ });
+ expect(findProjectMrButton().exists()).toBe(true);
+ expect(findCreateMrButton().exists()).toBe(false);
+ });
+
+ it('has "Create merge request" button', () => {
+ createComponent({
+ createMrPath: 'some/create/create/mr/path',
+ });
+ expect(findProjectMrButton().exists()).toBe(false);
+ expect(findCreateMrButton().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
new file mode 100644
index 00000000000..c9e87fb904d
--- /dev/null
+++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
@@ -0,0 +1,92 @@
+import { shallowMount } from '@vue/test-utils';
+import AxiosMockAdapter from 'axios-mock-adapter';
+import { GlDropdown } from '@gitlab/ui';
+import axios from '~/lib/utils/axios_utils';
+import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue';
+import createFlash from '~/flash';
+
+const defaultProps = {
+ refsProjectPath: 'some/refs/path',
+ revisionText: 'Target',
+ paramsName: 'from',
+ paramsBranch: 'master',
+};
+
+jest.mock('~/flash');
+
+describe('RevisionDropdown component', () => {
+ let wrapper;
+ let axiosMock;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(RevisionDropdown, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ axiosMock.restore();
+ });
+
+ const findGlDropdown = () => wrapper.find(GlDropdown);
+
+ it('sets hidden input', () => {
+ createComponent();
+ expect(wrapper.find('input[type="hidden"]').attributes('value')).toBe(
+ defaultProps.paramsBranch,
+ );
+ });
+
+ it('update the branches on success', async () => {
+ const Branches = ['branch-1', 'branch-2'];
+ const Tags = ['tag-1', 'tag-2', 'tag-3'];
+
+ axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(200, {
+ Branches,
+ Tags,
+ });
+
+ createComponent();
+
+ await axios.waitForAll();
+
+ expect(wrapper.vm.branches).toEqual(Branches);
+ expect(wrapper.vm.tags).toEqual(Tags);
+ });
+
+ it('shows flash message on error', async () => {
+ axiosMock.onGet('some/invalid/path').replyOnce(404);
+
+ createComponent();
+
+ await wrapper.vm.fetchBranchesAndTags();
+ expect(createFlash).toHaveBeenCalled();
+ });
+
+ describe('GlDropdown component', () => {
+ it('renders props', () => {
+ createComponent();
+ expect(wrapper.props()).toEqual(expect.objectContaining(defaultProps));
+ });
+
+ it('display default text', () => {
+ createComponent({
+ paramsBranch: null,
+ });
+ expect(findGlDropdown().props('text')).toBe('Select branch/tag');
+ });
+
+ it('display params branch text', () => {
+ createComponent();
+ expect(findGlDropdown().props('text')).toBe(defaultProps.paramsBranch);
+ });
+ });
+});
diff --git a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
index 0b9f095a700..f0d72124379 100644
--- a/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
+++ b/spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap
@@ -53,12 +53,12 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
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 repositories and %{strongStart}all related resources%{strongEnd} including issues, merge requests etc."
+ message="Once a project is permanently deleted, it %{strongStart}cannot be recovered%{strongEnd}. Permanently deleting this project will %{strongStart}immediately delete%{strongEnd} its repositories and %{strongStart}all related resources%{strongEnd}, including issues, merge requests etc."
/>
</gl-alert-stub>
<p>
- This action cannot be undone. You will lose this project's repository and all content: issues, merge requests, etc.
+ This action cannot be undone. You will lose this project's repository and all related resources, including issues, merge requests, etc.
</p>
<p
diff --git a/spec/frontend/projects/members/utils_spec.js b/spec/frontend/projects/members/utils_spec.js
new file mode 100644
index 00000000000..813e8455e85
--- /dev/null
+++ b/spec/frontend/projects/members/utils_spec.js
@@ -0,0 +1,14 @@
+import { projectMemberRequestFormatter } from '~/projects/members/utils';
+
+describe('project member utils', () => {
+ describe('projectMemberRequestFormatter', () => {
+ it('returns expected format', () => {
+ expect(
+ projectMemberRequestFormatter({
+ accessLevel: 50,
+ expires_at: '2020-10-16',
+ }),
+ ).toEqual({ project_member: { access_level: 50, expires_at: '2020-10-16' } });
+ });
+ });
+});
diff --git a/spec/frontend/projects/pipelines/charts/components/app_spec.js b/spec/frontend/projects/pipelines/charts/components/app_spec.js
index 44329944097..6dcfacb46cb 100644
--- a/spec/frontend/projects/pipelines/charts/components/app_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/app_spec.js
@@ -1,32 +1,19 @@
import { merge } from 'lodash';
-import { createLocalVue, shallowMount } from '@vue/test-utils';
-import VueApollo from 'vue-apollo';
+import { shallowMount } from '@vue/test-utils';
import { GlTabs, GlTab } from '@gitlab/ui';
-import createMockApollo from 'helpers/mock_apollo_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+import { mergeUrlParams, updateHistory, getParameterValues } from '~/lib/utils/url_utility';
import Component from '~/projects/pipelines/charts/components/app.vue';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
-import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql';
-import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql';
-import { mockPipelineCount, mockPipelineStatistics } from '../mock_data';
-const projectPath = 'gitlab-org/gitlab';
-const localVue = createLocalVue();
-localVue.use(VueApollo);
+jest.mock('~/lib/utils/url_utility');
const DeploymentFrequencyChartsStub = { name: 'DeploymentFrequencyCharts', render: () => {} };
describe('ProjectsPipelinesChartsApp', () => {
let wrapper;
- function createMockApolloProvider() {
- const requestHandlers = [
- [getPipelineCountByStatus, jest.fn().mockResolvedValue(mockPipelineCount)],
- [getProjectPipelineStatistics, jest.fn().mockResolvedValue(mockPipelineStatistics)],
- ];
-
- return createMockApollo(requestHandlers);
- }
-
function createComponent(mountOptions = {}) {
wrapper = shallowMount(
Component,
@@ -34,11 +21,8 @@ describe('ProjectsPipelinesChartsApp', () => {
{},
{
provide: {
- projectPath,
shouldRenderDeploymentFrequencyCharts: false,
},
- localVue,
- apolloProvider: createMockApolloProvider(),
stubs: {
DeploymentFrequencyCharts: DeploymentFrequencyChartsStub,
},
@@ -57,52 +41,15 @@ describe('ProjectsPipelinesChartsApp', () => {
wrapper = null;
});
- describe('pipelines charts', () => {
- it('displays the pipeline charts', () => {
- const chart = wrapper.find(PipelineCharts);
- const analytics = mockPipelineStatistics.data.project.pipelineAnalytics;
-
- const {
- totalPipelines: total,
- successfulPipelines: success,
- failedPipelines: failed,
- } = mockPipelineCount.data.project;
-
- expect(chart.exists()).toBe(true);
- expect(chart.props()).toMatchObject({
- counts: {
- failed: failed.count,
- success: success.count,
- total: total.count,
- successRatio: (success.count / (success.count + failed.count)) * 100,
- },
- lastWeek: {
- labels: analytics.weekPipelinesLabels,
- totals: analytics.weekPipelinesTotals,
- success: analytics.weekPipelinesSuccessful,
- },
- lastMonth: {
- labels: analytics.monthPipelinesLabels,
- totals: analytics.monthPipelinesTotals,
- success: analytics.monthPipelinesSuccessful,
- },
- lastYear: {
- labels: analytics.yearPipelinesLabels,
- totals: analytics.yearPipelinesTotals,
- success: analytics.yearPipelinesSuccessful,
- },
- timesChart: {
- labels: analytics.pipelineTimesLabels,
- values: analytics.pipelineTimesValues,
- },
- });
- });
- });
-
- const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub);
const findGlTabs = () => wrapper.find(GlTabs);
const findAllGlTab = () => wrapper.findAll(GlTab);
const findGlTabAt = (i) => findAllGlTab().at(i);
+ const findDeploymentFrequencyCharts = () => wrapper.find(DeploymentFrequencyChartsStub);
+ const findPipelineCharts = () => wrapper.find(PipelineCharts);
+
+ it('renders the pipeline charts', () => {
+ expect(findPipelineCharts().exists()).toBe(true);
+ });
describe('when shouldRenderDeploymentFrequencyCharts is true', () => {
beforeEach(() => {
@@ -115,6 +62,49 @@ describe('ProjectsPipelinesChartsApp', () => {
expect(findGlTabAt(1).attributes('title')).toBe('Deployments');
expect(findDeploymentFrequencyCharts().exists()).toBe(true);
});
+
+ it('sets the tab and url when a tab is clicked', async () => {
+ let chartsPath;
+ setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts`);
+
+ mergeUrlParams.mockImplementation(({ chart }, path) => {
+ expect(chart).toBe('deployments');
+ expect(path).toBe(window.location.pathname);
+ chartsPath = `${path}?chart=${chart}`;
+ return chartsPath;
+ });
+
+ updateHistory.mockImplementation(({ url }) => {
+ expect(url).toBe(chartsPath);
+ });
+ const tabs = findGlTabs();
+
+ expect(tabs.attributes('value')).toBe('0');
+
+ tabs.vm.$emit('input', 1);
+
+ await wrapper.vm.$nextTick();
+
+ expect(tabs.attributes('value')).toBe('1');
+ });
+ });
+
+ describe('when provided with a query param', () => {
+ it.each`
+ chart | tab
+ ${'deployments'} | ${'1'}
+ ${'pipelines'} | ${'0'}
+ ${'fake'} | ${'0'}
+ ${''} | ${'0'}
+ `('shows the correct tab for URL parameter "$chart"', ({ chart, tab }) => {
+ setWindowLocation(`${TEST_HOST}/gitlab-org/gitlab-test/-/pipelines/charts?chart=${chart}`);
+ getParameterValues.mockImplementation((name) => {
+ expect(name).toBe('chart');
+ return chart ? [chart] : [];
+ });
+ createComponent({ provide: { shouldRenderDeploymentFrequencyCharts: true } });
+ expect(findGlTabs().attributes('value')).toBe(tab);
+ });
});
describe('when shouldRenderDeploymentFrequencyCharts is false', () => {
diff --git a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
index 598055d5828..3e588d46a4f 100644
--- a/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/pipeline_charts_spec.js
@@ -1,35 +1,37 @@
-import { shallowMount } from '@vue/test-utils';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
import { GlColumnChart } from '@gitlab/ui/dist/charts';
+import createMockApollo from 'helpers/mock_apollo_helper';
import StatisticsList from '~/projects/pipelines/charts/components/statistics_list.vue';
import CiCdAnalyticsAreaChart from '~/projects/pipelines/charts/components/ci_cd_analytics_area_chart.vue';
import PipelineCharts from '~/projects/pipelines/charts/components/pipeline_charts.vue';
-import {
- counts,
- timesChartData as timesChart,
- areaChartData as lastWeek,
- areaChartData as lastMonth,
- lastYearChartData as lastYear,
-} from '../mock_data';
+import getPipelineCountByStatus from '~/projects/pipelines/charts/graphql/queries/get_pipeline_count_by_status.query.graphql';
+import getProjectPipelineStatistics from '~/projects/pipelines/charts/graphql/queries/get_project_pipeline_statistics.query.graphql';
+import { mockPipelineCount, mockPipelineStatistics } from '../mock_data';
-describe('ProjectsPipelinesChartsApp', () => {
+const projectPath = 'gitlab-org/gitlab';
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('~/projects/pipelines/charts/components/pipeline_charts.vue', () => {
let wrapper;
+ function createMockApolloProvider() {
+ const requestHandlers = [
+ [getPipelineCountByStatus, jest.fn().mockResolvedValue(mockPipelineCount)],
+ [getProjectPipelineStatistics, jest.fn().mockResolvedValue(mockPipelineStatistics)],
+ ];
+
+ return createMockApollo(requestHandlers);
+ }
+
beforeEach(() => {
wrapper = shallowMount(PipelineCharts, {
- propsData: {
- counts,
- timesChart,
- lastWeek,
- lastMonth,
- lastYear,
- },
provide: {
- projectPath: 'test/project',
- shouldRenderDeploymentFrequencyCharts: true,
- },
- stubs: {
- DeploymentFrequencyCharts: true,
+ projectPath,
},
+ localVue,
+ apolloProvider: createMockApolloProvider(),
});
});
@@ -43,7 +45,12 @@ describe('ProjectsPipelinesChartsApp', () => {
const list = wrapper.find(StatisticsList);
expect(list.exists()).toBe(true);
- expect(list.props('counts')).toBe(counts);
+ expect(list.props('counts')).toEqual({
+ total: 34,
+ success: 23,
+ failed: 1,
+ successRatio: (23 / (23 + 1)) * 100,
+ });
});
it('displays the commit duration chart', () => {
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
index c83b1852147..f9fbb1b3016 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
@@ -1,20 +1,36 @@
-import { mount } from '@vue/test-utils';
+import { GlAlert } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
-import ServiceDeskRoot from '~/projects/settings_service_desk/components/service_desk_root.vue';
-import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
import axios from '~/lib/utils/axios_utils';
import httpStatusCodes from '~/lib/utils/http_status';
+import ServiceDeskRoot from '~/projects/settings_service_desk/components/service_desk_root.vue';
+import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
describe('ServiceDeskRoot', () => {
- const endpoint = '/gitlab-org/gitlab-test/service_desk';
- const initialIncomingEmail = 'servicedeskaddress@example.com';
let axiosMock;
let wrapper;
let spy;
+ const provideData = {
+ customEmail: 'custom.email@example.com',
+ customEmailEnabled: true,
+ endpoint: '/gitlab-org/gitlab-test/service_desk',
+ initialIncomingEmail: 'servicedeskaddress@example.com',
+ initialIsEnabled: true,
+ outgoingName: 'GitLab Support Bot',
+ projectKey: 'key',
+ selectedTemplate: 'Bug',
+ templates: ['Bug', 'Documentation'],
+ };
+
+ const getAlertText = () => wrapper.find(GlAlert).text();
+
+ const createComponent = () => shallowMount(ServiceDeskRoot, { provide: provideData });
+
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
+ spy = jest.spyOn(axios, 'put');
});
afterEach(() => {
@@ -25,156 +41,122 @@ describe('ServiceDeskRoot', () => {
}
});
- it('sends a request to toggle service desk off when the toggle is clicked from the on state', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
+ describe('ServiceDeskSetting component', () => {
+ it('is rendered', () => {
+ wrapper = createComponent();
+
+ expect(wrapper.find(ServiceDeskSetting).props()).toEqual({
+ customEmail: provideData.customEmail,
+ customEmailEnabled: provideData.customEmailEnabled,
+ incomingEmail: provideData.initialIncomingEmail,
+ initialOutgoingName: provideData.outgoingName,
+ initialProjectKey: provideData.projectKey,
+ initialSelectedTemplate: provideData.selectedTemplate,
+ isEnabled: provideData.initialIsEnabled,
+ isTemplateSaving: false,
+ templates: provideData.templates,
+ });
+ });
+
+ describe('toggle event', () => {
+ describe('when toggling service desk on', () => {
+ beforeEach(async () => {
+ wrapper = createComponent();
- spy = jest.spyOn(axios, 'put');
+ wrapper.find(ServiceDeskSetting).vm.$emit('toggle', true);
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: true,
- initialIncomingEmail,
- endpoint,
- },
- });
+ await waitForPromises();
+ });
+
+ it('sends a request to turn service desk on', () => {
+ axiosMock.onPut(provideData.endpoint).replyOnce(httpStatusCodes.OK);
- wrapper.find('button.gl-toggle').trigger('click');
+ expect(spy).toHaveBeenCalledWith(provideData.endpoint, { service_desk_enabled: true });
+ });
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(spy).toHaveBeenCalledWith(endpoint, { service_desk_enabled: false });
+ it('shows a message when there is an error', () => {
+ axiosMock.onPut(provideData.endpoint).networkError();
+
+ expect(getAlertText()).toContain('An error occurred while enabling Service Desk.');
+ });
});
- });
- it('sends a request to toggle service desk on when the toggle is clicked from the off state', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
+ describe('when toggling service desk off', () => {
+ beforeEach(async () => {
+ wrapper = createComponent();
- spy = jest.spyOn(axios, 'put');
+ wrapper.find(ServiceDeskSetting).vm.$emit('toggle', false);
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: false,
- initialIncomingEmail: '',
- endpoint,
- },
- });
+ await waitForPromises();
+ });
- wrapper.find('button.gl-toggle').trigger('click');
+ it('sends a request to turn service desk off', () => {
+ axiosMock.onPut(provideData.endpoint).replyOnce(httpStatusCodes.OK);
- return wrapper.vm.$nextTick(() => {
- expect(spy).toHaveBeenCalledWith(endpoint, { service_desk_enabled: true });
- });
- });
+ expect(spy).toHaveBeenCalledWith(provideData.endpoint, { service_desk_enabled: false });
+ });
- it('shows an error message when there is an issue toggling service desk on', () => {
- axiosMock.onPut(endpoint).networkError();
+ it('shows a message when there is an error', () => {
+ axiosMock.onPut(provideData.endpoint).networkError();
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: false,
- initialIncomingEmail: '',
- endpoint,
- },
+ expect(getAlertText()).toContain('An error occurred while disabling Service Desk.');
+ });
+ });
});
- wrapper.find('button.gl-toggle').trigger('click');
+ describe('save event', () => {
+ describe('successful request', () => {
+ beforeEach(async () => {
+ axiosMock.onPut(provideData.endpoint).replyOnce(httpStatusCodes.OK);
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(wrapper.html()).toContain('An error occurred while enabling Service Desk.');
- });
- });
+ wrapper = createComponent();
- it('sends a request to update template when the "Save template" button is clicked', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
+ const payload = {
+ selectedTemplate: 'Bug',
+ outgoingName: 'GitLab Support Bot',
+ projectKey: 'key',
+ };
- spy = jest.spyOn(axios, 'put');
+ wrapper.find(ServiceDeskSetting).vm.$emit('save', payload);
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: true,
- endpoint,
- initialIncomingEmail,
- selectedTemplate: 'Bug',
- outgoingName: 'GitLab Support Bot',
- templates: ['Bug', 'Documentation'],
- projectKey: 'key',
- },
- });
+ await waitForPromises();
+ });
- wrapper.find('button.btn-success').trigger('click');
+ it('sends a request to update template', async () => {
+ expect(spy).toHaveBeenCalledWith(provideData.endpoint, {
+ issue_template_key: 'Bug',
+ outgoing_name: 'GitLab Support Bot',
+ project_key: 'key',
+ service_desk_enabled: true,
+ });
+ });
- return wrapper.vm.$nextTick(() => {
- expect(spy).toHaveBeenCalledWith(endpoint, {
- issue_template_key: 'Bug',
- outgoing_name: 'GitLab Support Bot',
- project_key: 'key',
- service_desk_enabled: true,
+ it('shows success message', () => {
+ expect(getAlertText()).toContain('Changes saved.');
+ });
});
- });
- });
- it('saves the template when the "Save template" button is clicked', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK);
-
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: true,
- endpoint,
- initialIncomingEmail,
- selectedTemplate: 'Bug',
- templates: ['Bug', 'Documentation'],
- },
- });
+ describe('unsuccessful request', () => {
+ beforeEach(async () => {
+ axiosMock.onPut(provideData.endpoint).networkError();
- wrapper.find('button.btn-success').trigger('click');
+ wrapper = createComponent();
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(wrapper.html()).toContain('Changes saved.');
- });
- });
+ const payload = {
+ selectedTemplate: 'Bug',
+ outgoingName: 'GitLab Support Bot',
+ projectKey: 'key',
+ };
- it('shows an error message when there is an issue saving the template', () => {
- axiosMock.onPut(endpoint).networkError();
-
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: true,
- endpoint,
- initialIncomingEmail,
- selectedTemplate: 'Bug',
- templates: ['Bug', 'Documentation'],
- },
- });
+ wrapper.find(ServiceDeskSetting).vm.$emit('save', payload);
- wrapper.find('button.btn-success').trigger('click');
+ await waitForPromises();
+ });
- return wrapper.vm
- .$nextTick()
- .then(waitForPromises)
- .then(() => {
- expect(wrapper.html()).toContain('An error occured while saving changes:');
+ it('shows an error message', () => {
+ expect(getAlertText()).toContain('An error occured while saving changes:');
+ });
});
- });
-
- it('passes customEmail through updatedCustomEmail correctly', () => {
- const customEmail = 'foo';
-
- wrapper = mount(ServiceDeskRoot, {
- propsData: {
- initialIsEnabled: true,
- endpoint,
- customEmail,
- },
});
-
- expect(wrapper.find(ServiceDeskSetting).props('customEmail')).toEqual(customEmail);
});
});
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
index ddd9a7b2fad..51d656e0ea4 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_setting_spec.js
@@ -1,63 +1,68 @@
import { shallowMount, mount } from '@vue/test-utils';
-import { GlLoadingIcon } from '@gitlab/ui';
-import eventHub from '~/projects/settings_service_desk/event_hub';
+import { GlButton, GlFormSelect, GlLoadingIcon, GlToggle } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
describe('ServiceDeskSetting', () => {
let wrapper;
+ const findButton = () => wrapper.find(GlButton);
+ const findClipboardButton = () => wrapper.find(ClipboardButton);
+ const findIncomingEmail = () => wrapper.findByTestId('incoming-email');
+ const findIncomingEmailLabel = () => wrapper.findByTestId('incoming-email-describer');
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findTemplateDropdown = () => wrapper.find(GlFormSelect);
+ const findToggle = () => wrapper.find(GlToggle);
+
+ const createComponent = ({ props = {}, mountFunction = shallowMount } = {}) =>
+ extendedWrapper(
+ mountFunction(ServiceDeskSetting, {
+ propsData: {
+ isEnabled: true,
+ ...props,
+ },
+ }),
+ );
+
afterEach(() => {
if (wrapper) {
wrapper.destroy();
}
});
- const findTemplateDropdown = () => wrapper.find('#service-desk-template-select');
- const findIncomingEmail = () => wrapper.find('[data-testid="incoming-email"]');
-
describe('when isEnabled=true', () => {
describe('only isEnabled', () => {
describe('as project admin', () => {
beforeEach(() => {
- wrapper = shallowMount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- },
- });
+ wrapper = createComponent();
});
it('should see activation checkbox', () => {
- expect(wrapper.find('#service-desk-checkbox').exists()).toBe(true);
+ expect(findToggle().exists()).toBe(true);
});
it('should see main panel with the email info', () => {
- expect(wrapper.find('#incoming-email-describer').exists()).toBe(true);
+ expect(findIncomingEmailLabel().exists()).toBe(true);
});
it('should see loading spinner and not the incoming email', () => {
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
expect(findIncomingEmail().exists()).toBe(false);
});
});
});
describe('service desk toggle', () => {
- it('emits an event to turn on Service Desk when clicked', () => {
- const eventSpy = jest.fn();
- eventHub.$on('serviceDeskEnabledCheckboxToggled', eventSpy);
-
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: false,
- },
- });
+ it('emits an event to turn on Service Desk when clicked', async () => {
+ wrapper = createComponent();
- wrapper.find('#service-desk-checkbox').trigger('click');
+ findToggle().vm.$emit('change', true);
- expect(eventSpy).toHaveBeenCalledWith(true);
+ await nextTick();
- eventHub.$off('serviceDeskEnabledCheckboxToggled', eventSpy);
- eventSpy.mockRestore();
+ expect(wrapper.emitted('toggle')[0]).toEqual([true]);
});
});
@@ -65,23 +70,23 @@ describe('ServiceDeskSetting', () => {
const incomingEmail = 'foo@bar.com';
beforeEach(() => {
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- incomingEmail,
- },
+ wrapper = createComponent({
+ props: { incomingEmail },
});
});
it('should see email and not the loading spinner', () => {
expect(findIncomingEmail().element.value).toEqual(incomingEmail);
- expect(wrapper.find(GlLoadingIcon).exists()).toBe(false);
+ expect(findLoadingIcon().exists()).toBe(false);
});
it('renders a copy to clipboard button', () => {
- expect(wrapper.find('.qa-clipboard-button').exists()).toBe(true);
- expect(wrapper.find('.qa-clipboard-button').element.dataset.clipboardText).toBe(
- incomingEmail,
+ expect(findClipboardButton().exists()).toBe(true);
+ expect(findClipboardButton().props()).toEqual(
+ expect.objectContaining({
+ title: 'Copy',
+ text: incomingEmail,
+ }),
);
});
});
@@ -92,12 +97,8 @@ describe('ServiceDeskSetting', () => {
const customEmail = 'custom@bar.com';
beforeEach(() => {
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- incomingEmail,
- customEmail,
- },
+ wrapper = createComponent({
+ props: { incomingEmail, customEmail },
});
});
@@ -110,12 +111,8 @@ describe('ServiceDeskSetting', () => {
const email = 'foo@bar.com';
beforeEach(() => {
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- incomingEmail: email,
- customEmail: email,
- },
+ wrapper = createComponent({
+ props: { incomingEmail: email, customEmail: email },
});
});
@@ -127,21 +124,13 @@ describe('ServiceDeskSetting', () => {
describe('templates dropdown', () => {
it('renders a dropdown to choose a template', () => {
- wrapper = shallowMount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- },
- });
+ wrapper = createComponent();
- expect(wrapper.find('#service-desk-template-select').exists()).toBe(true);
+ expect(findTemplateDropdown().exists()).toBe(true);
});
it('renders a dropdown with a default value of ""', () => {
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- },
- });
+ wrapper = createComponent({ mountFunction: mount });
expect(findTemplateDropdown().element.value).toEqual('');
});
@@ -149,23 +138,18 @@ describe('ServiceDeskSetting', () => {
it('renders a dropdown with a value of "Bug" when it is the initial value', () => {
const templates = ['Bug', 'Documentation', 'Security release'];
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- initialSelectedTemplate: 'Bug',
- templates,
- },
+ wrapper = createComponent({
+ props: { initialSelectedTemplate: 'Bug', templates },
+ mountFunction: mount,
});
expect(findTemplateDropdown().element.value).toEqual('Bug');
});
it('renders a dropdown with no options when the project has no templates', () => {
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- templates: [],
- },
+ wrapper = createComponent({
+ props: { templates: [] },
+ mountFunction: mount,
});
// The dropdown by default has one empty option
@@ -174,11 +158,10 @@ describe('ServiceDeskSetting', () => {
it('renders a dropdown with options when the project has templates', () => {
const templates = ['Bug', 'Documentation', 'Security release'];
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- templates,
- },
+
+ wrapper = createComponent({
+ props: { templates },
+ mountFunction: mount,
});
// An empty-named template is prepended so the user can select no template
@@ -199,78 +182,59 @@ describe('ServiceDeskSetting', () => {
describe('save button', () => {
it('renders a save button to save a template', () => {
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- },
- });
+ wrapper = createComponent();
- expect(wrapper.find('button.btn-success').text()).toContain('Save changes');
+ expect(findButton().text()).toContain('Save changes');
});
- it('emits a save event with the chosen template when the save button is clicked', () => {
- const eventSpy = jest.fn();
- eventHub.$on('serviceDeskTemplateSave', eventSpy);
-
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
+ it('emits a save event with the chosen template when the save button is clicked', async () => {
+ wrapper = createComponent({
+ props: {
initialSelectedTemplate: 'Bug',
initialOutgoingName: 'GitLab Support Bot',
initialProjectKey: 'key',
},
});
- wrapper.find('button.btn-success').trigger('click');
+ findButton().vm.$emit('click');
+
+ await nextTick();
- expect(eventSpy).toHaveBeenCalledWith({
+ const payload = {
selectedTemplate: 'Bug',
outgoingName: 'GitLab Support Bot',
projectKey: 'key',
- });
+ };
- eventHub.$off('serviceDeskTemplateSave', eventSpy);
- eventSpy.mockRestore();
+ expect(wrapper.emitted('save')[0]).toEqual([payload]);
});
});
describe('when isEnabled=false', () => {
beforeEach(() => {
- wrapper = shallowMount(ServiceDeskSetting, {
- propsData: {
- isEnabled: false,
- },
+ wrapper = createComponent({
+ props: { isEnabled: false },
});
});
it('does not render email panel', () => {
- expect(wrapper.find('#incoming-email-describer').exists()).toBe(false);
+ expect(findIncomingEmailLabel().exists()).toBe(false);
});
it('does not render template dropdown', () => {
- expect(wrapper.find('#service-desk-template-select').exists()).toBe(false);
+ expect(findTemplateDropdown().exists()).toBe(false);
});
it('does not render template save button', () => {
- expect(wrapper.find('button.btn-success').exists()).toBe(false);
+ expect(findButton().exists()).toBe(false);
});
- it('emits an event to turn on Service Desk when the toggle is clicked', () => {
- const eventSpy = jest.fn();
- eventHub.$on('serviceDeskEnabledCheckboxToggled', eventSpy);
-
- wrapper = mount(ServiceDeskSetting, {
- propsData: {
- isEnabled: true,
- },
- });
-
- wrapper.find('#service-desk-checkbox').trigger('click');
+ it('emits an event to turn on Service Desk when the toggle is clicked', async () => {
+ findToggle().vm.$emit('change', false);
- expect(eventSpy).toHaveBeenCalledWith(false);
+ await nextTick();
- eventHub.$off('serviceDeskEnabledCheckboxToggled', eventSpy);
- eventSpy.mockRestore();
+ expect(wrapper.emitted('toggle')[0]).toEqual([false]);
});
});
});
diff --git a/spec/frontend/projects/settings_service_desk/services/service_desk_service_spec.js b/spec/frontend/projects/settings_service_desk/services/service_desk_service_spec.js
deleted file mode 100644
index d5340df03fe..00000000000
--- a/spec/frontend/projects/settings_service_desk/services/service_desk_service_spec.js
+++ /dev/null
@@ -1,111 +0,0 @@
-import AxiosMockAdapter from 'axios-mock-adapter';
-import ServiceDeskService from '~/projects/settings_service_desk/services/service_desk_service';
-import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
-
-describe('ServiceDeskService', () => {
- const endpoint = `/gitlab-org/gitlab-test/service_desk`;
- const dummyResponse = { message: 'Dummy response' };
- const errorMessage = 'Network Error';
- let axiosMock;
- let service;
-
- beforeEach(() => {
- axiosMock = new AxiosMockAdapter(axios);
- service = new ServiceDeskService(endpoint);
- });
-
- afterEach(() => {
- axiosMock.restore();
- });
-
- describe('toggleServiceDesk', () => {
- it('makes a request to set service desk', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK, dummyResponse);
-
- return service.toggleServiceDesk(true).then((response) => {
- expect(response.data).toEqual(dummyResponse);
- });
- });
-
- it('fails on error response', () => {
- axiosMock.onPut(endpoint).networkError();
-
- return service.toggleServiceDesk(true).catch((error) => {
- expect(error.message).toBe(errorMessage);
- });
- });
-
- it('makes a request with the expected body', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK, dummyResponse);
-
- const spy = jest.spyOn(axios, 'put');
-
- service.toggleServiceDesk(true);
-
- expect(spy).toHaveBeenCalledWith(endpoint, {
- service_desk_enabled: true,
- });
-
- spy.mockRestore();
- });
- });
-
- describe('updateTemplate', () => {
- it('makes a request to update template', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK, dummyResponse);
-
- return service
- .updateTemplate(
- {
- selectedTemplate: 'Bug',
- outgoingName: 'GitLab Support Bot',
- },
- true,
- )
- .then((response) => {
- expect(response.data).toEqual(dummyResponse);
- });
- });
-
- it('fails on error response', () => {
- axiosMock.onPut(endpoint).networkError();
-
- return service
- .updateTemplate(
- {
- selectedTemplate: 'Bug',
- outgoingName: 'GitLab Support Bot',
- },
- true,
- )
- .catch((error) => {
- expect(error.message).toBe(errorMessage);
- });
- });
-
- it('makes a request with the expected body', () => {
- axiosMock.onPut(endpoint).replyOnce(httpStatusCodes.OK, dummyResponse);
-
- const spy = jest.spyOn(axios, 'put');
-
- service.updateTemplate(
- {
- selectedTemplate: 'Bug',
- outgoingName: 'GitLab Support Bot',
- projectKey: 'key',
- },
- true,
- );
-
- expect(spy).toHaveBeenCalledWith(endpoint, {
- issue_template_key: 'Bug',
- outgoing_name: 'GitLab Support Bot',
- project_key: 'key',
- service_desk_enabled: true,
- });
-
- spy.mockRestore();
- });
- });
-});
diff --git a/spec/frontend/registry/explorer/components/delete_image_spec.js b/spec/frontend/registry/explorer/components/delete_image_spec.js
new file mode 100644
index 00000000000..5c9ecf57a9f
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/delete_image_spec.js
@@ -0,0 +1,153 @@
+import { shallowMount } from '@vue/test-utils';
+import waitForPromises from 'helpers/wait_for_promises';
+import component from '~/registry/explorer/components/delete_image.vue';
+import deleteContainerRepositoryMutation from '~/registry/explorer/graphql/mutations/delete_container_repository.mutation.graphql';
+import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
+
+import { GRAPHQL_PAGE_SIZE } from '~/registry/explorer/constants/index';
+
+describe('Delete Image', () => {
+ let wrapper;
+ const id = '1';
+ const storeMock = {
+ readQuery: jest.fn().mockReturnValue({
+ containerRepository: {
+ status: 'foo',
+ },
+ }),
+ writeQuery: jest.fn(),
+ };
+
+ const updatePayload = {
+ data: {
+ destroyContainerRepository: {
+ containerRepository: {
+ status: 'baz',
+ },
+ },
+ },
+ };
+
+ const findButton = () => wrapper.find('button');
+
+ const mountComponent = ({
+ propsData = { id },
+ mutate = jest.fn().mockResolvedValue({}),
+ } = {}) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ mocks: {
+ $apollo: {
+ mutate,
+ },
+ },
+ scopedSlots: {
+ default: '<button @click="props.doDelete">test</button>',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('executes apollo mutate on doDelete', () => {
+ const mutate = jest.fn().mockResolvedValue({});
+ mountComponent({ mutate });
+
+ wrapper.vm.doDelete();
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: deleteContainerRepositoryMutation,
+ variables: {
+ id,
+ },
+ update: undefined,
+ });
+ });
+
+ it('on success emits the correct events', async () => {
+ const mutate = jest.fn().mockResolvedValue({});
+ mountComponent({ mutate });
+
+ wrapper.vm.doDelete();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('start')).toEqual([[]]);
+ expect(wrapper.emitted('success')).toEqual([[]]);
+ expect(wrapper.emitted('end')).toEqual([[]]);
+ });
+
+ it('when a payload contains an error emits an error event', async () => {
+ const mutate = jest
+ .fn()
+ .mockResolvedValue({ data: { destroyContainerRepository: { errors: ['foo'] } } });
+
+ mountComponent({ mutate });
+ wrapper.vm.doDelete();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[['foo']]]);
+ });
+
+ it('when the api call errors emits an error event', async () => {
+ const mutate = jest.fn().mockRejectedValue('error');
+
+ mountComponent({ mutate });
+ wrapper.vm.doDelete();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[['error']]]);
+ });
+
+ it('uses the update function, when the prop is set to true', () => {
+ const mutate = jest.fn().mockResolvedValue({});
+
+ mountComponent({ mutate, propsData: { id, useUpdateFn: true } });
+ wrapper.vm.doDelete();
+
+ expect(mutate).toHaveBeenCalledWith({
+ mutation: deleteContainerRepositoryMutation,
+ variables: {
+ id,
+ },
+ update: wrapper.vm.updateImageStatus,
+ });
+ });
+
+ it('updateImage status reads and write to the cache', () => {
+ mountComponent();
+
+ const variables = {
+ id,
+ first: GRAPHQL_PAGE_SIZE,
+ };
+
+ wrapper.vm.updateImageStatus(storeMock, updatePayload);
+
+ expect(storeMock.readQuery).toHaveBeenCalledWith({
+ query: getContainerRepositoryDetailsQuery,
+ variables,
+ });
+ expect(storeMock.writeQuery).toHaveBeenCalledWith({
+ query: getContainerRepositoryDetailsQuery,
+ variables,
+ data: {
+ containerRepository: {
+ status: updatePayload.data.destroyContainerRepository.containerRepository.status,
+ },
+ },
+ });
+ });
+
+ it('binds the doDelete function to the default scoped slot', () => {
+ const mutate = jest.fn().mockResolvedValue({});
+ mountComponent({ mutate });
+ findButton().trigger('click');
+ expect(mutate).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/registry/explorer/components/details_page/empty_state_spec.js b/spec/frontend/registry/explorer/components/details_page/empty_state_spec.js
new file mode 100644
index 00000000000..7739f111906
--- /dev/null
+++ b/spec/frontend/registry/explorer/components/details_page/empty_state_spec.js
@@ -0,0 +1,54 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlEmptyState } from '@gitlab/ui';
+import component from '~/registry/explorer/components/details_page/empty_state.vue';
+import {
+ NO_TAGS_TITLE,
+ NO_TAGS_MESSAGE,
+ MISSING_OR_DELETED_IMAGE_TITLE,
+ MISSING_OR_DELETED_IMAGE_MESSAGE,
+} from '~/registry/explorer/constants';
+
+describe('EmptyTagsState component', () => {
+ let wrapper;
+
+ const findEmptyState = () => wrapper.find(GlEmptyState);
+
+ const mountComponent = (propsData) => {
+ wrapper = shallowMount(component, {
+ stubs: {
+ GlEmptyState,
+ },
+ propsData,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('contains gl-empty-state', () => {
+ mountComponent();
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it.each`
+ isEmptyImage | title | description
+ ${false} | ${NO_TAGS_TITLE} | ${NO_TAGS_MESSAGE}
+ ${true} | ${MISSING_OR_DELETED_IMAGE_TITLE} | ${MISSING_OR_DELETED_IMAGE_MESSAGE}
+ `(
+ 'when isEmptyImage is $isEmptyImage has the correct props',
+ ({ isEmptyImage, title, description }) => {
+ mountComponent({
+ noContainersImage: 'foo',
+ isEmptyImage,
+ });
+
+ expect(findEmptyState().props()).toMatchObject({
+ title,
+ description,
+ svgPath: 'foo',
+ });
+ },
+ );
+});
diff --git a/spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js b/spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js
deleted file mode 100644
index 09afd9d2d84..00000000000
--- a/spec/frontend/registry/explorer/components/details_page/empty_tags_state_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlEmptyState } from '@gitlab/ui';
-import component from '~/registry/explorer/components/details_page/empty_tags_state.vue';
-import {
- EMPTY_IMAGE_REPOSITORY_TITLE,
- EMPTY_IMAGE_REPOSITORY_MESSAGE,
-} from '~/registry/explorer/constants';
-
-describe('EmptyTagsState component', () => {
- let wrapper;
-
- const findEmptyState = () => wrapper.find(GlEmptyState);
-
- const mountComponent = () => {
- wrapper = shallowMount(component, {
- stubs: {
- GlEmptyState,
- },
- propsData: {
- noContainersImage: 'foo',
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- it('contains gl-empty-state', () => {
- mountComponent();
- expect(findEmptyState().exists()).toBe(true);
- });
-
- it('has the correct props', () => {
- mountComponent();
- expect(findEmptyState().props()).toMatchObject({
- title: EMPTY_IMAGE_REPOSITORY_TITLE,
- description: EMPTY_IMAGE_REPOSITORY_MESSAGE,
- svgPath: 'foo',
- });
- });
-});
diff --git a/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js b/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js
index 1ba2036dc34..ffdc6b787cb 100644
--- a/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/group_empty_state_spec.js
@@ -1,8 +1,8 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
-import { GlEmptyState } from '../../stubs';
import groupEmptyState from '~/registry/explorer/components/list_page/group_empty_state.vue';
+import { GlEmptyState } from '../../stubs';
const localVue = createLocalVue();
localVue.use(Vuex);
diff --git a/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js b/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js
index 3a27cf1923c..cbd1f1a99db 100644
--- a/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js
+++ b/spec/frontend/registry/explorer/components/list_page/project_empty_state_spec.js
@@ -1,8 +1,8 @@
import Vuex from 'vuex';
import { shallowMount, createLocalVue } from '@vue/test-utils';
import { GlSprintf } from '@gitlab/ui';
-import { GlEmptyState } from '../../stubs';
import projectEmptyState from '~/registry/explorer/components/list_page/project_empty_state.vue';
+import { GlEmptyState } from '../../stubs';
import { dockerCommands } from '../../mock_data';
const localVue = createLocalVue();
diff --git a/spec/frontend/registry/explorer/mock_data.js b/spec/frontend/registry/explorer/mock_data.js
index b0fc009872c..f4453912db4 100644
--- a/spec/frontend/registry/explorer/mock_data.js
+++ b/spec/frontend/registry/explorer/mock_data.js
@@ -235,3 +235,9 @@ export const graphQLProjectImageRepositoriesDetailsMock = {
},
},
};
+
+export const graphQLEmptyImageDetailsMock = {
+ data: {
+ containerRepository: null,
+ },
+};
diff --git a/spec/frontend/registry/explorer/pages/details_spec.js b/spec/frontend/registry/explorer/pages/details_spec.js
index 1746a6a63b6..7106cdae27f 100644
--- a/spec/frontend/registry/explorer/pages/details_spec.js
+++ b/spec/frontend/registry/explorer/pages/details_spec.js
@@ -4,13 +4,14 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Tracking from '~/tracking';
+import axios from '~/lib/utils/axios_utils';
import component from '~/registry/explorer/pages/details.vue';
import DeleteAlert from '~/registry/explorer/components/details_page/delete_alert.vue';
import PartialCleanupAlert from '~/registry/explorer/components/details_page/partial_cleanup_alert.vue';
import DetailsHeader from '~/registry/explorer/components/details_page/details_header.vue';
import TagsLoader from '~/registry/explorer/components/details_page/tags_loader.vue';
import TagsList from '~/registry/explorer/components/details_page/tags_list.vue';
-import EmptyTagsState from '~/registry/explorer/components/details_page/empty_tags_state.vue';
+import EmptyTagsState from '~/registry/explorer/components/details_page/empty_state.vue';
import getContainerRepositoryDetailsQuery from '~/registry/explorer/graphql/queries/get_container_repository_details.query.graphql';
import deleteContainerRepositoryTagsMutation from '~/registry/explorer/graphql/mutations/delete_container_repository_tags.mutation.graphql';
@@ -22,6 +23,7 @@ import {
graphQLImageDetailsEmptyTagsMock,
graphQLDeleteImageRepositoryTagsMock,
containerRepositoryMock,
+ graphQLEmptyImageDetailsMock,
tagsMock,
tagsPageInfo,
} from '../mock_data';
@@ -39,7 +41,7 @@ describe('Details Page', () => {
const findTagsList = () => wrapper.find(TagsList);
const findDeleteAlert = () => wrapper.find(DeleteAlert);
const findDetailsHeader = () => wrapper.find(DetailsHeader);
- const findEmptyTagsState = () => wrapper.find(EmptyTagsState);
+ const findEmptyState = () => wrapper.find(EmptyTagsState);
const findPartialCleanupAlert = () => wrapper.find(PartialCleanupAlert);
const routeId = 1;
@@ -133,6 +135,27 @@ describe('Details Page', () => {
});
});
+ describe('when the image does not exist', () => {
+ it('does not show the default ui', async () => {
+ mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLEmptyImageDetailsMock) });
+
+ await waitForApolloRequestRender();
+
+ expect(findTagsLoader().exists()).toBe(false);
+ expect(findDetailsHeader().exists()).toBe(false);
+ expect(findTagsList().exists()).toBe(false);
+ expect(findPagination().exists()).toBe(false);
+ });
+
+ it('shows an empty state message', async () => {
+ mountComponent({ resolver: jest.fn().mockResolvedValue(graphQLEmptyImageDetailsMock) });
+
+ await waitForApolloRequestRender();
+
+ expect(findEmptyState().exists()).toBe(true);
+ });
+ });
+
describe('when the list of tags is empty', () => {
const resolver = jest.fn().mockResolvedValue(graphQLImageDetailsEmptyTagsMock);
@@ -141,7 +164,7 @@ describe('Details Page', () => {
await waitForApolloRequestRender();
- expect(findEmptyTagsState().exists()).toBe(true);
+ expect(findEmptyState().exists()).toBe(true);
});
it('does not show the loader', async () => {
@@ -401,6 +424,9 @@ describe('Details Page', () => {
const config = {
runCleanupPoliciesHelpPagePath: 'foo',
cleanupPoliciesHelpPagePath: 'bar',
+ userCalloutsPath: 'call_out_path',
+ userCalloutId: 'call_out_id',
+ showUnfinishedTagCleanupCallout: true,
};
describe(`when expirationPolicyCleanupStatus is ${UNFINISHED_STATUS}`, () => {
@@ -413,8 +439,9 @@ describe('Details Page', () => {
}),
);
});
+
it('exists', async () => {
- mountComponent({ resolver });
+ mountComponent({ resolver, config });
await waitForApolloRequestRender();
@@ -426,11 +453,16 @@ describe('Details Page', () => {
await waitForApolloRequestRender();
- expect(findPartialCleanupAlert().props()).toEqual({ ...config });
+ expect(findPartialCleanupAlert().props()).toEqual({
+ runCleanupPoliciesHelpPagePath: config.runCleanupPoliciesHelpPagePath,
+ cleanupPoliciesHelpPagePath: config.cleanupPoliciesHelpPagePath,
+ });
});
it('dismiss hides the component', async () => {
- mountComponent({ resolver });
+ jest.spyOn(axios, 'post').mockReturnValue();
+
+ mountComponent({ resolver, config });
await waitForApolloRequestRender();
@@ -440,13 +472,25 @@ describe('Details Page', () => {
await wrapper.vm.$nextTick();
+ expect(axios.post).toHaveBeenCalledWith(config.userCalloutsPath, {
+ feature_name: config.userCalloutId,
+ });
+ expect(findPartialCleanupAlert().exists()).toBe(false);
+ });
+
+ it('is hidden if the callout is dismissed', async () => {
+ mountComponent({ resolver });
+
+ await waitForApolloRequestRender();
+
expect(findPartialCleanupAlert().exists()).toBe(false);
});
});
describe(`when expirationPolicyCleanupStatus is not ${UNFINISHED_STATUS}`, () => {
it('the component is hidden', async () => {
- mountComponent();
+ mountComponent({ config });
+
await waitForApolloRequestRender();
expect(findPartialCleanupAlert().exists()).toBe(false);
diff --git a/spec/frontend/registry/explorer/pages/list_spec.js b/spec/frontend/registry/explorer/pages/list_spec.js
index c4556934934..9c49ff0c9e5 100644
--- a/spec/frontend/registry/explorer/pages/list_spec.js
+++ b/spec/frontend/registry/explorer/pages/list_spec.js
@@ -11,6 +11,7 @@ import GroupEmptyState from '~/registry/explorer/components/list_page/group_empt
import ProjectEmptyState from '~/registry/explorer/components/list_page/project_empty_state.vue';
import RegistryHeader from '~/registry/explorer/components/list_page/registry_header.vue';
import ImageList from '~/registry/explorer/components/list_page/image_list.vue';
+import DeleteImage from '~/registry/explorer/components/delete_image.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
import {
@@ -27,7 +28,6 @@ import {
graphQLImageListMock,
graphQLImageDeleteMock,
deletedContainerRepository,
- graphQLImageDeleteMockError,
graphQLEmptyImageListMock,
graphQLEmptyGroupImageListMock,
pageInfo,
@@ -58,6 +58,7 @@ describe('List Page', () => {
const findListHeader = () => wrapper.find('[data-testid="listHeader"]');
const findSearchBox = () => wrapper.find(GlSearchBoxByClick);
const findEmptySearchMessage = () => wrapper.find('[data-testid="emptySearch"]');
+ const findDeleteImage = () => wrapper.find(DeleteImage);
const waitForApolloRequestRender = async () => {
jest.runOnlyPendingTimers();
@@ -91,6 +92,7 @@ describe('List Page', () => {
GlSprintf,
RegistryHeader,
TitleArea,
+ DeleteImage,
},
mocks: {
$toast,
@@ -300,23 +302,22 @@ describe('List Page', () => {
});
describe('delete image', () => {
- const deleteImage = async () => {
- await wrapper.vm.$nextTick();
+ const selectImageForDeletion = async () => {
+ await waitForApolloRequestRender();
findImageList().vm.$emit('delete', deletedContainerRepository);
- findDeleteModal().vm.$emit('ok');
-
- await waitForApolloRequestRender();
};
it('should call deleteItem when confirming deletion', async () => {
const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock);
mountComponent({ mutationResolver });
- await deleteImage();
+ await selectImageForDeletion();
+
+ findDeleteModal().vm.$emit('primary');
+ await waitForApolloRequestRender();
expect(wrapper.vm.itemToDelete).toEqual(deletedContainerRepository);
- expect(mutationResolver).toHaveBeenCalledWith({ id: deletedContainerRepository.id });
const updatedImage = findImageList()
.props('images')
@@ -326,10 +327,12 @@ describe('List Page', () => {
});
it('should show a success alert when delete request is successful', async () => {
- const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMock);
- mountComponent({ mutationResolver });
+ mountComponent();
- await deleteImage();
+ await selectImageForDeletion();
+
+ findDeleteImage().vm.$emit('success');
+ await wrapper.vm.$nextTick();
const alert = findDeleteAlert();
expect(alert.exists()).toBe(true);
@@ -340,23 +343,12 @@ describe('List Page', () => {
describe('when delete request fails it shows an alert', () => {
it('user recoverable error', async () => {
- const mutationResolver = jest.fn().mockResolvedValue(graphQLImageDeleteMockError);
- mountComponent({ mutationResolver });
+ mountComponent();
- await deleteImage();
+ await selectImageForDeletion();
- const alert = findDeleteAlert();
- expect(alert.exists()).toBe(true);
- expect(alert.text().replace(/\s\s+/gm, ' ')).toBe(
- DELETE_IMAGE_ERROR_MESSAGE.replace('%{title}', wrapper.vm.itemToDelete.path),
- );
- });
-
- it('network error', async () => {
- const mutationResolver = jest.fn().mockRejectedValue();
- mountComponent({ mutationResolver });
-
- await deleteImage();
+ findDeleteImage().vm.$emit('error');
+ await wrapper.vm.$nextTick();
const alert = findDeleteAlert();
expect(alert.exists()).toBe(true);
@@ -499,9 +491,8 @@ describe('List Page', () => {
testTrackingCall('cancel_delete');
});
- it('send an event when confirm is clicked on modal', () => {
- const deleteModal = findDeleteModal();
- deleteModal.vm.$emit('ok');
+ it('send an event when the deletion starts', () => {
+ findDeleteImage().vm.$emit('start');
testTrackingCall('confirm_delete');
});
});
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index 1481dd30fd4..3c5ee212225 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -7,9 +7,9 @@ import ReleasesApp from '~/releases/components/app_index.vue';
import createStore from '~/releases/stores';
import createListModule from '~/releases/stores/modules/list';
import api from '~/api';
-import { pageInfoHeadersWithoutPagination, pageInfoHeadersWithPagination } from '../mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import ReleasesPagination from '~/releases/components/releases_pagination.vue';
+import { pageInfoHeadersWithoutPagination, pageInfoHeadersWithPagination } from '../mock_data';
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
diff --git a/spec/frontend/releases/components/release_block_footer_spec.js b/spec/frontend/releases/components/release_block_footer_spec.js
index f1c0c24f8ca..b3161f9fc0d 100644
--- a/spec/frontend/releases/components/release_block_footer_spec.js
+++ b/spec/frontend/releases/components/release_block_footer_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
import { GlLink, GlIcon } from '@gitlab/ui';
+import { cloneDeep } from 'lodash';
import { trimText } from 'helpers/text_helper';
import { getJSONFixture } from 'helpers/fixtures';
-import { cloneDeep } from 'lodash';
import ReleaseBlockFooter from '~/releases/components/release_block_footer.vue';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index 396e7bd8745..39c00ae69fe 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -1,8 +1,8 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { cloneDeep } from 'lodash';
import testAction from 'helpers/vuex_action_helper';
import { getJSONFixture } from 'helpers/fixtures';
-import { cloneDeep } from 'lodash';
import * as actions from '~/releases/stores/modules/detail/actions';
import * as types from '~/releases/stores/modules/detail/mutation_types';
import createState from '~/releases/stores/modules/detail/state';
diff --git a/spec/frontend/releases/stores/modules/list/actions_spec.js b/spec/frontend/releases/stores/modules/list/actions_spec.js
index 35551b77dc4..278c8b3b3d3 100644
--- a/spec/frontend/releases/stores/modules/list/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/list/actions_spec.js
@@ -17,9 +17,9 @@ import {
parseIntPagination,
convertObjectPropsToCamelCase,
} from '~/lib/utils/common_utils';
-import { pageInfoHeadersWithoutPagination } from '../../../mock_data';
import allReleasesQuery from '~/releases/queries/all_releases.query.graphql';
import { PAGE_SIZE } from '~/releases/constants';
+import { pageInfoHeadersWithoutPagination } from '../../../mock_data';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalReleases = [originalRelease];
diff --git a/spec/frontend/releases/stores/modules/list/mutations_spec.js b/spec/frontend/releases/stores/modules/list/mutations_spec.js
index 78071573072..7eb78b23b7b 100644
--- a/spec/frontend/releases/stores/modules/list/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/list/mutations_spec.js
@@ -3,8 +3,8 @@ import createState from '~/releases/stores/modules/list/state';
import mutations from '~/releases/stores/modules/list/mutations';
import * as types from '~/releases/stores/modules/list/mutation_types';
import { parseIntPagination, convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
-import { pageInfoHeadersWithoutPagination } from '../../../mock_data';
import { convertAllReleasesGraphQLResponse } from '~/releases/util';
+import { pageInfoHeadersWithoutPagination } from '../../../mock_data';
const originalRelease = getJSONFixture('api/releases/release.json');
const originalReleases = [originalRelease];
diff --git a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
index ecb657af6f1..fb743c8b90c 100644
--- a/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
+++ b/spec/frontend/reports/codequality_report/grouped_codequality_reports_app_spec.js
@@ -9,7 +9,6 @@ const localVue = createLocalVue();
localVue.use(Vuex);
describe('Grouped code quality reports app', () => {
- const Component = localVue.extend(GroupedCodequalityReportsApp);
let wrapper;
let mockStore;
@@ -22,7 +21,7 @@ describe('Grouped code quality reports app', () => {
};
const mountComponent = (props = {}) => {
- wrapper = mount(Component, {
+ wrapper = mount(GroupedCodequalityReportsApp, {
store: mockStore,
localVue,
propsData: {
@@ -135,7 +134,7 @@ describe('Grouped code quality reports app', () => {
});
it('renders error text', () => {
- expect(findWidget().text()).toEqual('Failed to load codeclimate report');
+ expect(findWidget().text()).toContain('Failed to load codeclimate report');
});
it('renders a help icon with more information', () => {
diff --git a/spec/frontend/reports/codequality_report/mock_data.js b/spec/frontend/reports/codequality_report/mock_data.js
index 9bd61527d3f..c5cecb34509 100644
--- a/spec/frontend/reports/codequality_report/mock_data.js
+++ b/spec/frontend/reports/codequality_report/mock_data.js
@@ -88,3 +88,53 @@ export const issueDiff = [
urlPath: 'headPath/lib/six.rb#L6',
},
];
+
+export const reportIssues = {
+ status: 'failed',
+ new_errors: [
+ {
+ description:
+ 'Method `long_if` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.',
+ severity: 'minor',
+ file_path: 'codequality.rb',
+ line: 5,
+ },
+ ],
+ resolved_errors: [
+ {
+ description: 'Insecure Dependency',
+ severity: 'major',
+ file_path: 'lib/six.rb',
+ line: 22,
+ },
+ ],
+ existing_errors: [],
+ summary: { total: 3, resolved: 0, errored: 3 },
+};
+
+export const parsedReportIssues = {
+ newIssues: [
+ {
+ description:
+ 'Method `long_if` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.',
+ file_path: 'codequality.rb',
+ line: 5,
+ name:
+ 'Method `long_if` has a Cognitive Complexity of 10 (exceeds 5 allowed). Consider refactoring.',
+ path: 'codequality.rb',
+ severity: 'minor',
+ urlPath: 'null/codequality.rb#L5',
+ },
+ ],
+ resolvedIssues: [
+ {
+ description: 'Insecure Dependency',
+ file_path: 'lib/six.rb',
+ line: 22,
+ name: 'Insecure Dependency',
+ path: 'lib/six.rb',
+ severity: 'major',
+ urlPath: 'null/lib/six.rb#L22',
+ },
+ ],
+};
diff --git a/spec/frontend/reports/codequality_report/store/actions_spec.js b/spec/frontend/reports/codequality_report/store/actions_spec.js
index 321785cb85a..6ff6bae1284 100644
--- a/spec/frontend/reports/codequality_report/store/actions_spec.js
+++ b/spec/frontend/reports/codequality_report/store/actions_spec.js
@@ -5,7 +5,14 @@ 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 { headIssues, baseIssues, mockParsedHeadIssues, mockParsedBaseIssues } from '../mock_data';
+import {
+ headIssues,
+ baseIssues,
+ mockParsedHeadIssues,
+ mockParsedBaseIssues,
+ reportIssues,
+ parsedReportIssues,
+} from '../mock_data';
// mock codequality comparison worker
jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () =>
@@ -39,6 +46,7 @@ describe('Codequality Reports actions', () => {
headPath: 'headPath',
baseBlobPath: 'baseBlobPath',
headBlobPath: 'headBlobPath',
+ reportsPath: 'reportsPath',
helpPath: 'codequalityHelpPath',
};
@@ -55,68 +63,119 @@ describe('Codequality Reports actions', () => {
describe('fetchReports', () => {
let mock;
+ let diffFeatureFlagEnabled;
- beforeEach(() => {
- localState.headPath = `${TEST_HOST}/head.json`;
- localState.basePath = `${TEST_HOST}/base.json`;
- mock = new MockAdapter(axios);
- });
+ describe('with codequalityBackendComparison feature flag enabled', () => {
+ beforeEach(() => {
+ diffFeatureFlagEnabled = true;
+ localState.reportsPath = `${TEST_HOST}/codequality_reports.json`;
+ mock = new MockAdapter(axios);
+ });
- afterEach(() => {
- mock.restore();
- });
+ afterEach(() => {
+ mock.restore();
+ });
- describe('on success', () => {
- it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => {
- mock.onGet(`${TEST_HOST}/head.json`).reply(200, headIssues);
- mock.onGet(`${TEST_HOST}/base.json`).reply(200, baseIssues);
-
- testAction(
- actions.fetchReports,
- null,
- localState,
- [{ type: types.REQUEST_REPORTS }],
- [
- {
- payload: {
- newIssues: [mockParsedHeadIssues[0]],
- resolvedIssues: [mockParsedBaseIssues[0]],
+ describe('on success', () => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => {
+ mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(200, reportIssues);
+
+ testAction(
+ actions.fetchReports,
+ diffFeatureFlagEnabled,
+ localState,
+ [{ type: types.REQUEST_REPORTS }],
+ [
+ {
+ payload: parsedReportIssues,
+ type: 'receiveReportsSuccess',
},
- type: 'receiveReportsSuccess',
- },
- ],
- done,
- );
+ ],
+ done,
+ );
+ });
});
- });
- describe('on error', () => {
- it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
- mock.onGet(`${TEST_HOST}/head.json`).reply(500);
-
- testAction(
- actions.fetchReports,
- null,
- localState,
- [{ type: types.REQUEST_REPORTS }],
- [{ type: 'receiveReportsError' }],
- done,
- );
+ describe('on error', () => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
+ mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(500);
+
+ testAction(
+ actions.fetchReports,
+ diffFeatureFlagEnabled,
+ localState,
+ [{ type: types.REQUEST_REPORTS }],
+ [{ type: 'receiveReportsError', payload: expect.any(Error) }],
+ done,
+ );
+ });
});
});
- describe('with no base path', () => {
- it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
- localState.basePath = null;
-
- testAction(
- actions.fetchReports,
- null,
- localState,
- [{ type: types.REQUEST_REPORTS }],
- [{ type: 'receiveReportsError' }],
- done,
- );
+ describe('with codequalityBackendComparison feature flag disabled', () => {
+ beforeEach(() => {
+ diffFeatureFlagEnabled = false;
+ localState.headPath = `${TEST_HOST}/head.json`;
+ localState.basePath = `${TEST_HOST}/base.json`;
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('on success', () => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', (done) => {
+ mock.onGet(`${TEST_HOST}/head.json`).reply(200, headIssues);
+ mock.onGet(`${TEST_HOST}/base.json`).reply(200, baseIssues);
+
+ testAction(
+ actions.fetchReports,
+ diffFeatureFlagEnabled,
+ localState,
+ [{ type: types.REQUEST_REPORTS }],
+ [
+ {
+ payload: {
+ newIssues: [mockParsedHeadIssues[0]],
+ resolvedIssues: [mockParsedBaseIssues[0]],
+ },
+ type: 'receiveReportsSuccess',
+ },
+ ],
+ done,
+ );
+ });
+ });
+
+ describe('on error', () => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
+ mock.onGet(`${TEST_HOST}/head.json`).reply(500);
+
+ testAction(
+ actions.fetchReports,
+ diffFeatureFlagEnabled,
+ localState,
+ [{ type: types.REQUEST_REPORTS }],
+ [{ type: 'receiveReportsError', payload: expect.any(Error) }],
+ done,
+ );
+ });
+ });
+
+ describe('with no base path', () => {
+ it('commits REQUEST_REPORTS and dispatches receiveReportsError', (done) => {
+ localState.basePath = null;
+
+ testAction(
+ actions.fetchReports,
+ diffFeatureFlagEnabled,
+ localState,
+ [{ type: types.REQUEST_REPORTS }],
+ [{ type: 'receiveReportsError' }],
+ done,
+ );
+ });
});
});
});
@@ -142,7 +201,7 @@ describe('Codequality Reports actions', () => {
actions.receiveReportsError,
null,
localState,
- [{ type: types.RECEIVE_REPORTS_ERROR }],
+ [{ type: types.RECEIVE_REPORTS_ERROR, payload: null }],
[],
done,
);
diff --git a/spec/frontend/reports/codequality_report/store/mutations_spec.js b/spec/frontend/reports/codequality_report/store/mutations_spec.js
index 658abf3088c..f7f9e611ee8 100644
--- a/spec/frontend/reports/codequality_report/store/mutations_spec.js
+++ b/spec/frontend/reports/codequality_report/store/mutations_spec.js
@@ -55,6 +55,12 @@ describe('Codequality Reports mutations', () => {
expect(localState.hasError).toEqual(false);
});
+ it('clears statusReason', () => {
+ mutations.RECEIVE_REPORTS_SUCCESS(localState, {});
+
+ expect(localState.statusReason).toEqual('');
+ });
+
it('sets newIssues and resolvedIssues from response data', () => {
const data = { newIssues: [{ id: 1 }], resolvedIssues: [{ id: 2 }] };
mutations.RECEIVE_REPORTS_SUCCESS(localState, data);
@@ -76,5 +82,13 @@ describe('Codequality Reports mutations', () => {
expect(localState.hasError).toEqual(true);
});
+
+ it('sets statusReason to string from error response data', () => {
+ const data = { status_reason: 'This merge request does not have codequality reports' };
+ const error = { response: { data } };
+ mutations.RECEIVE_REPORTS_ERROR(localState, error);
+
+ expect(localState.statusReason).toEqual(data.status_reason);
+ });
});
});
diff --git a/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js b/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js
index 085d697672d..389e9b4a1f6 100644
--- a/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js
+++ b/spec/frontend/reports/codequality_report/store/utils/codequality_comparison_spec.js
@@ -2,7 +2,13 @@ import {
parseCodeclimateMetrics,
doCodeClimateComparison,
} from '~/reports/codequality_report/store/utils/codequality_comparison';
-import { baseIssues, mockParsedHeadIssues, mockParsedBaseIssues } from '../../mock_data';
+import {
+ baseIssues,
+ mockParsedHeadIssues,
+ mockParsedBaseIssues,
+ reportIssues,
+ parsedReportIssues,
+} from '../../mock_data';
jest.mock('~/reports/codequality_report/workers/codequality_comparison_worker', () => {
let mockPostMessageCallback;
@@ -34,7 +40,7 @@ describe('Codequality report store utils', () => {
let result;
describe('parseCodeclimateMetrics', () => {
- it('should parse the received issues', () => {
+ it('should parse the issues from codeclimate artifacts', () => {
[result] = parseCodeclimateMetrics(baseIssues, 'path');
expect(result.name).toEqual(baseIssues[0].check_name);
@@ -42,6 +48,14 @@ describe('Codequality report store utils', () => {
expect(result.line).toEqual(baseIssues[0].location.lines.begin);
});
+ it('should parse the issues from backend codequality diff', () => {
+ [result] = parseCodeclimateMetrics(reportIssues.new_errors, 'path');
+
+ expect(result.name).toEqual(parsedReportIssues.newIssues[0].name);
+ expect(result.path).toEqual(parsedReportIssues.newIssues[0].path);
+ expect(result.line).toEqual(parsedReportIssues.newIssues[0].line);
+ });
+
describe('when an issue has no location or path', () => {
const issue = { description: 'Insecure Dependency' };
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 492192988fb..88a82908e1e 100644
--- a/spec/frontend/reports/components/grouped_test_reports_app_spec.js
+++ b/spec/frontend/reports/components/grouped_test_reports_app_spec.js
@@ -18,12 +18,11 @@ localVue.use(Vuex);
describe('Grouped test reports app', () => {
const endpoint = 'endpoint.json';
const pipelinePath = '/path/to/pipeline';
- const Component = localVue.extend(GroupedTestReportsApp);
let wrapper;
let mockStore;
const mountComponent = ({ props = { pipelinePath } } = {}) => {
- wrapper = mount(Component, {
+ wrapper = mount(GroupedTestReportsApp, {
store: mockStore,
localVue,
propsData: {
diff --git a/spec/frontend/reports/components/summary_row_spec.js b/spec/frontend/reports/components/summary_row_spec.js
index 85c68ed069b..bdd6de1e0be 100644
--- a/spec/frontend/reports/components/summary_row_spec.js
+++ b/spec/frontend/reports/components/summary_row_spec.js
@@ -32,7 +32,7 @@ describe('Summary row', () => {
it('renders provided summary', () => {
createComponent();
- expect(findSummary().text()).toEqual(props.summary);
+ expect(findSummary().text()).toContain(props.summary);
});
it('renders provided icon', () => {
@@ -48,7 +48,7 @@ describe('Summary row', () => {
createComponent({ slots: { summary: summarySlotContent } });
expect(wrapper.text()).not.toContain(props.summary);
- expect(findSummary().text()).toEqual(summarySlotContent);
+ expect(findSummary().text()).toContain(summarySlotContent);
});
});
});
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index fe77057c3d4..f611d6a3535 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -58,77 +58,75 @@ describe('Repository last commit component', () => {
loading | label
${true} | ${'shows'}
${false} | ${'hides'}
- `('$label when loading icon $loading is true', ({ loading }) => {
+ `('$label when loading icon $loading is true', async ({ loading }) => {
factory(createCommitData(), loading);
- return vm.vm.$nextTick(() => {
- expect(vm.find(GlLoadingIcon).exists()).toBe(loading);
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find(GlLoadingIcon).exists()).toBe(loading);
});
- it('renders commit widget', () => {
+ it('renders commit widget', async () => {
factory();
- return vm.vm.$nextTick(() => {
- expect(vm.element).toMatchSnapshot();
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.element).toMatchSnapshot();
});
- it('renders short commit ID', () => {
+ it('renders short commit ID', async () => {
factory();
- return vm.vm.$nextTick(() => {
- expect(vm.find('[data-testid="last-commit-id-label"]').text()).toEqual('12345678');
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find('[data-testid="last-commit-id-label"]').text()).toEqual('12345678');
});
- it('hides pipeline components when pipeline does not exist', () => {
+ it('hides pipeline components when pipeline does not exist', async () => {
factory(createCommitData({ pipeline: null }));
- return vm.vm.$nextTick(() => {
- expect(vm.find('.js-commit-pipeline').exists()).toBe(false);
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find('.js-commit-pipeline').exists()).toBe(false);
});
- it('renders pipeline components', () => {
+ it('renders pipeline components', async () => {
factory();
- return vm.vm.$nextTick(() => {
- expect(vm.find('.js-commit-pipeline').exists()).toBe(true);
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find('.js-commit-pipeline').exists()).toBe(true);
});
- it('hides author component when author does not exist', () => {
+ it('hides author component when author does not exist', async () => {
factory(createCommitData({ author: null }));
- return vm.vm.$nextTick(() => {
- expect(vm.find('.js-user-link').exists()).toBe(false);
- expect(vm.find(UserAvatarLink).exists()).toBe(false);
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find('.js-user-link').exists()).toBe(false);
+ expect(vm.find(UserAvatarLink).exists()).toBe(false);
});
- it('does not render description expander when description is null', () => {
+ it('does not render description expander when description is null', async () => {
factory(createCommitData({ descriptionHtml: null }));
- return vm.vm.$nextTick(() => {
- expect(vm.find('.text-expander').exists()).toBe(false);
- expect(vm.find('.commit-row-description').exists()).toBe(false);
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find('.text-expander').exists()).toBe(false);
+ expect(vm.find('.commit-row-description').exists()).toBe(false);
});
- it('expands commit description when clicking expander', () => {
+ it('expands commit description when clicking expander', async () => {
factory(createCommitData({ descriptionHtml: 'Test description' }));
- return vm.vm
- .$nextTick()
- .then(() => {
- vm.find('.text-expander').vm.$emit('click');
- return vm.vm.$nextTick();
- })
- .then(() => {
- expect(vm.find('.commit-row-description').isVisible()).toBe(true);
- expect(vm.find('.text-expander').classes('open')).toBe(true);
- });
+ await vm.vm.$nextTick();
+
+ vm.find('.text-expander').vm.$emit('click');
+
+ await vm.vm.$nextTick();
+
+ expect(vm.find('.commit-row-description').isVisible()).toBe(true);
+ expect(vm.find('.text-expander').classes('open')).toBe(true);
});
it('strips the first newline of the description', async () => {
@@ -141,19 +139,19 @@ describe('Repository last commit component', () => {
);
});
- it('renders the signature HTML as returned by the backend', () => {
+ it('renders the signature HTML as returned by the backend', async () => {
factory(createCommitData({ signatureHtml: '<button>Verified</button>' }));
- return vm.vm.$nextTick().then(() => {
- expect(vm.element).toMatchSnapshot();
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.element).toMatchSnapshot();
});
- it('sets correct CSS class if the commit message is empty', () => {
+ it('sets correct CSS class if the commit message is empty', async () => {
factory(createCommitData({ message: '' }));
- return vm.vm.$nextTick().then(() => {
- expect(vm.find('.item-title').classes()).toContain(emptyMessageClass);
- });
+ await vm.vm.$nextTick();
+
+ expect(vm.find('.item-title').classes()).toContain(emptyMessageClass);
});
});
diff --git a/spec/frontend/search/highlight_blob_search_result_spec.js b/spec/frontend/search/highlight_blob_search_result_spec.js
index 112e6f5124f..c1b0c7d794b 100644
--- a/spec/frontend/search/highlight_blob_search_result_spec.js
+++ b/spec/frontend/search/highlight_blob_search_result_spec.js
@@ -1,6 +1,7 @@
import setHighlightClass from '~/search/highlight_blob_search_result';
const fixture = 'search/blob_search_result.html';
+const searchKeyword = 'Send'; // spec/frontend/fixtures/search.rb#79
describe('search/highlight_blob_search_result', () => {
preloadFixtures(fixture);
@@ -8,7 +9,7 @@ describe('search/highlight_blob_search_result', () => {
beforeEach(() => loadFixtures(fixture));
it('highlights lines with search term occurrence', () => {
- setHighlightClass();
+ setHighlightClass(searchKeyword);
expect(document.querySelectorAll('.blob-result .hll').length).toBe(4);
});
diff --git a/spec/frontend/search/index_spec.js b/spec/frontend/search/index_spec.js
index 023cd341345..1992a7f4437 100644
--- a/spec/frontend/search/index_spec.js
+++ b/spec/frontend/search/index_spec.js
@@ -1,9 +1,11 @@
+import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
import { initSearchApp } from '~/search';
import createStore from '~/search/store';
jest.mock('~/search/store');
jest.mock('~/search/topbar');
jest.mock('~/search/sidebar');
+jest.mock('ee_else_ce/search/highlight_blob_search_result');
describe('initSearchApp', () => {
let defaultLocation;
@@ -42,6 +44,7 @@ describe('initSearchApp', () => {
it(`decodes ${search} to ${decodedSearch}`, () => {
expect(createStore).toHaveBeenCalledWith({ query: { search: decodedSearch } });
+ expect(setHighlightClass).toHaveBeenCalledWith(decodedSearch);
});
});
});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index ee509eaad8d..d076997b04a 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -26,7 +26,7 @@ export const MOCK_GROUPS = [
export const MOCK_PROJECT = {
name: 'test project',
- namespace_id: MOCK_GROUP.id,
+ namespace: MOCK_GROUP,
nameWithNamespace: 'test group test project',
id: 'test_1',
};
@@ -34,14 +34,30 @@ export const MOCK_PROJECT = {
export const MOCK_PROJECTS = [
{
name: 'test project',
- namespace_id: MOCK_GROUP.id,
+ namespace: MOCK_GROUP,
name_with_namespace: 'test group test project',
id: 'test_1',
},
{
name: 'test project 2',
- namespace_id: MOCK_GROUP.id,
+ namespace: MOCK_GROUP,
name_with_namespace: 'test group test project 2',
id: 'test_2',
},
];
+
+export const MOCK_SORT_OPTIONS = [
+ {
+ title: 'Most relevant',
+ sortable: false,
+ sortParam: 'relevant',
+ },
+ {
+ title: 'Created date',
+ sortable: true,
+ sortParam: {
+ asc: 'created_asc',
+ desc: 'created_desc',
+ },
+ },
+];
diff --git a/spec/frontend/search/sort/components/app_spec.js b/spec/frontend/search/sort/components/app_spec.js
new file mode 100644
index 00000000000..8e3f08f7e28
--- /dev/null
+++ b/spec/frontend/search/sort/components/app_spec.js
@@ -0,0 +1,168 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlButtonGroup, GlButton, GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { MOCK_QUERY, MOCK_SORT_OPTIONS } from 'jest/search/mock_data';
+import GlobalSearchSort from '~/search/sort/components/app.vue';
+import { SORT_DIRECTION_UI } from '~/search/sort/constants';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('GlobalSearchSort', () => {
+ let wrapper;
+
+ const actionSpies = {
+ setQuery: jest.fn(),
+ applyQuery: jest.fn(),
+ };
+
+ const defaultProps = {
+ searchSortOptions: MOCK_SORT_OPTIONS,
+ };
+
+ const createComponent = (initialState, props) => {
+ const store = new Vuex.Store({
+ state: {
+ query: MOCK_QUERY,
+ ...initialState,
+ },
+ actions: actionSpies,
+ });
+
+ wrapper = shallowMount(GlobalSearchSort, {
+ localVue,
+ store,
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findSortButtonGroup = () => wrapper.find(GlButtonGroup);
+ const findSortDropdown = () => wrapper.find(GlDropdown);
+ const findSortDirectionButton = () => wrapper.find(GlButton);
+ const findDropdownItems = () => findSortDropdown().findAll(GlDropdownItem);
+ const findDropdownItemsText = () => findDropdownItems().wrappers.map((w) => w.text());
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders Sort Button Group', () => {
+ expect(findSortButtonGroup().exists()).toBe(true);
+ });
+
+ it('renders Sort Dropdown', () => {
+ expect(findSortDropdown().exists()).toBe(true);
+ });
+
+ it('renders Sort Direction Button', () => {
+ expect(findSortDirectionButton().exists()).toBe(true);
+ });
+ });
+
+ describe('Sort Dropdown Items', () => {
+ describe('renders', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('an instance for each namespace', () => {
+ expect(findDropdownItemsText()).toStrictEqual(
+ MOCK_SORT_OPTIONS.map((option) => option.title),
+ );
+ });
+ });
+
+ describe.each`
+ sortQuery | value
+ ${null} | ${MOCK_SORT_OPTIONS[0].title}
+ ${'asdf'} | ${MOCK_SORT_OPTIONS[0].title}
+ ${MOCK_SORT_OPTIONS[0].sortParam} | ${MOCK_SORT_OPTIONS[0].title}
+ ${MOCK_SORT_OPTIONS[1].sortParam.desc} | ${MOCK_SORT_OPTIONS[1].title}
+ ${MOCK_SORT_OPTIONS[1].sortParam.asc} | ${MOCK_SORT_OPTIONS[1].title}
+ `('selected', ({ sortQuery, value }) => {
+ describe(`when sort option is ${sortQuery}`, () => {
+ beforeEach(() => {
+ createComponent({ query: { sort: sortQuery } });
+ });
+
+ it('is set correctly', () => {
+ expect(findSortDropdown().attributes('text')).toBe(value);
+ });
+ });
+ });
+ });
+
+ describe.each`
+ description | sortQuery | sortUi | disabled
+ ${'non-sortable'} | ${MOCK_SORT_OPTIONS[0].sortParam} | ${SORT_DIRECTION_UI.disabled} | ${'true'}
+ ${'descending sortable'} | ${MOCK_SORT_OPTIONS[1].sortParam.desc} | ${SORT_DIRECTION_UI.desc} | ${undefined}
+ ${'ascending sortable'} | ${MOCK_SORT_OPTIONS[1].sortParam.asc} | ${SORT_DIRECTION_UI.asc} | ${undefined}
+ `('Sort Direction Button', ({ description, sortQuery, sortUi, disabled }) => {
+ describe(`when sort option is ${description}`, () => {
+ beforeEach(() => {
+ createComponent({ query: { sort: sortQuery } });
+ });
+
+ it('sets the UI correctly', () => {
+ expect(findSortDirectionButton().attributes('disabled')).toBe(disabled);
+ expect(findSortDirectionButton().attributes('title')).toBe(sortUi.tooltip);
+ expect(findSortDirectionButton().attributes('icon')).toBe(sortUi.icon);
+ });
+ });
+ });
+
+ describe('actions', () => {
+ describe.each`
+ description | index | value
+ ${'non-sortable'} | ${0} | ${MOCK_SORT_OPTIONS[0].sortParam}
+ ${'sortable'} | ${1} | ${MOCK_SORT_OPTIONS[1].sortParam.desc}
+ `('handleSortChange', ({ description, index, value }) => {
+ describe(`when clicking a ${description} option`, () => {
+ beforeEach(() => {
+ createComponent();
+ findDropdownItems().at(index).vm.$emit('click');
+ });
+
+ it('calls setQuery and applyQuery correctly', () => {
+ expect(actionSpies.setQuery).toHaveBeenCalledTimes(1);
+ expect(actionSpies.applyQuery).toHaveBeenCalledTimes(1);
+ expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
+ key: 'sort',
+ value,
+ });
+ });
+ });
+ });
+
+ describe.each`
+ description | sortQuery | value
+ ${'descending'} | ${MOCK_SORT_OPTIONS[1].sortParam.desc} | ${MOCK_SORT_OPTIONS[1].sortParam.asc}
+ ${'ascending'} | ${MOCK_SORT_OPTIONS[1].sortParam.asc} | ${MOCK_SORT_OPTIONS[1].sortParam.desc}
+ `('handleSortDirectionChange', ({ description, sortQuery, value }) => {
+ describe(`when toggling a ${description} option`, () => {
+ beforeEach(() => {
+ createComponent({ query: { sort: sortQuery } });
+ findSortDirectionButton().vm.$emit('click');
+ });
+
+ it('calls setQuery and applyQuery correctly', () => {
+ expect(actionSpies.setQuery).toHaveBeenCalledTimes(1);
+ expect(actionSpies.applyQuery).toHaveBeenCalledTimes(1);
+ expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
+ key: 'sort',
+ value,
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/search/topbar/components/app_spec.js b/spec/frontend/search/topbar/components/app_spec.js
new file mode 100644
index 00000000000..faf3629b444
--- /dev/null
+++ b/spec/frontend/search/topbar/components/app_spec.js
@@ -0,0 +1,113 @@
+import Vuex from 'vuex';
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import { GlForm, GlSearchBoxByType, GlButton } from '@gitlab/ui';
+import { MOCK_QUERY } from 'jest/search/mock_data';
+import GlobalSearchTopbar from '~/search/topbar/components/app.vue';
+import GroupFilter from '~/search/topbar/components/group_filter.vue';
+import ProjectFilter from '~/search/topbar/components/project_filter.vue';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('GlobalSearchTopbar', () => {
+ let wrapper;
+
+ const actionSpies = {
+ applyQuery: jest.fn(),
+ setQuery: jest.fn(),
+ };
+
+ const createComponent = (initialState) => {
+ const store = new Vuex.Store({
+ state: {
+ query: MOCK_QUERY,
+ ...initialState,
+ },
+ actions: actionSpies,
+ });
+
+ wrapper = shallowMount(GlobalSearchTopbar, {
+ localVue,
+ store,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findTopbarForm = () => wrapper.find(GlForm);
+ const findGlSearchBox = () => wrapper.find(GlSearchBoxByType);
+ const findGroupFilter = () => wrapper.find(GroupFilter);
+ const findProjectFilter = () => wrapper.find(ProjectFilter);
+ const findSearchButton = () => wrapper.find(GlButton);
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders Topbar Form always', () => {
+ expect(findTopbarForm().exists()).toBe(true);
+ });
+
+ describe('Search box', () => {
+ it('renders always', () => {
+ expect(findGlSearchBox().exists()).toBe(true);
+ });
+
+ describe('onSearch', () => {
+ const testSearch = 'test search';
+
+ beforeEach(() => {
+ findGlSearchBox().vm.$emit('input', testSearch);
+ });
+
+ it('calls setQuery when input event is fired from GlSearchBoxByType', () => {
+ expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
+ key: 'search',
+ value: testSearch,
+ });
+ });
+ });
+ });
+
+ describe.each`
+ snippets | showFilters
+ ${null} | ${true}
+ ${{ query: { snippets: '' } }} | ${true}
+ ${{ query: { snippets: false } }} | ${true}
+ ${{ query: { snippets: true } }} | ${false}
+ ${{ query: { snippets: 'false' } }} | ${true}
+ ${{ query: { snippets: 'true' } }} | ${false}
+ `('topbar filters', ({ snippets, showFilters }) => {
+ beforeEach(() => {
+ createComponent(snippets);
+ });
+
+ it(`does${showFilters ? '' : ' not'} render when snippets is ${JSON.stringify(
+ snippets,
+ )}`, () => {
+ expect(findGroupFilter().exists()).toBe(showFilters);
+ expect(findProjectFilter().exists()).toBe(showFilters);
+ });
+ });
+
+ it('renders SearchButton always', () => {
+ expect(findSearchButton().exists()).toBe(true);
+ });
+ });
+
+ describe('actions', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('clicking SearchButton calls applyQuery', () => {
+ findTopbarForm().vm.$emit('submit', { preventDefault: () => {} });
+
+ expect(actionSpies.applyQuery).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/search/topbar/components/project_filter_spec.js b/spec/frontend/search/topbar/components/project_filter_spec.js
index c1fc61d7e89..f2ac8f2689d 100644
--- a/spec/frontend/search/topbar/components/project_filter_spec.js
+++ b/spec/frontend/search/topbar/components/project_filter_spec.js
@@ -99,7 +99,7 @@ describe('ProjectFilter', () => {
it('calls setUrlParams with project id, group id, then calls visitUrl', () => {
expect(setUrlParams).toHaveBeenCalledWith({
- [GROUP_DATA.queryParam]: MOCK_PROJECT.namespace_id,
+ [GROUP_DATA.queryParam]: MOCK_PROJECT.namespace.id,
[PROJECT_DATA.queryParam]: MOCK_PROJECT.id,
});
expect(visitUrl).toHaveBeenCalled();
diff --git a/spec/frontend/search_settings/index_spec.js b/spec/frontend/search_settings/index_spec.js
index 122ee1251bb..1d56d054eea 100644
--- a/spec/frontend/search_settings/index_spec.js
+++ b/spec/frontend/search_settings/index_spec.js
@@ -1,36 +1,25 @@
-import $ from 'jquery';
-import { setHTMLFixture } from 'helpers/fixtures';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import initSearch from '~/search_settings';
-import { expandSection, closeSection } from '~/settings_panels';
+import mount from '~/search_settings/mount';
-jest.mock('~/settings_panels');
-
-describe('search_settings/index', () => {
- let app;
-
- beforeEach(() => {
- const el = document.createElement('div');
-
- setHTMLFixture('<div id="content-body"></div>');
-
- app = initSearch({ el });
- });
+jest.mock('~/search_settings/mount');
+describe('~/search_settings', () => {
afterEach(() => {
- app.$destroy();
+ resetHTMLFixture();
});
- it('calls settings_panel.onExpand when expand event is emitted', () => {
- const section = { name: 'section' };
- app.$refs.searchSettings.$emit('expand', section);
+ it('initializes search settings when js-search-settings-app is available', async () => {
+ setHTMLFixture('<div class="js-search-settings-app"></div>');
+
+ await initSearch();
- expect(expandSection).toHaveBeenCalledWith($(section));
+ expect(mount).toHaveBeenCalled();
});
- it('calls settings_panel.closeSection when collapse event is emitted', () => {
- const section = { name: 'section' };
- app.$refs.searchSettings.$emit('collapse', section);
+ it('does not initialize search settings when js-search-settings-app is unavailable', async () => {
+ await initSearch();
- expect(closeSection).toHaveBeenCalledWith($(section));
+ expect(mount).not.toHaveBeenCalled();
});
});
diff --git a/spec/frontend/search_settings/mount_spec.js b/spec/frontend/search_settings/mount_spec.js
new file mode 100644
index 00000000000..b35266e56ae
--- /dev/null
+++ b/spec/frontend/search_settings/mount_spec.js
@@ -0,0 +1,36 @@
+import $ from 'jquery';
+import { setHTMLFixture } from 'helpers/fixtures';
+import mount from '~/search_settings/mount';
+import { expandSection, closeSection } from '~/settings_panels';
+
+jest.mock('~/settings_panels');
+
+describe('search_settings/mount', () => {
+ let app;
+
+ beforeEach(() => {
+ const el = document.createElement('div');
+
+ setHTMLFixture('<div id="content-body"></div>');
+
+ app = mount({ el });
+ });
+
+ afterEach(() => {
+ app.$destroy();
+ });
+
+ it('calls settings_panel.onExpand when expand event is emitted', () => {
+ const section = { name: 'section' };
+ app.$refs.searchSettings.$emit('expand', section);
+
+ expect(expandSection).toHaveBeenCalledWith($(section));
+ });
+
+ it('calls settings_panel.closeSection when collapse event is emitted', () => {
+ const section = { name: 'section' };
+ app.$refs.searchSettings.$emit('collapse', section);
+
+ expect(closeSection).toHaveBeenCalledWith($(section));
+ });
+});
diff --git a/spec/frontend/search_spec.js b/spec/frontend/search_spec.js
deleted file mode 100644
index d234a7fccb9..00000000000
--- a/spec/frontend/search_spec.js
+++ /dev/null
@@ -1,23 +0,0 @@
-import setHighlightClass from 'ee_else_ce/search/highlight_blob_search_result';
-import Search from '~/pages/search/show/search';
-
-jest.mock('~/api');
-jest.mock('ee_else_ce/search/highlight_blob_search_result');
-
-describe('Search', () => {
- const fixturePath = 'search/show.html';
-
- preloadFixtures(fixturePath);
-
- describe('constructor side effects', () => {
- afterEach(() => {
- jest.restoreAllMocks();
- });
-
- it('highlights lines with search terms in blob search results', () => {
- new Search(); // eslint-disable-line no-new
-
- expect(setHighlightClass).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/serverless/components/environment_row_spec.js b/spec/frontend/serverless/components/environment_row_spec.js
index a59b4fdbb7b..944283136d0 100644
--- a/spec/frontend/serverless/components/environment_row_spec.js
+++ b/spec/frontend/serverless/components/environment_row_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import environmentRowComponent from '~/serverless/components/environment_row.vue';
-import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
import { translate } from '~/serverless/utils';
+import { mockServerlessFunctions, mockServerlessFunctionsDiffEnv } from '../mock_data';
const createComponent = (env, envName) =>
shallowMount(environmentRowComponent, {
diff --git a/spec/frontend/serverless/store/actions_spec.js b/spec/frontend/serverless/store/actions_spec.js
index 32e30a57d4b..41d99fcb26c 100644
--- a/spec/frontend/serverless/store/actions_spec.js
+++ b/spec/frontend/serverless/store/actions_spec.js
@@ -2,8 +2,8 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import statusCodes from '~/lib/utils/http_status';
import { fetchFunctions, fetchMetrics } from '~/serverless/store/actions';
-import { mockServerlessFunctions, mockMetrics } from '../mock_data';
import axios from '~/lib/utils/axios_utils';
+import { mockServerlessFunctions, mockMetrics } from '../mock_data';
import { adjustMetricQuery } from '../utils';
describe('ServerlessActions', () => {
diff --git a/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap b/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
index e295c587d70..846f45345e7 100644
--- a/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
+++ b/spec/frontend/sidebar/__snapshots__/todo_spec.js.snap
@@ -3,7 +3,7 @@
exports[`SidebarTodo template renders component container element with proper data attributes 1`] = `
<button
aria-label="Mark as done"
- class="btn btn-default btn-todo issuable-header-btn float-right"
+ class="gl-button btn btn-default btn-todo issuable-header-btn float-right"
data-issuable-id="1"
data-issuable-type="epic"
type="button"
diff --git a/spec/frontend/sidebar/assignees_realtime_spec.js b/spec/frontend/sidebar/assignees_realtime_spec.js
index 1c62c52dc67..a7e280b5ae1 100644
--- a/spec/frontend/sidebar/assignees_realtime_spec.js
+++ b/spec/frontend/sidebar/assignees_realtime_spec.js
@@ -2,8 +2,8 @@ import { shallowMount } from '@vue/test-utils';
import ActionCable from '@rails/actioncable';
import AssigneesRealtime from '~/sidebar/components/assignees/assignees_realtime.vue';
import SidebarMediator from '~/sidebar/sidebar_mediator';
-import Mock from './mock_data';
import query from '~/issuable_sidebar/queries/issue_sidebar.query.graphql';
+import Mock from './mock_data';
jest.mock('@rails/actioncable', () => {
const mockConsumer = {
diff --git a/spec/frontend/sidebar/assignees_spec.js b/spec/frontend/sidebar/assignees_spec.js
index 23e82171fe9..fbf5fca2234 100644
--- a/spec/frontend/sidebar/assignees_spec.js
+++ b/spec/frontend/sidebar/assignees_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
-import { trimText } from 'helpers/text_helper';
import { GlIcon } from '@gitlab/ui';
+import { trimText } from 'helpers/text_helper';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import Assignee from '~/sidebar/components/assignees/assignees.vue';
import UsersMock from './mock_data';
diff --git a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
index 0b6a2e6ceb9..d52a59bb67b 100644
--- a/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/time_tracker_spec.js
@@ -1,5 +1,5 @@
-import { createMockDirective } from 'helpers/vue_mock_directive';
import { mount } from '@vue/test-utils';
+import { createMockDirective } from 'helpers/vue_mock_directive';
import { stubTransition } from 'helpers/stub_transition';
import TimeTracker from '~/sidebar/components/time_tracking/time_tracker.vue';
diff --git a/spec/frontend/sidebar/reviewers_spec.js b/spec/frontend/sidebar/reviewers_spec.js
index 91f28e85f3b..9208fb7288b 100644
--- a/spec/frontend/sidebar/reviewers_spec.js
+++ b/spec/frontend/sidebar/reviewers_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
-import { trimText } from 'helpers/text_helper';
import { GlIcon } from '@gitlab/ui';
+import { trimText } from 'helpers/text_helper';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import Reviewer from '~/sidebar/components/reviewers/reviewers.vue';
import UsersMock from './mock_data';
@@ -114,8 +114,7 @@ describe('Reviewer component', () => {
editable: true,
});
- expect(wrapper.findAll('.user-item').length).toBe(users.length);
- expect(wrapper.find('.user-list-more').exists()).toBe(false);
+ expect(wrapper.findAll('[data-testid="reviewer"]').length).toBe(users.length);
});
it('shows sorted reviewer where "can merge" users are sorted first', () => {
@@ -144,10 +143,10 @@ describe('Reviewer component', () => {
users,
});
- const userItems = wrapper.findAll('.user-list .user-item a');
+ const userItems = wrapper.findAll('[data-testid="reviewer"]');
expect(userItems.length).toBe(3);
- expect(userItems.at(0).attributes('title')).toBe(users[2].name);
+ expect(userItems.at(0).find('a').attributes('title')).toBe(users[2].name);
});
it('passes the sorted reviewers to the collapsed-reviewer-list', () => {
diff --git a/spec/frontend/sidebar/subscriptions_spec.js b/spec/frontend/sidebar/subscriptions_spec.js
index 043ffd972da..e7ae59e26cf 100644
--- a/spec/frontend/sidebar/subscriptions_spec.js
+++ b/spec/frontend/sidebar/subscriptions_spec.js
@@ -1,17 +1,20 @@
+import { GlToggle } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import Subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
import eventHub from '~/sidebar/event_hub';
-import ToggleButton from '~/vue_shared/components/toggle_button.vue';
describe('Subscriptions', () => {
let wrapper;
- const findToggleButton = () => wrapper.find(ToggleButton);
+ const findToggleButton = () => wrapper.findComponent(GlToggle);
const mountComponent = (propsData) =>
- shallowMount(Subscriptions, {
- propsData,
- });
+ extendedWrapper(
+ shallowMount(Subscriptions, {
+ propsData,
+ }),
+ );
afterEach(() => {
wrapper.destroy();
@@ -24,7 +27,7 @@ describe('Subscriptions', () => {
subscribed: undefined,
});
- expect(findToggleButton().attributes('isloading')).toBe('true');
+ expect(findToggleButton().props('isLoading')).toBe(true);
});
it('is toggled "off" when currently not subscribed', () => {
@@ -32,7 +35,7 @@ describe('Subscriptions', () => {
subscribed: false,
});
- expect(findToggleButton().attributes('value')).toBeFalsy();
+ expect(findToggleButton().props('value')).toBe(false);
});
it('is toggled "on" when currently subscribed', () => {
@@ -40,7 +43,7 @@ describe('Subscriptions', () => {
subscribed: true,
});
- expect(findToggleButton().attributes('value')).toBe('true');
+ expect(findToggleButton().props('value')).toBe(true);
});
it('toggleSubscription method emits `toggleSubscription` event on eventHub and Component', () => {
@@ -93,14 +96,16 @@ describe('Subscriptions', () => {
});
it('sets the correct display text', () => {
- expect(wrapper.find('.issuable-header-text').text()).toContain(subscribeDisabledDescription);
+ expect(wrapper.findByTestId('subscription-title').text()).toContain(
+ subscribeDisabledDescription,
+ );
expect(wrapper.find({ ref: 'tooltip' }).attributes('title')).toBe(
subscribeDisabledDescription,
);
});
it('does not render the toggle button', () => {
- expect(wrapper.find('.js-issuable-subscribe-button').exists()).toBe(false);
+ expect(findToggleButton().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/sidebar/todo_spec.js b/spec/frontend/sidebar/todo_spec.js
index 4adfaf7ad7b..7b7e24f2a8f 100644
--- a/spec/frontend/sidebar/todo_spec.js
+++ b/spec/frontend/sidebar/todo_spec.js
@@ -26,7 +26,7 @@ describe('SidebarTodo', () => {
it.each`
state | classes
- ${false} | ${['btn', 'btn-default', 'btn-todo', 'issuable-header-btn', 'float-right']}
+ ${false} | ${['gl-button', 'btn', 'btn-default', 'btn-todo', 'issuable-header-btn', 'float-right']}
${true} | ${['btn-blank', 'btn-todo', 'sidebar-collapsed-icon', 'dont-change-state']}
`('returns todo button classes for when `collapsed` prop is `$state`', ({ state, classes }) => {
createComponent({ collapsed: state });
@@ -35,7 +35,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/snippets/components/show_spec.js b/spec/frontend/snippets/components/show_spec.js
index b5ab7def753..6dbf7cfc4b4 100644
--- a/spec/frontend/snippets/components/show_spec.js
+++ b/spec/frontend/snippets/components/show_spec.js
@@ -1,6 +1,6 @@
import { GlLoadingIcon } from '@gitlab/ui';
-import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import { shallowMount } from '@vue/test-utils';
+import { Blob, BinaryBlob } from 'jest/blob/components/mock_data';
import SnippetApp from '~/snippets/components/show.vue';
import EmbedDropdown from '~/snippets/components/embed_dropdown.vue';
import SnippetHeader from '~/snippets/components/snippet_header.vue';
diff --git a/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js b/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js
index c7d0abee05c..d80b28b91f8 100644
--- a/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js
+++ b/spec/frontend/static_site_editor/components/edit_meta_modal_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlModal } from '@gitlab/ui';
-import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import MockAdapter from 'axios-mock-adapter';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import axios from '~/lib/utils/axios_utils';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import EditMetaModal from '~/static_site_editor/components/edit_meta_modal.vue';
diff --git a/spec/frontend/static_site_editor/pages/success_spec.js b/spec/frontend/static_site_editor/pages/success_spec.js
index 3fc69dc4586..7d2d5edda84 100644
--- a/spec/frontend/static_site_editor/pages/success_spec.js
+++ b/spec/frontend/static_site_editor/pages/success_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
import { GlButton, GlEmptyState, GlLoadingIcon } from '@gitlab/ui';
import Success from '~/static_site_editor/pages/success.vue';
-import { savedContentMeta, returnUrl, sourcePath } from '../mock_data';
import { HOME_ROUTE } from '~/static_site_editor/router/constants';
+import { savedContentMeta, returnUrl, sourcePath } from '../mock_data';
describe('~/static_site_editor/pages/success.vue', () => {
const mergeRequestsIllustrationPath = 'illustrations/merge_requests.svg';
diff --git a/spec/frontend/static_site_editor/services/front_matterify_spec.js b/spec/frontend/static_site_editor/services/front_matterify_spec.js
index 866897f21ef..ec3752b30c6 100644
--- a/spec/frontend/static_site_editor/services/front_matterify_spec.js
+++ b/spec/frontend/static_site_editor/services/front_matterify_spec.js
@@ -1,3 +1,4 @@
+import { frontMatterify, stringify } from '~/static_site_editor/services/front_matterify';
import {
sourceContentYAML as content,
sourceContentHeaderObjYAML as yamlFrontMatterObj,
@@ -5,8 +6,6 @@ import {
sourceContentBody as body,
} from '../mock_data';
-import { frontMatterify, stringify } from '~/static_site_editor/services/front_matterify';
-
describe('static_site_editor/services/front_matterify', () => {
const frontMatterifiedContent = {
source: content,
diff --git a/spec/frontend/static_site_editor/services/parse_source_file_spec.js b/spec/frontend/static_site_editor/services/parse_source_file_spec.js
index ab9e63f4cd2..fdd11297e09 100644
--- a/spec/frontend/static_site_editor/services/parse_source_file_spec.js
+++ b/spec/frontend/static_site_editor/services/parse_source_file_spec.js
@@ -1,3 +1,4 @@
+import parseSourceFile from '~/static_site_editor/services/parse_source_file';
import {
sourceContentYAML as content,
sourceContentHeaderYAML as yamlFrontMatter,
@@ -5,8 +6,6 @@ import {
sourceContentBody as body,
} from '../mock_data';
-import parseSourceFile from '~/static_site_editor/services/parse_source_file';
-
describe('static_site_editor/services/parse_source_file', () => {
const contentComplex = [content, content, content].join('');
const complexBody = [body, content, content].join('');
diff --git a/spec/frontend/terraform/components/states_table_actions_spec.js b/spec/frontend/terraform/components/states_table_actions_spec.js
index 3f5df8a96f8..6100b1f67a2 100644
--- a/spec/frontend/terraform/components/states_table_actions_spec.js
+++ b/spec/frontend/terraform/components/states_table_actions_spec.js
@@ -1,7 +1,8 @@
import { GlDropdown, GlModal, GlSprintf } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
-import createMockApollo from 'helpers/mock_apollo_helper';
import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import StateActions from '~/terraform/components/states_table_actions.vue';
import lockStateMutation from '~/terraform/graphql/mutations/lock_state.mutation.graphql';
import removeStateMutation from '~/terraform/graphql/mutations/remove_state.mutation.graphql';
@@ -14,6 +15,7 @@ describe('StatesTableActions', () => {
let lockResponse;
let removeResponse;
let unlockResponse;
+ let updateStateResponse;
let wrapper;
const defaultProps = {
@@ -26,7 +28,9 @@ describe('StatesTableActions', () => {
};
const createMockApolloProvider = () => {
- lockResponse = jest.fn().mockResolvedValue({ data: { terraformStateLock: { errors: [] } } });
+ lockResponse = jest
+ .fn()
+ .mockResolvedValue({ data: { terraformStateLock: { errors: ['There was an error'] } } });
removeResponse = jest
.fn()
@@ -36,11 +40,20 @@ describe('StatesTableActions', () => {
.fn()
.mockResolvedValue({ data: { terraformStateUnlock: { errors: [] } } });
- return createMockApollo([
- [lockStateMutation, lockResponse],
- [removeStateMutation, removeResponse],
- [unlockStateMutation, unlockResponse],
- ]);
+ updateStateResponse = jest.fn().mockResolvedValue({});
+
+ return createMockApollo(
+ [
+ [lockStateMutation, lockResponse],
+ [removeStateMutation, removeResponse],
+ [unlockStateMutation, unlockResponse],
+ ],
+ {
+ Mutation: {
+ addDataToTerraformState: updateStateResponse,
+ },
+ },
+ );
};
const createComponent = (propsData = defaultProps) => {
@@ -56,6 +69,7 @@ describe('StatesTableActions', () => {
return wrapper.vm.$nextTick();
};
+ const findActionsDropdown = () => wrapper.find(GlDropdown);
const findLockBtn = () => wrapper.find('[data-testid="terraform-state-lock"]');
const findUnlockBtn = () => wrapper.find('[data-testid="terraform-state-unlock"]');
const findDownloadBtn = () => wrapper.find('[data-testid="terraform-state-download"]');
@@ -70,9 +84,25 @@ describe('StatesTableActions', () => {
lockResponse = null;
removeResponse = null;
unlockResponse = null;
+ updateStateResponse = null;
wrapper.destroy();
});
+ describe('when the state is loading', () => {
+ beforeEach(() => {
+ return createComponent({
+ state: {
+ ...defaultProps.state,
+ loadingActions: true,
+ },
+ });
+ });
+
+ it('disables the actions dropdown', () => {
+ expect(findActionsDropdown().props('disabled')).toBe(true);
+ });
+ });
+
describe('download button', () => {
it('displays a download button', () => {
expect(findDownloadBtn().text()).toBe('Download JSON');
@@ -104,7 +134,8 @@ describe('StatesTableActions', () => {
describe('when clicking the unlock button', () => {
beforeEach(() => {
findUnlockBtn().vm.$emit('click');
- return wrapper.vm.$nextTick();
+
+ return waitForPromises();
});
it('calls the unlock mutation', () => {
@@ -137,7 +168,8 @@ describe('StatesTableActions', () => {
describe('when clicking the lock button', () => {
beforeEach(() => {
findLockBtn().vm.$emit('click');
- return wrapper.vm.$nextTick();
+
+ return waitForPromises();
});
it('calls the lock mutation', () => {
@@ -145,6 +177,42 @@ describe('StatesTableActions', () => {
stateID: unlockedProps.state.id,
});
});
+
+ it('calls mutations to set loading and errors', () => {
+ // loading update
+ expect(updateStateResponse).toHaveBeenNthCalledWith(
+ 1,
+ {},
+ {
+ terraformState: {
+ ...unlockedProps.state,
+ _showDetails: false,
+ errorMessages: [],
+ loadingActions: true,
+ },
+ },
+ // Apollo fields
+ expect.any(Object),
+ expect.any(Object),
+ );
+
+ // final update
+ expect(updateStateResponse).toHaveBeenNthCalledWith(
+ 2,
+ {},
+ {
+ terraformState: {
+ ...unlockedProps.state,
+ _showDetails: true,
+ errorMessages: ['There was an error'],
+ loadingActions: false,
+ },
+ },
+ // Apollo fields
+ expect.any(Object),
+ expect.any(Object),
+ );
+ });
});
});
@@ -156,7 +224,8 @@ describe('StatesTableActions', () => {
describe('when clicking the remove button', () => {
beforeEach(() => {
findRemoveBtn().vm.$emit('click');
- return wrapper.vm.$nextTick();
+
+ return waitForPromises();
});
it('displays a remove modal', () => {
diff --git a/spec/frontend/terraform/components/states_table_spec.js b/spec/frontend/terraform/components/states_table_spec.js
index f2b7bc00e5b..ba676c9741e 100644
--- a/spec/frontend/terraform/components/states_table_spec.js
+++ b/spec/frontend/terraform/components/states_table_spec.js
@@ -11,6 +11,8 @@ describe('StatesTable', () => {
const defaultProps = {
states: [
{
+ _showDetails: true,
+ errorMessages: ['State 1 has errored'],
name: 'state-1',
lockedAt: '2020-10-13T00:00:00Z',
lockedByUser: {
@@ -20,6 +22,8 @@ describe('StatesTable', () => {
latestVersion: null,
},
{
+ _showDetails: false,
+ errorMessages: [],
name: 'state-2',
lockedAt: null,
lockedByUser: null,
@@ -27,6 +31,8 @@ describe('StatesTable', () => {
latestVersion: null,
},
{
+ _showDetails: false,
+ errorMessages: [],
name: 'state-3',
lockedAt: '2020-10-10T00:00:00Z',
lockedByUser: {
@@ -54,6 +60,8 @@ describe('StatesTable', () => {
},
},
{
+ _showDetails: true,
+ errorMessages: ['State 4 has errored'],
name: 'state-4',
lockedAt: '2020-10-10T00:00:00Z',
lockedByUser: null,
@@ -154,6 +162,17 @@ describe('StatesTable', () => {
expect(findActions().length).toEqual(0);
});
+ it.each`
+ errorMessage | lineNumber
+ ${defaultProps.states[0].errorMessages[0]} | ${0}
+ ${defaultProps.states[3].errorMessages[0]} | ${1}
+ `('displays table error message "$errorMessage"', ({ errorMessage, lineNumber }) => {
+ const states = wrapper.findAll('[data-testid="terraform-states-table-error"]');
+ const state = states.at(lineNumber);
+
+ expect(state.text()).toBe(errorMessage);
+ });
+
describe('when user is a terraform administrator', () => {
beforeEach(() => {
return createComponent({
diff --git a/spec/frontend/terraform/components/terraform_list_spec.js b/spec/frontend/terraform/components/terraform_list_spec.js
index fb56a7135a3..20d42ebd4fd 100644
--- a/spec/frontend/terraform/components/terraform_list_spec.js
+++ b/spec/frontend/terraform/components/terraform_list_spec.js
@@ -1,7 +1,7 @@
import { GlAlert, GlBadge, GlKeysetPagination, GlLoadingIcon, GlTab } from '@gitlab/ui';
import { createLocalVue, shallowMount } from '@vue/test-utils';
-import createMockApollo from 'helpers/mock_apollo_helper';
import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import EmptyState from '~/terraform/components/empty_state.vue';
import StatesTable from '~/terraform/components/states_table.vue';
import TerraformList from '~/terraform/components/terraform_list.vue';
@@ -27,6 +27,15 @@ describe('TerraformList', () => {
},
};
+ // Override @client _showDetails
+ getStatesQuery.getStates.definitions[1].selectionSet.selections[0].directives = [];
+
+ // Override @client errorMessages
+ getStatesQuery.getStates.definitions[1].selectionSet.selections[1].directives = [];
+
+ // Override @client loadingActions
+ getStatesQuery.getStates.definitions[1].selectionSet.selections[2].directives = [];
+
const statsQueryResponse = queryResponse || jest.fn().mockResolvedValue(apolloQueryResponse);
const apolloProvider = createMockApollo([[getStatesQuery, statsQueryResponse]]);
@@ -52,20 +61,26 @@ describe('TerraformList', () => {
describe('when there is a list of terraform states', () => {
const states = [
{
+ _showDetails: false,
+ errorMessages: [],
id: 'gid://gitlab/Terraform::State/1',
name: 'state-1',
+ latestVersion: null,
+ loadingActions: false,
lockedAt: null,
- updatedAt: null,
lockedByUser: null,
- latestVersion: null,
+ updatedAt: null,
},
{
+ _showDetails: false,
+ errorMessages: [],
id: 'gid://gitlab/Terraform::State/2',
name: 'state-2',
+ latestVersion: null,
+ loadingActions: false,
lockedAt: null,
- updatedAt: null,
lockedByUser: null,
- latestVersion: null,
+ updatedAt: null,
},
];
diff --git a/spec/frontend/user_lists/components/edit_user_list_spec.js b/spec/frontend/user_lists/components/edit_user_list_spec.js
index 958e86ac050..4149a43e06b 100644
--- a/spec/frontend/user_lists/components/edit_user_list_spec.js
+++ b/spec/frontend/user_lists/components/edit_user_list_spec.js
@@ -7,8 +7,8 @@ import Api from '~/api';
import createStore from '~/user_lists/store/edit';
import EditUserList from '~/user_lists/components/edit_user_list.vue';
import UserListForm from '~/user_lists/components/user_list_form.vue';
-import { userList } from '../../feature_flags/mock_data';
import { redirectTo } from '~/lib/utils/url_utility';
+import { userList } from '../../feature_flags/mock_data';
jest.mock('~/api');
jest.mock('~/lib/utils/url_utility');
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 7ff8d9678fe..bc192a7d9d8 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
@@ -3,8 +3,8 @@ 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 axios from '~/lib/utils/axios_utils';
+import { mockStore } from '../mock_data';
describe('MrWidgetPipelineContainer', () => {
let wrapper;
diff --git a/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js
index 6c3b4a01659..c25e10c5249 100644
--- a/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js
+++ b/spec/frontend/vue_mr_widget/components/mr_widget_status_icon_spec.js
@@ -1,48 +1,60 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, mount } from '@vue/test-utils';
import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
describe('MR widget status icon component', () => {
- let vm;
- let Component;
+ let wrapper;
- beforeEach(() => {
- Component = Vue.extend(mrStatusIcon);
- });
+ const findLoadingIcon = () => wrapper.find(GlLoadingIcon);
+ const findDisabledMergeButton = () => wrapper.find('[data-testid="disabled-merge-button"]');
+
+ const createWrapper = (props, mountFn = shallowMount) => {
+ wrapper = mountFn(mrStatusIcon, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
describe('while loading', () => {
it('renders loading icon', () => {
- vm = mountComponent(Component, { status: 'loading' });
+ createWrapper({ status: 'loading' });
- expect(vm.$el.querySelector('.mr-widget-icon span').classList).toContain('gl-spinner');
+ expect(findLoadingIcon().exists()).toBe(true);
});
});
describe('with status icon', () => {
- it('renders ci status icon', () => {
- vm = mountComponent(Component, { status: 'failed' });
+ it('renders success status icon', () => {
+ createWrapper({ status: 'success' }, mount);
+
+ expect(wrapper.find('[data-testid="status_success-icon"]').exists()).toBe(true);
+ });
+
+ it('renders failed status icon', () => {
+ createWrapper({ status: 'failed' }, mount);
- expect(vm.$el.querySelector('.js-ci-status-icon-failed')).not.toBeNull();
+ expect(wrapper.find('[data-testid="status_failed-icon"]').exists()).toBe(true);
});
});
describe('with disabled button', () => {
it('renders a disabled button', () => {
- vm = mountComponent(Component, { status: 'failed', showDisabledButton: true });
+ createWrapper({ status: 'failed', showDisabledButton: true });
- expect(vm.$el.querySelector('.js-disabled-merge-button').textContent.trim()).toEqual('Merge');
+ expect(findDisabledMergeButton().exists()).toBe(true);
});
});
describe('without disabled button', () => {
it('does not render a disabled button', () => {
- vm = mountComponent(Component, { status: 'failed' });
+ createWrapper({ status: 'failed' });
- expect(vm.$el.querySelector('.js-disabled-merge-button')).toBeNull();
+ expect(findDisabledMergeButton().exists()).toBe(false);
});
});
});
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 8fcc982ac99..6ce6253c74d 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,11 +1,10 @@
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 { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
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 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,
@@ -15,6 +14,7 @@ import {
SP_SHOW_TRACK_VALUE,
SP_HELP_URL,
} from '~/vue_merge_request_widget/constants';
+import { suggestProps, iconName } from './pipeline_tour_mock_data';
describe('MRWidgetSuggestPipeline', () => {
describe('template', () => {
diff --git a/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap
new file mode 100644
index 00000000000..4c3c7fbea36
--- /dev/null
+++ b/spec/frontend/vue_mr_widget/components/states/__snapshots__/mr_widget_pipeline_failed_spec.js.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`PipelineFailed should render error message with a disabled merge button 1`] = `
+<div
+ class="mr-widget-body media"
+>
+ <status-icon-stub
+ showdisabledbutton="true"
+ status="warning"
+ />
+
+ <div
+ class="media-body space-children"
+ >
+ <span
+ class="bold"
+ >
+ <gl-sprintf-stub
+ message="The pipeline for this merge request did not complete. Push a new commit to fix the failure or check the %{linkStart}troubleshooting documentation%{linkEnd} to see other possible actions."
+ />
+ </span>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index 850bbd93df5..d309aa905e8 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -202,7 +202,11 @@ describe('MRWidgetAutoMergeEnabled', () => {
wrapper.vm.cancelAutomaticMerge();
setImmediate(() => {
expect(wrapper.vm.isCancellingAutoMerge).toBeTruthy();
- expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
+ if (mergeRequestWidgetGraphql) {
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ } else {
+ expect(eventHub.$emit).toHaveBeenCalledWith('UpdateWidgetData', mrObj);
+ }
done();
});
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
index 48c1a9eedf9..c1471314c4a 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -54,7 +54,7 @@ describe('MRWidgetFailedToMerge', () => {
Vue.nextTick()
.then(() => {
- expect(vm.mergeError).toBe('contains line breaks');
+ expect(vm.mergeError).toBe('contains line breaks.');
})
.then(done)
.catch(done.fail);
@@ -113,14 +113,14 @@ describe('MRWidgetFailedToMerge', () => {
describe('while it is not regresing', () => {
it('renders warning icon and disabled merge button', () => {
expect(vm.$el.querySelector('.js-ci-status-icon-warning')).not.toBeNull();
- expect(vm.$el.querySelector('.js-disabled-merge-button').getAttribute('disabled')).toEqual(
- 'disabled',
- );
+ expect(
+ vm.$el.querySelector('[data-testid="disabled-merge-button"]').getAttribute('disabled'),
+ ).toEqual('disabled');
});
it('renders given error', () => {
expect(vm.$el.querySelector('.has-error-message').textContent.trim()).toEqual(
- 'Merge error happened',
+ 'Merge error happened.',
);
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 36c4174c03d..cdb80b30d36 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -1,7 +1,10 @@
import Vue from 'vue';
+import { getByRole } from '@testing-library/dom';
import mountComponent from 'helpers/vue_mount_component_helper';
import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
+import modalEventHub from '~/projects/commit/event_hub';
+import { OPEN_REVERT_MODAL } from '~/projects/commit/constants';
describe('MRWidgetMerged', () => {
let vm;
@@ -16,6 +19,7 @@ describe('MRWidgetMerged', () => {
};
beforeEach(() => {
+ jest.spyOn(document, 'dispatchEvent');
const Component = Vue.extend(mergedComponent);
const mr = {
isRemovingSourceBranch: false,
@@ -147,6 +151,18 @@ describe('MRWidgetMerged', () => {
});
});
+ it('calls dispatchDocumentEvent to load in the modal component', () => {
+ expect(document.dispatchEvent).toHaveBeenCalledWith(new CustomEvent('merged:UpdateActions'));
+ });
+
+ it('emits event to open the revert modal on revert button click', () => {
+ const eventHubSpy = jest.spyOn(modalEventHub, '$emit');
+
+ getByRole(vm.$el, 'button', { name: /Revert/i }).click();
+
+ expect(eventHubSpy).toHaveBeenCalledWith(OPEN_REVERT_MODAL);
+ });
+
it('has merged by information', () => {
expect(vm.$el.textContent).toContain('Merged by');
expect(vm.$el.textContent).toContain('Administrator');
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
index 8847e4e6bdd..bd77a1d657e 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -1,25 +1,27 @@
-import Vue from 'vue';
-import mountComponent from 'helpers/vue_mount_component_helper';
-import { removeBreakLine } from 'helpers/text_helper';
-import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
+import { shallowMount, mount } from '@vue/test-utils';
+import PipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
describe('MRWidgetPipelineBlocked', () => {
- let vm;
- beforeEach(() => {
- const Component = Vue.extend(pipelineBlockedComponent);
- vm = mountComponent(Component);
- });
+ let wrapper;
+
+ const createWrapper = (mountFn = shallowMount) => {
+ wrapper = mountFn(PipelineBlockedComponent);
+ };
afterEach(() => {
- vm.$destroy();
+ wrapper.destroy();
});
it('renders warning icon', () => {
- expect(vm.$el.querySelector('.ci-status-icon-warning')).not.toBe(null);
+ createWrapper(mount);
+
+ expect(wrapper.find('.ci-status-icon-warning').exists()).toBe(true);
});
it('renders information text', () => {
- expect(removeBreakLine(vm.$el.textContent).trim()).toContain(
+ createWrapper();
+
+ expect(wrapper.text()).toBe(
'Pipeline blocked. The pipeline for this merge request requires a manual action to proceed',
);
});
diff --git a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
index 179adef12d9..ed3e7d9c0eb 100644
--- a/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
+++ b/spec/frontend/vue_mr_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -1,19 +1,30 @@
-import Vue from 'vue';
-import { removeBreakLine } from 'helpers/text_helper';
+import { shallowMount } from '@vue/test-utils';
import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
+import statusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
describe('PipelineFailed', () => {
- describe('template', () => {
- const Component = Vue.extend(PipelineFailed);
- const vm = new Component({
- el: document.createElement('div'),
- });
- it('should have correct elements', () => {
- expect(vm.$el.classList.contains('mr-widget-body')).toBeTruthy();
- expect(vm.$el.querySelector('button').getAttribute('disabled')).toBeTruthy();
- expect(removeBreakLine(vm.$el.innerText).trim()).toContain(
- 'The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure',
- );
- });
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(PipelineFailed);
+ };
+
+ const findStatusIcon = () => wrapper.find(statusIcon);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should render error message with a disabled merge button', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('merge button should be disabled', () => {
+ expect(findStatusIcon().props('showDisabledButton')).toBe(true);
});
});
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 8da0d0f16d6..a9b852b084d 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,12 +1,12 @@
import { GlDeprecatedSkeletonLoading as GlSkeletonLoading, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-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';
import TerraformPlan from '~/vue_merge_request_widget/components/terraform/terraform_plan.vue';
+import { invalidPlanWithName, plans, validPlanWithName } from './mock_data';
describe('MrWidgetTerraformConainer', () => {
let mock;
diff --git a/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js b/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js
index ea4eb44ebfe..f95a92c2cb1 100644
--- a/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js
+++ b/spec/frontend/vue_mr_widget/components/terraform/terraform_plan_spec.js
@@ -33,7 +33,7 @@ describe('TerraformPlan', () => {
it('diplays the header text with a name', () => {
expect(wrapper.text()).toContain(
- `The Terraform report ${validPlanWithName.job_name} was generated in your pipelines.`,
+ `The report ${validPlanWithName.job_name} was generated in your pipelines.`,
);
});
@@ -55,7 +55,7 @@ describe('TerraformPlan', () => {
});
it('diplays the header text without a name', () => {
- expect(wrapper.text()).toContain('A Terraform report was generated in your pipelines.');
+ expect(wrapper.text()).toContain('A report was generated in your pipelines.');
});
});
@@ -70,7 +70,7 @@ describe('TerraformPlan', () => {
it('diplays the header text with a name', () => {
expect(wrapper.text()).toContain(
- `The Terraform report ${invalidPlanWithName.job_name} failed to generate.`,
+ `The report ${invalidPlanWithName.job_name} failed to generate.`,
);
});
@@ -85,7 +85,7 @@ describe('TerraformPlan', () => {
});
it('diplays the header text without a name', () => {
- expect(wrapper.text()).toContain('A Terraform report failed to generate.');
+ expect(wrapper.text()).toContain('A report failed to generate.');
});
it('does not render button because url is missing', () => {
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 1ea7fe1fbfe..872dcd2af7f 100644
--- a/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_mr_widget/mr_widget_options_spec.js
@@ -1,7 +1,9 @@
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
-import Api from '~/api';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { securityReportDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
import axios from '~/lib/utils/axios_utils';
import MrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
@@ -9,20 +11,16 @@ import notify from '~/lib/utils/notify';
import SmartInterval from '~/smart_interval';
import { setFaviconOverlay } from '~/lib/utils/favicon';
import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
-import mockData from './mock_data';
-import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
+import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/queries/security_report_download_paths.query.graphql';
+import { faviconDataUrl, overlayDataUrl } from '../lib/utils/mock_data';
+import mockData from './mock_data';
jest.mock('~/smart_interval');
jest.mock('~/lib/utils/favicon');
-const returnPromise = (data) =>
- new Promise((resolve) => {
- resolve({
- data,
- });
- });
+Vue.use(VueApollo);
describe('MrWidgetOptions', () => {
let wrapper;
@@ -48,7 +46,7 @@ describe('MrWidgetOptions', () => {
gon.features = {};
});
- const createComponent = (mrData = mockData) => {
+ const createComponent = (mrData = mockData, options = {}) => {
if (wrapper) {
wrapper.destroy();
}
@@ -57,6 +55,7 @@ describe('MrWidgetOptions', () => {
propsData: {
mrData: { ...mrData },
},
+ ...options,
});
return axios.waitForAll();
@@ -68,6 +67,7 @@ describe('MrWidgetOptions', () => {
describe('default', () => {
beforeEach(() => {
+ jest.spyOn(document, 'dispatchEvent');
return createComponent();
});
@@ -281,7 +281,7 @@ describe('MrWidgetOptions', () => {
let isCbExecuted;
beforeEach(() => {
- jest.spyOn(wrapper.vm.service, 'checkStatus').mockReturnValue(returnPromise(mockData));
+ jest.spyOn(wrapper.vm.service, 'checkStatus').mockResolvedValue({ data: mockData });
jest.spyOn(wrapper.vm.mr, 'setData').mockImplementation(() => {});
jest.spyOn(wrapper.vm, 'handleNotification').mockImplementation(() => {});
@@ -331,7 +331,7 @@ describe('MrWidgetOptions', () => {
it('should fetch deployments', () => {
jest
.spyOn(wrapper.vm.service, 'fetchDeployments')
- .mockReturnValue(returnPromise([{ id: 1, status: SUCCESS }]));
+ .mockResolvedValue({ data: [{ id: 1, status: SUCCESS }] });
wrapper.vm.fetchPreMergeDeployments();
@@ -347,13 +347,16 @@ describe('MrWidgetOptions', () => {
it('should fetch content of Cherry Pick and Revert modals', () => {
jest
.spyOn(wrapper.vm.service, 'fetchMergeActionsContent')
- .mockReturnValue(returnPromise('hello world'));
+ .mockResolvedValue({ data: 'hello world' });
wrapper.vm.fetchActionsContent();
return nextTick().then(() => {
expect(wrapper.vm.service.fetchMergeActionsContent).toHaveBeenCalled();
expect(document.body.textContent).toContain('hello world');
+ expect(document.dispatchEvent).toHaveBeenCalledWith(
+ new CustomEvent('merged:UpdateActions'),
+ );
});
});
});
@@ -822,36 +825,34 @@ describe('MrWidgetOptions', () => {
describe('security widget', () => {
describe.each`
- context | hasPipeline | reportType | isFlagEnabled | shouldRender
- ${'security report and flag enabled'} | ${true} | ${'sast'} | ${true} | ${true}
- ${'security report and flag disabled'} | ${true} | ${'sast'} | ${false} | ${false}
- ${'no security report and flag enabled'} | ${true} | ${'foo'} | ${true} | ${false}
- ${'no pipeline and flag enabled'} | ${false} | ${'sast'} | ${true} | ${false}
- `('given $context', ({ hasPipeline, reportType, isFlagEnabled, shouldRender }) => {
+ context | hasPipeline | shouldRender
+ ${'there is a pipeline'} | ${true} | ${true}
+ ${'no pipeline'} | ${false} | ${false}
+ `('given $context', ({ hasPipeline, shouldRender }) => {
beforeEach(() => {
- gon.features.coreSecurityMrWidget = isFlagEnabled;
+ const mrData = {
+ ...mockData,
+ ...(hasPipeline ? {} : { pipeline: null }),
+ };
- if (hasPipeline) {
- jest.spyOn(Api, 'pipelineJobs').mockResolvedValue({
- data: [{ artifacts: [{ file_type: reportType }] }],
- });
- }
+ // Override top-level mocked requests, which always use a fresh copy of
+ // mockData, which always includes the full pipeline object.
+ mock.onGet(mockData.merge_request_widget_path).reply(() => [200, mrData]);
+ mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, mrData]);
- return createComponent({
- ...mockData,
- ...(hasPipeline ? {} : { pipeline: undefined }),
+ return createComponent(mrData, {
+ apolloProvider: createMockApollo([
+ [
+ securityReportDownloadPathsQuery,
+ async () => ({ data: securityReportDownloadPathsQueryResponse }),
+ ],
+ ]),
});
});
- if (shouldRender) {
- it('renders', () => {
- expect(findSecurityMrWidget().exists()).toBe(true);
- });
- } else {
- it('does not render', () => {
- expect(findSecurityMrWidget().exists()).toBe(false);
- });
- }
+ it(shouldRender ? 'renders' : 'does not render', () => {
+ expect(findSecurityMrWidget().exists()).toBe(shouldRender);
+ });
});
});
diff --git a/spec/frontend/alert_management/components/alert_details_spec.js b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
index 976e50625a6..40af8e60008 100644
--- a/spec/frontend/alert_management/components/alert_details_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_details_spec.js
@@ -4,17 +4,14 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import AlertDetails from '~/alert_management/components/alert_details.vue';
-import AlertSummaryRow from '~/alert_management/components/alert_summary_row.vue';
-import {
- ALERTS_SEVERITY_LABELS,
- trackAlertsDetailsViewsOptions,
-} from '~/alert_management/constants';
-import createIssueMutation from '~/alert_management/graphql/mutations/create_issue_from_alert.mutation.graphql';
+import AlertDetails from '~/vue_shared/alert_details/components/alert_details.vue';
+import AlertSummaryRow from '~/vue_shared/alert_details/components/alert_summary_row.vue';
+import { SEVERITY_LEVELS } from '~/vue_shared/alert_details/constants';
+import createIssueMutation from '~/vue_shared/alert_details/graphql/mutations/alert_issue_create.mutation.graphql';
import { joinPaths } from '~/lib/utils/url_utility';
import Tracking from '~/tracking';
import AlertDetailsTable from '~/vue_shared/components/alert_details_table.vue';
-import mockAlerts from '../mocks/alerts.json';
+import mockAlerts from './mocks/alerts.json';
const mockAlert = mockAlerts[0];
const environmentName = 'Production';
@@ -29,7 +26,13 @@ describe('AlertDetails', () => {
const projectId = '1';
const $router = { replace: jest.fn() };
- function mountComponent({ data, loading = false, mountMethod = shallowMount, stubs = {} } = {}) {
+ function mountComponent({
+ data,
+ loading = false,
+ mountMethod = shallowMount,
+ provide = {},
+ stubs = {},
+ } = {}) {
wrapper = extendedWrapper(
mountMethod(AlertDetails, {
provide: {
@@ -37,6 +40,7 @@ describe('AlertDetails', () => {
projectPath,
projectIssuesPath,
projectId,
+ ...provide,
},
data() {
return {
@@ -112,9 +116,7 @@ describe('AlertDetails', () => {
});
it('renders severity', () => {
- expect(wrapper.findByTestId('severity').text()).toBe(
- ALERTS_SEVERITY_LABELS[mockAlert.severity],
- );
+ expect(wrapper.findByTestId('severity').text()).toBe(SEVERITY_LEVELS[mockAlert.severity]);
});
it('renders a title', () => {
@@ -321,16 +323,27 @@ describe('AlertDetails', () => {
});
describe('Snowplow tracking', () => {
+ const mountOptions = {
+ props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
+ data: { alert: mockAlert },
+ loading: false,
+ };
+
beforeEach(() => {
jest.spyOn(Tracking, 'event');
- mountComponent({
- props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alert: mockAlert },
- loading: false,
- });
});
- it('should track alert details page views', () => {
+ it('should not track alert details page views when the tracking options do not exist', () => {
+ mountComponent(mountOptions);
+ expect(Tracking.event).not.toHaveBeenCalled();
+ });
+
+ it('should track alert details page views when the tracking options exist', () => {
+ const trackAlertsDetailsViewsOptions = {
+ category: 'Alert Management',
+ action: 'view_alert_details',
+ };
+ mountComponent({ ...mountOptions, provide: { trackAlertsDetailsViewsOptions } });
const { category, action } = trackAlertsDetailsViewsOptions;
expect(Tracking.event).toHaveBeenCalledWith(category, action);
});
diff --git a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
index ea7b4584a63..e08955d5f8e 100644
--- a/spec/frontend/alert_management/components/alert_management_sidebar_todo_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_management_sidebar_todo_spec.js
@@ -1,8 +1,8 @@
import { mount } from '@vue/test-utils';
-import SidebarTodo from '~/alert_management/components/sidebar/sidebar_todo.vue';
-import createAlertTodoMutation from '~/alert_management/graphql/mutations/alert_todo_create.mutation.graphql';
+import SidebarTodo from '~/vue_shared/alert_details/components/sidebar/sidebar_todo.vue';
+import createAlertTodoMutation from '~/vue_shared/alert_details/graphql/mutations/alert_todo_create.mutation.graphql';
import todoMarkDoneMutation from '~/graphql_shared/mutations/todo_mark_done.mutation.graphql';
-import mockAlerts from '../mocks/alerts.json';
+import mockAlerts from './mocks/alerts.json';
const mockAlert = mockAlerts[0];
@@ -59,7 +59,7 @@ describe('Alert Details Sidebar To Do', () => {
it('renders a button for adding a To-Do', async () => {
await wrapper.vm.$nextTick();
- expect(findToDoButton().text()).toBe('Add a To-Do');
+ expect(findToDoButton().text()).toBe('Add a to do');
});
it('calls `$apollo.mutate` with `createAlertTodoMutation` mutation and variables containing `iid`, `todoEvent`, & `projectPath`', async () => {
diff --git a/spec/frontend/alert_management/components/alert_metrics_spec.js b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
index 42da8c3768b..c75909651d7 100644
--- a/spec/frontend/alert_management/components/alert_metrics_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_metrics_spec.js
@@ -1,8 +1,8 @@
import { shallowMount } from '@vue/test-utils';
-import waitForPromises from 'helpers/wait_for_promises';
import MockAdapter from 'axios-mock-adapter';
import axios from 'axios';
-import AlertMetrics from '~/alert_management/components/alert_metrics.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import AlertMetrics from '~/vue_shared/alert_details/components/alert_metrics.vue';
import MetricEmbed from '~/monitoring/components/embeds/metric_embed.vue';
jest.mock('~/monitoring/stores', () => ({
diff --git a/spec/frontend/alert_management/components/alert_status_spec.js b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
index 6f2ddb86020..b37518cc424 100644
--- a/spec/frontend/alert_management/components/alert_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
@@ -1,11 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
import waitForPromises from 'helpers/wait_for_promises';
-import { trackAlertStatusUpdateOptions } from '~/alert_management/constants';
-import AlertManagementStatus from '~/alert_management/components/alert_status.vue';
-import updateAlertStatusMutation from '~/graphql_shared/mutations/update_alert_status.mutation.graphql';
+import AlertManagementStatus from '~/vue_shared/alert_details/components/alert_status.vue';
+import updateAlertStatusMutation from '~/graphql_shared//mutations/alert_status_update.mutation.graphql';
import Tracking from '~/tracking';
-import mockAlerts from '../mocks/alerts.json';
+import mockAlerts from './mocks/alerts.json';
const mockAlert = mockAlerts[0];
@@ -20,7 +19,7 @@ describe('AlertManagementStatus', () => {
return waitForPromises();
};
- function mountComponent({ props = {}, loading = false, stubs = {} } = {}) {
+ function mountComponent({ props = {}, provide = {}, loading = false, stubs = {} } = {}) {
wrapper = shallowMount(AlertManagementStatus, {
propsData: {
alert: { ...mockAlert },
@@ -28,6 +27,7 @@ describe('AlertManagementStatus', () => {
isSidebar: false,
...props,
},
+ provide,
mocks: {
$apollo: {
mutate: jest.fn(),
@@ -134,10 +134,25 @@ describe('AlertManagementStatus', () => {
describe('Snowplow tracking', () => {
beforeEach(() => {
jest.spyOn(Tracking, 'event');
+ });
+
+ it('should not track alert status updates when the tracking options do not exist', () => {
mountComponent({});
+ Tracking.event.mockClear();
+ jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
+ findFirstStatusOption().vm.$emit('click');
+ setImmediate(() => {
+ expect(Tracking.event).not.toHaveBeenCalled();
+ });
});
- it('should track alert status updates', () => {
+ it('should track alert status updates when the tracking options exist', () => {
+ const trackAlertStatusUpdateOptions = {
+ category: 'Alert Management',
+ action: 'update_alert_status',
+ label: 'Status',
+ };
+ mountComponent({ provide: { trackAlertStatusUpdateOptions } });
Tracking.event.mockClear();
jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
findFirstStatusOption().vm.$emit('click');
diff --git a/spec/frontend/alert_management/components/alert_summary_row_spec.js b/spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js
index 47c715c089a..a2981478954 100644
--- a/spec/frontend/alert_management/components/alert_summary_row_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_summary_row_spec.js
@@ -1,5 +1,5 @@
import { shallowMount } from '@vue/test-utils';
-import AlertSummaryRow from '~/alert_management/components/alert_summary_row.vue';
+import AlertSummaryRow from '~/vue_shared/alert_details/components/alert_summary_row.vue';
const label = 'a label';
const value = 'a value';
diff --git a/spec/frontend/alert_management/mocks/alerts.json b/spec/frontend/vue_shared/alert_details/mocks/alerts.json
index 5267a4fe50d..5267a4fe50d 100644
--- a/spec/frontend/alert_management/mocks/alerts.json
+++ b/spec/frontend/vue_shared/alert_details/mocks/alerts.json
diff --git a/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_managment_sidebar_assignees_spec.js
index 00c479071fe..4c7f86a40ee 100644
--- a/spec/frontend/alert_management/components/sidebar/alert_managment_sidebar_assignees_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_managment_sidebar_assignees_spec.js
@@ -2,10 +2,10 @@ import { shallowMount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { GlDropdownItem } 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';
-import mockAlerts from '../../mocks/alerts.json';
+import SidebarAssignee from '~/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue';
+import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue';
+import AlertSetAssignees from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
+import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
diff --git a/spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
index 5235ae63fee..c2df37821d3 100644
--- a/spec/frontend/alert_management/components/sidebar/alert_sidebar_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_spec.js
@@ -1,9 +1,9 @@
import { shallowMount, mount } from '@vue/test-utils';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
-import AlertSidebar from '~/alert_management/components/alert_sidebar.vue';
-import SidebarAssignees from '~/alert_management/components/sidebar/sidebar_assignees.vue';
-import mockAlerts from '../../mocks/alerts.json';
+import AlertSidebar from '~/vue_shared/alert_details/components/alert_sidebar.vue';
+import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue';
+import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
diff --git a/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
index 0b60a36cf54..95c9dac5034 100644
--- a/spec/frontend/alert_management/components/sidebar/alert_sidebar_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_status_spec.js
@@ -1,10 +1,8 @@
import { mount } from '@vue/test-utils';
import { GlDropdown, GlDropdownItem, GlLoadingIcon } from '@gitlab/ui';
-import { trackAlertStatusUpdateOptions } from '~/alert_management/constants';
-import AlertSidebarStatus from '~/alert_management/components/sidebar/sidebar_status.vue';
-import updateAlertStatusMutation from '~/graphql_shared/mutations/update_alert_status.mutation.graphql';
-import Tracking from '~/tracking';
-import mockAlerts from '../../mocks/alerts.json';
+import AlertSidebarStatus from '~/vue_shared/alert_details/components/sidebar/sidebar_status.vue';
+import updateAlertStatusMutation from '~/graphql_shared/mutations/alert_status_update.mutation.graphql';
+import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[0];
@@ -101,30 +99,5 @@ describe('Alert Details Sidebar Status', () => {
expect(wrapper.find('[data-testid="status"]').text()).toBe('Triggered');
});
});
-
- describe('Snowplow tracking', () => {
- beforeEach(() => {
- jest.spyOn(Tracking, 'event');
- mountComponent({
- props: { alertManagementEnabled: true, userCanEnableAlertManagement: true },
- data: { alert: mockAlert },
- loading: false,
- });
- });
-
- it('should track alert status updates', () => {
- Tracking.event.mockClear();
- jest.spyOn(wrapper.vm.$apollo, 'mutate').mockResolvedValue({});
- findStatusDropdownItem().vm.$emit('click');
- const status = findStatusDropdownItem().text();
- setImmediate(() => {
- const { category, action, label } = trackAlertStatusUpdateOptions;
- expect(Tracking.event).toHaveBeenCalledWith(category, action, {
- label,
- property: status,
- });
- });
- });
- });
});
});
diff --git a/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js b/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js
index 65cfc600d76..90966482bb4 100644
--- a/spec/frontend/alert_management/components/system_notes/alert_management_system_note_spec.js
+++ b/spec/frontend/vue_shared/alert_details/system_notes/alert_management_system_note_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlIcon } from '@gitlab/ui';
-import SystemNote from '~/alert_management/components/system_notes/system_note.vue';
-import mockAlerts from '../../mocks/alerts.json';
+import SystemNote from '~/vue_shared/alert_details/components/system_notes/system_note.vue';
+import mockAlerts from '../mocks/alerts.json';
const mockAlert = mockAlerts[1];
diff --git a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
index c8fe6c3131c..d30f36ec63c 100644
--- a/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
+++ b/spec/frontend/vue_shared/components/color_picker/color_picker_spec.js
@@ -13,6 +13,7 @@ describe('ColorPicker', () => {
};
const setColor = '#000000';
+ const invalidText = 'Please enter a valid hex (#RRGGBB or #RGB) color value';
const label = () => wrapper.find(GlFormGroup).attributes('label');
const colorPreview = () => wrapper.find('[data-testid="color-preview"]');
const colorPicker = () => wrapper.find(GlFormInput);
@@ -28,8 +29,6 @@ describe('ColorPicker', () => {
'#428BCA': 'Moderate blue',
'#44AD8E': 'Lime green',
};
-
- createComponent(shallowMount);
});
afterEach(() => {
@@ -38,6 +37,8 @@ describe('ColorPicker', () => {
describe('label', () => {
it('hides the label if the label is not passed', () => {
+ createComponent(shallowMount);
+
expect(label()).toBe('');
});
@@ -55,43 +56,37 @@ describe('ColorPicker', () => {
expect(colorPreview().attributes('style')).toBe(undefined);
expect(colorPicker().attributes('value')).toBe(undefined);
expect(colorInput().props('value')).toBe('');
+ expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400');
});
it('has a color set on initialization', () => {
- createComponent(shallowMount, { setColor });
+ createComponent(mount, { value: setColor });
- expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ expect(colorInput().props('value')).toBe(setColor);
});
it('emits input event from component when a color is selected', async () => {
createComponent();
await colorInput().setValue(setColor);
- expect(wrapper.emitted().input[0]).toEqual([setColor]);
+ expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
it('trims spaces from submitted colors', async () => {
createComponent();
await colorInput().setValue(` ${setColor} `);
- expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
+ expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-gray-400');
+ expect(colorInput().attributes('class')).not.toContain('is-invalid');
});
- it('shows invalid feedback when an invalid color is used', async () => {
- createComponent();
- await colorInput().setValue('abcd');
-
- expect(invalidFeedback().text()).toBe(
- 'Please enter a valid hex (#RRGGBB or #RGB) color value',
- );
- expect(wrapper.emitted().input).toBe(undefined);
- });
-
- it('shows an invalid feedback border on the preview when an invalid color is used', async () => {
- createComponent();
- await colorInput().setValue('abcd');
+ it('shows invalid feedback when the state is marked as invalid', async () => {
+ createComponent(mount, { invalidFeedback: invalidText, state: false });
+ expect(invalidFeedback().text()).toBe(invalidText);
expect(colorPreview().attributes('class')).toContain('gl-inset-border-1-red-500');
+ expect(colorInput().attributes('class')).toContain('is-invalid');
});
});
@@ -100,14 +95,14 @@ describe('ColorPicker', () => {
createComponent();
await colorInput().setValue(setColor);
- expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
it('has color picker value entered', async () => {
createComponent();
await colorPicker().setValue(setColor);
- expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
});
@@ -132,7 +127,7 @@ describe('ColorPicker', () => {
createComponent();
await presetColors().at(0).trigger('click');
- expect(wrapper.vm.$data.selectedColor).toBe(setColor);
+ expect(wrapper.emitted().input[0]).toStrictEqual([setColor]);
});
});
});
diff --git a/spec/frontend/vue_shared/components/editor_lite_spec.js b/spec/frontend/vue_shared/components/editor_lite_spec.js
index 70fdd8e24a5..9298851d143 100644
--- a/spec/frontend/vue_shared/components/editor_lite_spec.js
+++ b/spec/frontend/vue_shared/components/editor_lite_spec.js
@@ -2,6 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import EditorLite from '~/vue_shared/components/editor_lite.vue';
import Editor from '~/editor/editor_lite';
+import { EDITOR_READY_EVENT } from '~/editor/constants';
jest.mock('~/editor/editor_lite');
@@ -110,13 +111,13 @@ describe('Editor Lite component', () => {
expect(wrapper.emitted().input).toEqual([[value]]);
});
- it('emits editor-ready event when the Editor Lite is ready', async () => {
+ it('emits EDITOR_READY_EVENT event when the Editor Lite is ready', async () => {
const el = wrapper.find({ ref: 'editor' }).element;
- expect(wrapper.emitted()['editor-ready']).toBeUndefined();
+ expect(wrapper.emitted()[EDITOR_READY_EVENT]).toBeUndefined();
- await el.dispatchEvent(new Event('editor-ready'));
+ await el.dispatchEvent(new Event(EDITOR_READY_EVENT));
- expect(wrapper.emitted()['editor-ready']).toBeDefined();
+ expect(wrapper.emitted()[EDITOR_READY_EVENT]).toBeDefined();
});
it('component API `getEditor()` returns the editor instance', () => {
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index bd6a18bf704..92690f6fc37 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -1,6 +1,6 @@
-import { file } from 'jest/ide/helpers';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+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 FileIcon from '~/vue_shared/components/file_icon.vue';
diff --git a/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap b/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
index d8e6e37bb89..5f55502567b 100644
--- a/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
+++ b/spec/frontend/vue_shared/components/gfm_autocomplete/__snapshots__/utils_spec.js.snap
@@ -21,10 +21,10 @@ exports[`gfm_autocomplete/utils labels config shows the title in the menu item 1
exports[`gfm_autocomplete/utils members config shows an avatar character, name, parent name, and count in the menu item for a group 1`] = `
"
<div class=\\"gl-display-flex gl-align-items-center\\">
- <div class=\\"gl-avatar gl-avatar-s24 gl-flex-shrink-0 gl-rounded-small
+ <div class=\\"gl-avatar gl-avatar-s32 gl-flex-shrink-0 gl-rounded-small
gl-display-flex gl-align-items-center gl-justify-content-center\\" aria-hidden=\\"true\\">
G</div>
- <div class=\\"gl-font-sm gl-line-height-normal gl-ml-3\\">
+ <div class=\\"gl-line-height-normal gl-ml-4\\">
<div>1-1s &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt; (2)</div>
<div class=\\"gl-text-gray-700\\">GitLab Support Team</div>
</div>
@@ -36,8 +36,8 @@ exports[`gfm_autocomplete/utils members config shows an avatar character, name,
exports[`gfm_autocomplete/utils members config shows the avatar, name and username in the menu item for a user 1`] = `
"
<div class=\\"gl-display-flex gl-align-items-center\\">
- <img class=\\"gl-avatar gl-avatar-s24 gl-flex-shrink-0 gl-avatar-circle\\" src=\\"/uploads/-/system/user/avatar/123456/avatar.png\\" alt=\\"\\" />
- <div class=\\"gl-font-sm gl-line-height-normal gl-ml-3\\">
+ <img class=\\"gl-avatar gl-avatar-s32 gl-flex-shrink-0 gl-avatar-circle\\" src=\\"/uploads/-/system/user/avatar/123456/avatar.png\\" alt=\\"\\" />
+ <div class=\\"gl-line-height-normal gl-ml-4\\">
<div>My Name &lt;script&gt;alert(&#39;hi&#39;)&lt;/script&gt;</div>
<div class=\\"gl-text-gray-700\\">@myusername</div>
</div>
diff --git a/spec/frontend/vue_shared/components/gl_countdown_spec.js b/spec/frontend/vue_shared/components/gl_countdown_spec.js
index fcc5c0cd310..82d18c7fd3f 100644
--- a/spec/frontend/vue_shared/components/gl_countdown_spec.js
+++ b/spec/frontend/vue_shared/components/gl_countdown_spec.js
@@ -1,5 +1,5 @@
-import mountComponent from 'helpers/vue_mount_component_helper';
import Vue from 'vue';
+import mountComponent from 'helpers/vue_mount_component_helper';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
describe('GlCountdown', () => {
diff --git a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
index 6802499ed52..81518e67377 100644
--- a/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
+++ b/spec/frontend/vue_shared/components/gl_modal_vuex_spec.js
@@ -3,6 +3,7 @@ import Vuex from 'vuex';
import { GlModal } from '@gitlab/ui';
import GlModalVuex from '~/vue_shared/components/gl_modal_vuex.vue';
import createState from '~/vuex_shared/modules/modal/state';
+import { BV_SHOW_MODAL, BV_HIDE_MODAL } from '~/lib/utils/constants';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -129,7 +130,7 @@ describe('GlModalVuex', () => {
wrapper.vm
.$nextTick()
.then(() => {
- expect(rootEmit).toHaveBeenCalledWith('bv::show::modal', TEST_MODAL_ID);
+ expect(rootEmit).toHaveBeenCalledWith(BV_SHOW_MODAL, TEST_MODAL_ID);
})
.then(done)
.catch(done.fail);
@@ -146,7 +147,7 @@ describe('GlModalVuex', () => {
wrapper.vm
.$nextTick()
.then(() => {
- expect(rootEmit).toHaveBeenCalledWith('bv::hide::modal', TEST_MODAL_ID);
+ expect(rootEmit).toHaveBeenCalledWith(BV_HIDE_MODAL, TEST_MODAL_ID);
})
.then(done)
.catch(done.fail);
diff --git a/spec/frontend/vue_shared/components/help_popover_spec.js b/spec/frontend/vue_shared/components/help_popover_spec.js
new file mode 100644
index 00000000000..112085e91e1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/help_popover_spec.js
@@ -0,0 +1,65 @@
+import { mount } from '@vue/test-utils';
+import { GlButton, GlPopover } from '@gitlab/ui';
+import HelpPopover from '~/vue_shared/components/help_popover.vue';
+
+describe('HelpPopover', () => {
+ let wrapper;
+ const title = 'popover <strong>title</strong>';
+ const content = 'popover <b>content</b>';
+
+ const findQuestionButton = () => wrapper.find(GlButton);
+ const findPopover = () => wrapper.find(GlPopover);
+ const buildWrapper = (options = {}) => {
+ wrapper = mount(HelpPopover, {
+ propsData: {
+ options: {
+ title,
+ content,
+ ...options,
+ },
+ },
+ });
+ };
+
+ beforeEach(() => {
+ buildWrapper();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('renders a link button with an icon question', () => {
+ expect(findQuestionButton().props()).toMatchObject({
+ icon: 'question',
+ variant: 'link',
+ });
+ expect(findQuestionButton().attributes().tabindex).toBe('0');
+ });
+
+ it('renders popover that uses the question button as target', () => {
+ expect(findPopover().props().target()).toBe(findQuestionButton().vm.$el);
+ });
+
+ it('triggers popover on hover and focus', () => {
+ expect(findPopover().props().triggers).toBe('hover focus');
+ });
+
+ it('allows rendering title with HTML tags', () => {
+ expect(findPopover().find('strong').exists()).toBe(true);
+ });
+
+ it('allows rendering content with HTML tags', () => {
+ expect(findPopover().find('b').exists()).toBe(true);
+ });
+
+ it('binds other popover options to the popover instance', () => {
+ const placement = 'bottom';
+
+ wrapper.destroy();
+ buildWrapper({ placement });
+
+ expect(findPopover().props().placement).toBe(placement);
+ });
+});
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 ffcb891c4fc..8143196c6e8 100644
--- a/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
+++ b/spec/frontend/vue_shared/components/issue/issue_milestone_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
-import { mockMilestone } from 'jest/boards/mock_data';
import { GlIcon } from '@gitlab/ui';
+import { mockMilestone } from 'jest/boards/mock_data';
import IssueMilestone from '~/vue_shared/components/issue/issue_milestone.vue';
const createComponent = (milestone = mockMilestone) => {
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index a2ce6f40193..97cc22e1418 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -1,7 +1,7 @@
import { mount } from '@vue/test-utils';
-import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import AxiosMockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
+import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import axios from '~/lib/utils/axios_utils';
diff --git a/spec/frontend/vue_shared/components/modal_copy_button_spec.js b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
index ca9f8ff54d4..97d4313b89e 100644
--- a/spec/frontend/vue_shared/components/modal_copy_button_spec.js
+++ b/spec/frontend/vue_shared/components/modal_copy_button_spec.js
@@ -1,5 +1,6 @@
import { shallowMount, createWrapper } from '@vue/test-utils';
import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
describe('modal copy button', () => {
let wrapper;
@@ -31,7 +32,7 @@ describe('modal copy button', () => {
return wrapper.vm.$nextTick().then(() => {
expect(wrapper.emitted().success).not.toBeEmpty();
expect(document.execCommand).toHaveBeenCalledWith('copy');
- expect(root.emitted('bv::hide::tooltip')).toEqual([['test-id']]);
+ expect(root.emitted(BV_HIDE_TOOLTIP)).toEqual([['test-id']]);
});
});
it("should propagate the clipboard error event if execCommand doesn't work", () => {
diff --git a/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js b/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js
index 0e6f951bd53..594b1ab9d78 100644
--- a/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js
+++ b/spec/frontend/vue_shared/components/rich_content_editor/toolbar_item_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { GlIcon } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ToolbarItem from '~/vue_shared/components/rich_content_editor/toolbar_item.vue';
describe('Toolbar Item', () => {
diff --git a/spec/frontend/vue_shared/components/runner_instructions/mock_data.js b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
new file mode 100644
index 00000000000..01f7f3d49c7
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
@@ -0,0 +1,107 @@
+export const mockGraphqlRunnerPlatforms = {
+ data: {
+ runnerPlatforms: {
+ nodes: [
+ {
+ name: 'linux',
+ humanReadableName: 'Linux',
+ architectures: {
+ nodes: [
+ {
+ name: 'amd64',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64',
+ __typename: 'RunnerArchitecture',
+ },
+ {
+ name: '386',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-386',
+ __typename: 'RunnerArchitecture',
+ },
+ {
+ name: 'arm',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm',
+ __typename: 'RunnerArchitecture',
+ },
+ {
+ name: 'arm64',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-arm64',
+ __typename: 'RunnerArchitecture',
+ },
+ ],
+ __typename: 'RunnerArchitectureConnection',
+ },
+ __typename: 'RunnerPlatform',
+ },
+ {
+ name: 'osx',
+ humanReadableName: 'macOS',
+ architectures: {
+ nodes: [
+ {
+ name: 'amd64',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-darwin-amd64',
+ __typename: 'RunnerArchitecture',
+ },
+ ],
+ __typename: 'RunnerArchitectureConnection',
+ },
+ __typename: 'RunnerPlatform',
+ },
+ {
+ name: 'windows',
+ humanReadableName: 'Windows',
+ architectures: {
+ nodes: [
+ {
+ name: 'amd64',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-amd64.exe',
+ __typename: 'RunnerArchitecture',
+ },
+ {
+ name: '386',
+ downloadLocation:
+ 'https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-windows-386.exe',
+ __typename: 'RunnerArchitecture',
+ },
+ ],
+ __typename: 'RunnerArchitectureConnection',
+ },
+ __typename: 'RunnerPlatform',
+ },
+ {
+ name: 'docker',
+ humanReadableName: 'Docker',
+ architectures: null,
+ __typename: 'RunnerPlatform',
+ },
+ {
+ name: 'kubernetes',
+ humanReadableName: 'Kubernetes',
+ architectures: null,
+ __typename: 'RunnerPlatform',
+ },
+ ],
+ __typename: 'RunnerPlatformConnection',
+ },
+ project: { id: 'gid://gitlab/Project/1', __typename: 'Project' },
+ group: null,
+ },
+};
+
+export const mockGraphqlInstructions = {
+ data: {
+ runnerSetup: {
+ installInstructions:
+ "# Download the binary for your system\nsudo curl -L --output /usr/local/bin/gitlab-runner https://gitlab-runner-downloads.s3.amazonaws.com/latest/binaries/gitlab-runner-linux-amd64\n\n# Give it permissions to execute\nsudo chmod +x /usr/local/bin/gitlab-runner\n\n# Create a GitLab CI user\nsudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash\n\n# Install and run as service\nsudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner\nsudo gitlab-runner start\n",
+ registerInstructions:
+ 'sudo gitlab-runner register --url http://192.168.1.81:3000/ --registration-token GE5gsjeep_HAtBf9s3Yz',
+ __typename: 'RunnerSetup',
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
new file mode 100644
index 00000000000..6e2ba603e04
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_spec.js
@@ -0,0 +1,113 @@
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import RunnerInstructions from '~/vue_shared/components/runner_instructions/runner_instructions.vue';
+import getRunnerPlatforms from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
+import getRunnerSetupInstructions from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
+
+import { mockGraphqlRunnerPlatforms, mockGraphqlInstructions } from './mock_data';
+
+const projectPath = 'gitlab-org/gitlab';
+const localVue = createLocalVue();
+localVue.use(VueApollo);
+
+describe('RunnerInstructions component', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const findModalButton = () => wrapper.find('[data-testid="show-modal-button"]');
+ const findPlatformButtons = () => wrapper.findAll('[data-testid="platform-button"]');
+ const findArchitectureDropdownItems = () =>
+ wrapper.findAll('[data-testid="architecture-dropdown-item"]');
+ const findBinaryInstructionsSection = () => wrapper.find('[data-testid="binary-instructions"]');
+ const findRunnerInstructionsSection = () => wrapper.find('[data-testid="runner-instructions"]');
+
+ beforeEach(async () => {
+ const requestHandlers = [
+ [getRunnerPlatforms, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
+ [getRunnerSetupInstructions, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
+ ];
+
+ fakeApollo = createMockApollo(requestHandlers);
+
+ wrapper = shallowMount(RunnerInstructions, {
+ provide: {
+ projectPath,
+ },
+ localVue,
+ apolloProvider: fakeApollo,
+ });
+
+ await wrapper.vm.$nextTick();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ it('should show the "Show Runner installation instructions" button', () => {
+ const button = findModalButton();
+
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toBe('Show Runner installation instructions');
+ });
+
+ it('should contain a number of platforms buttons', () => {
+ const buttons = findPlatformButtons();
+
+ expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
+ });
+
+ it('should contain a number of dropdown items for the architecture options', () => {
+ const platformButton = findPlatformButtons().at(0);
+ platformButton.vm.$emit('click');
+
+ return wrapper.vm.$nextTick(() => {
+ const dropdownItems = findArchitectureDropdownItems();
+
+ expect(dropdownItems).toHaveLength(
+ mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
+ );
+ });
+ });
+
+ it('should display the binary installation instructions for a selected architecture', async () => {
+ const platformButton = findPlatformButtons().at(0);
+ platformButton.vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ const dropdownItem = findArchitectureDropdownItems().at(0);
+ dropdownItem.vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ const runner = findBinaryInstructionsSection();
+
+ expect(runner.text()).toMatch('sudo chmod +x /usr/local/bin/gitlab-runner');
+ expect(runner.text()).toMatch(
+ `sudo useradd --comment 'GitLab Runner' --create-home gitlab-runner --shell /bin/bash`,
+ );
+ expect(runner.text()).toMatch(
+ 'sudo gitlab-runner install --user=gitlab-runner --working-directory=/home/gitlab-runner',
+ );
+ expect(runner.text()).toMatch('sudo gitlab-runner start');
+ });
+
+ it('should display the runner register instructions for a selected architecture', async () => {
+ const platformButton = findPlatformButtons().at(0);
+ platformButton.vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ const dropdownItem = findArchitectureDropdownItems().at(0);
+ dropdownItem.vm.$emit('click');
+
+ await wrapper.vm.$nextTick();
+
+ const runner = findRunnerInstructionsSection();
+
+ expect(runner.text()).toMatch(mockGraphqlInstructions.data.runnerSetup.registerInstructions);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
new file mode 100644
index 00000000000..51b8aa162bc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/settings/__snapshots__/settings_block_spec.js.snap
@@ -0,0 +1,43 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Settings Block renders the correct markup 1`] = `
+<section
+ class="settings no-animate"
+>
+ <div
+ class="settings-header"
+ >
+ <h4>
+ <div
+ data-testid="title-slot"
+ />
+ </h4>
+
+ <gl-button-stub
+ buttontextclasses=""
+ category="primary"
+ icon=""
+ size="medium"
+ variant="default"
+ >
+
+ Expand
+
+ </gl-button-stub>
+
+ <p>
+ <div
+ data-testid="description-slot"
+ />
+ </p>
+ </div>
+
+ <div
+ class="settings-content"
+ >
+ <div
+ data-testid="default-slot"
+ />
+ </div>
+</section>
+`;
diff --git a/spec/frontend/vue_shared/components/settings/settings_block_spec.js b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
new file mode 100644
index 00000000000..b550c4cfcc3
--- /dev/null
+++ b/spec/frontend/vue_shared/components/settings/settings_block_spec.js
@@ -0,0 +1,86 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+import component from '~/vue_shared/components/settings/settings_block.vue';
+
+describe('Settings Block', () => {
+ let wrapper;
+
+ const mountComponent = (propsData) => {
+ wrapper = shallowMount(component, {
+ propsData,
+ slots: {
+ title: '<div data-testid="title-slot"></div>',
+ description: '<div data-testid="description-slot"></div>',
+ default: '<div data-testid="default-slot"></div>',
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ const findDefaultSlot = () => wrapper.find('[data-testid="default-slot"]');
+ const findTitleSlot = () => wrapper.find('[data-testid="title-slot"]');
+ const findDescriptionSlot = () => wrapper.find('[data-testid="description-slot"]');
+ const findExpandButton = () => wrapper.find(GlButton);
+
+ it('renders the correct markup', () => {
+ mountComponent();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('has a default slot', () => {
+ mountComponent();
+
+ expect(findDefaultSlot().exists()).toBe(true);
+ });
+
+ it('has a title slot', () => {
+ mountComponent();
+
+ expect(findTitleSlot().exists()).toBe(true);
+ });
+
+ it('has a description slot', () => {
+ mountComponent();
+
+ expect(findDescriptionSlot().exists()).toBe(true);
+ });
+
+ describe('expanded behaviour', () => {
+ it('is collapsed by default', () => {
+ mountComponent();
+
+ expect(wrapper.classes('expanded')).toBe(false);
+ });
+
+ it('adds expanded class when the expand button is clicked', async () => {
+ mountComponent();
+
+ expect(wrapper.classes('expanded')).toBe(false);
+ expect(findExpandButton().text()).toBe('Expand');
+
+ await findExpandButton().vm.$emit('click');
+
+ expect(wrapper.classes('expanded')).toBe(true);
+ expect(findExpandButton().text()).toBe('Collapse');
+ });
+
+ it('is expanded when `defaultExpanded` is true no matter what', async () => {
+ mountComponent({ defaultExpanded: true });
+
+ expect(wrapper.classes('expanded')).toBe(true);
+
+ await findExpandButton().vm.$emit('click');
+
+ expect(wrapper.classes('expanded')).toBe(true);
+
+ await findExpandButton().vm.$emit('click');
+
+ expect(wrapper.classes('expanded')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/todo_button_spec.js b/spec/frontend/vue_shared/components/todo_button_spec.js
index 1f8a214d632..2066c3bd671 100644
--- a/spec/frontend/vue_shared/components/todo_button_spec.js
+++ b/spec/frontend/vue_shared/components/todo_button_spec.js
@@ -33,7 +33,7 @@ describe('Todo Button', () => {
it.each`
label | isTodo
${'Mark as done'} | ${true}
- ${'Add a To Do'} | ${false}
+ ${'Add a to do'} | ${false}
`('sets correct label when isTodo is $isTodo', ({ label, isTodo }) => {
createComponent({ isTodo });
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
index d151cd15bc4..3eae707daf8 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_link_spec.js
@@ -1,7 +1,7 @@
import { each } from 'lodash';
-import { trimText } from 'helpers/text_helper';
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
+import { trimText } from 'helpers/text_helper';
import { TEST_HOST } from 'spec/test_constants';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import UserAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
diff --git a/spec/frontend/vue_shared/directives/track_event_spec.js b/spec/frontend/vue_shared/directives/track_event_spec.js
index 8d867c8e3fc..c42169440ed 100644
--- a/spec/frontend/vue_shared/directives/track_event_spec.js
+++ b/spec/frontend/vue_shared/directives/track_event_spec.js
@@ -5,7 +5,7 @@ import TrackEvent from '~/vue_shared/directives/track_event';
jest.mock('~/tracking');
-const Component = Vue.component('dummy-element', {
+const Component = Vue.component('DummyElement', {
directives: {
TrackEvent,
},
diff --git a/spec/frontend/vue_shared/security_reports/mock_data.js b/spec/frontend/vue_shared/security_reports/mock_data.js
index b3ff7daef2b..7918f70d702 100644
--- a/spec/frontend/vue_shared/security_reports/mock_data.js
+++ b/spec/frontend/vue_shared/security_reports/mock_data.js
@@ -322,6 +322,23 @@ export const secretScanningDiffSuccessMock = {
head_report_created_at: '2020-01-10T10:00:00.000Z',
};
+export const securityReportDownloadPathsQueryNoArtifactsResponse = {
+ project: {
+ mergeRequest: {
+ headPipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/176',
+ jobs: {
+ nodes: [],
+ __typename: 'CiJobConnection',
+ },
+ __typename: 'Pipeline',
+ },
+ __typename: 'MergeRequest',
+ },
+ __typename: 'Project',
+ },
+};
+
export const securityReportDownloadPathsQueryResponse = {
project: {
mergeRequest: {
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
index 50d1d130675..bd66142ab9e 100644
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
@@ -1,6 +1,7 @@
-import { mount, createLocalVue } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { merge } from 'lodash';
+import Vue from 'vue';
import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -8,11 +9,11 @@ import { trimText } from 'helpers/text_helper';
import waitForPromises from 'helpers/wait_for_promises';
import {
expectedDownloadDropdownProps,
+ securityReportDownloadPathsQueryNoArtifactsResponse,
securityReportDownloadPathsQueryResponse,
sastDiffSuccessMock,
secretScanningDiffSuccessMock,
} from 'jest/vue_shared/security_reports/mock_data';
-import Api from '~/api';
import createFlash from '~/flash';
import axios from '~/lib/utils/axios_utils';
import {
@@ -26,8 +27,8 @@ import securityReportDownloadPathsQuery from '~/vue_shared/security_reports/quer
jest.mock('~/flash');
-const localVue = createLocalVue();
-localVue.use(Vuex);
+Vue.use(VueApollo);
+Vue.use(Vuex);
const SAST_COMPARISON_PATH = '/sast.json';
const SECRET_SCANNING_COMPARISON_PATH = '/secret_detection.json';
@@ -47,7 +48,6 @@ describe('Security reports app', () => {
SecurityReportsApp,
merge(
{
- localVue,
propsData: { ...props },
stubs: {
HelpIcon: true,
@@ -60,187 +60,94 @@ describe('Security reports app', () => {
const pendingHandler = () => new Promise(() => {});
const successHandler = () => Promise.resolve({ data: securityReportDownloadPathsQueryResponse });
+ const successEmptyHandler = () =>
+ Promise.resolve({ data: securityReportDownloadPathsQueryNoArtifactsResponse });
const failureHandler = () => Promise.resolve({ errors: [{ message: 'some error' }] });
const createMockApolloProvider = (handler) => {
- localVue.use(VueApollo);
-
const requestHandlers = [[securityReportDownloadPathsQuery, handler]];
return createMockApollo(requestHandlers);
};
- const anyParams = expect.any(Object);
-
const findDownloadDropdown = () => wrapper.find(SecurityReportDownloadDropdown);
- const findPipelinesTabAnchor = () => wrapper.find('[data-testid="show-pipelines"]');
const findHelpIconComponent = () => wrapper.find(HelpIcon);
- const setupMockJobArtifact = (reportType) => {
- jest
- .spyOn(Api, 'pipelineJobs')
- .mockResolvedValue({ data: [{ artifacts: [{ file_type: reportType }] }] });
- };
- const expectPipelinesTabAnchor = () => {
- const mrTabsMock = { tabShown: jest.fn() };
- window.mrTabs = mrTabsMock;
- findPipelinesTabAnchor().trigger('click');
- expect(mrTabsMock.tabShown.mock.calls).toEqual([['pipelines']]);
- };
afterEach(() => {
wrapper.destroy();
- delete window.mrTabs;
});
- describe.each([false, true])(
- 'given the coreSecurityMrWidgetCounts feature flag is %p',
- (coreSecurityMrWidgetCounts) => {
- const createComponentWithFlag = (options) =>
- createComponent(
- merge(
- {
- provide: {
- glFeatures: {
- coreSecurityMrWidgetCounts,
- },
- },
- },
- options,
- ),
- );
-
- describe.each(SecurityReportsApp.reportTypes)('given a report type %p', (reportType) => {
- beforeEach(() => {
- window.mrTabs = { tabShown: jest.fn() };
- setupMockJobArtifact(reportType);
- createComponentWithFlag();
- return wrapper.vm.$nextTick();
- });
-
- it('calls the pipelineJobs API correctly', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
- expect(Api.pipelineJobs).toHaveBeenCalledWith(
- props.projectId,
- props.pipelineId,
- anyParams,
- );
- });
-
- it('renders the expected message', () => {
- expect(wrapper.text()).toMatchInterpolatedText(
- SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance,
- );
- });
-
- describe('clicking the anchor to the pipelines tab', () => {
- it('calls the mrTabs.tabShown global', () => {
- expectPipelinesTabAnchor();
- });
- });
-
- it('renders a help link', () => {
- expect(findHelpIconComponent().props()).toEqual({
- helpPath: props.securityReportsDocsPath,
- discoverProjectSecurityPath: props.discoverProjectSecurityPath,
- });
- });
+ describe('given the artifacts query is loading', () => {
+ beforeEach(() => {
+ createComponent({
+ apolloProvider: createMockApolloProvider(pendingHandler),
});
+ });
- describe('given a report type "foo"', () => {
- beforeEach(() => {
- setupMockJobArtifact('foo');
- createComponentWithFlag();
- return wrapper.vm.$nextTick();
- });
-
- it('calls the pipelineJobs API correctly', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
- expect(Api.pipelineJobs).toHaveBeenCalledWith(
- props.projectId,
- props.pipelineId,
- anyParams,
- );
- });
+ // TODO: Remove this assertion as part of
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
+ it('initially renders nothing', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
- it('renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
+ describe('given the artifacts query loads successfully', () => {
+ beforeEach(() => {
+ createComponent({
+ apolloProvider: createMockApolloProvider(successHandler),
});
+ });
- describe('security artifacts on last page of multi-page response', () => {
- const numPages = 3;
-
- beforeEach(() => {
- jest
- .spyOn(Api, 'pipelineJobs')
- .mockImplementation(async (projectId, pipelineId, { page }) => {
- const requestedPage = parseInt(page, 10);
- if (requestedPage < numPages) {
- return {
- // Some jobs with no relevant artifacts
- data: [{}, {}],
- headers: { 'x-next-page': String(requestedPage + 1) },
- };
- } else if (requestedPage === numPages) {
- return {
- data: [{ artifacts: [{ file_type: SecurityReportsApp.reportTypes[0] }] }],
- };
- }
-
- throw new Error('Test failed due to request of non-existent jobs page');
- });
-
- createComponentWithFlag();
- return wrapper.vm.$nextTick();
- });
+ it('renders the download dropdown', () => {
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
+ });
- it('fetches all pages', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(numPages);
- });
+ it('renders the expected message', () => {
+ expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
+ });
- it('renders the expected message', () => {
- expect(wrapper.text()).toMatchInterpolatedText(
- SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance,
- );
- });
+ it('renders a help link', () => {
+ expect(findHelpIconComponent().props()).toEqual({
+ helpPath: props.securityReportsDocsPath,
+ discoverProjectSecurityPath: props.discoverProjectSecurityPath,
});
+ });
+ });
- describe('given an error from the API', () => {
- let error;
-
- beforeEach(() => {
- error = new Error('an error');
- jest.spyOn(Api, 'pipelineJobs').mockRejectedValue(error);
- createComponentWithFlag();
- return wrapper.vm.$nextTick();
- });
+ describe('given the artifacts query loads successfully with no artifacts', () => {
+ beforeEach(() => {
+ createComponent({
+ apolloProvider: createMockApolloProvider(successEmptyHandler),
+ });
+ });
- it('calls the pipelineJobs API correctly', () => {
- expect(Api.pipelineJobs).toHaveBeenCalledTimes(1);
- expect(Api.pipelineJobs).toHaveBeenCalledWith(
- props.projectId,
- props.pipelineId,
- anyParams,
- );
- });
+ // TODO: Remove this assertion as part of
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
+ it('initially renders nothing', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
- it('renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
+ describe('given the artifacts query fails', () => {
+ beforeEach(() => {
+ createComponent({
+ apolloProvider: createMockApolloProvider(failureHandler),
+ });
+ });
- it('calls createFlash correctly', () => {
- expect(createFlash.mock.calls).toEqual([
- [
- {
- message: SecurityReportsApp.i18n.apiError,
- captureError: true,
- error,
- },
- ],
- ]);
- });
+ it('calls createFlash correctly', () => {
+ expect(createFlash).toHaveBeenCalledWith({
+ message: SecurityReportsApp.i18n.apiError,
+ captureError: true,
+ error: expect.any(Error),
});
- },
- );
+ });
+
+ // TODO: Remove this assertion as part of
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
+ it('renders nothing', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
describe('given the coreSecurityMrWidgetCounts feature flag is enabled', () => {
let mock;
@@ -253,6 +160,7 @@ describe('Security reports app', () => {
coreSecurityMrWidgetCounts: true,
},
},
+ apolloProvider: createMockApolloProvider(successHandler),
}),
);
@@ -274,11 +182,7 @@ describe('Security reports app', () => {
${REPORT_TYPE_SECRET_DETECTION} | ${'secretScanningComparisonPath'} | ${SECRET_SCANNING_COMPARISON_PATH} | ${secretScanningDiffSuccessMock} | ${SECRET_SCANNING_SUCCESS_MESSAGE}
`(
'given a $pathProp and $reportType artifact',
- ({ reportType, pathProp, path, successResponse, successMessage }) => {
- beforeEach(() => {
- setupMockJobArtifact(reportType);
- });
-
+ ({ pathProp, path, successResponse, successMessage }) => {
describe('when loading', () => {
beforeEach(() => {
mock = new MockAdapter(axios, { delayResponse: 1 });
@@ -294,11 +198,11 @@ describe('Security reports app', () => {
});
it('should have loading message', () => {
- expect(wrapper.text()).toBe('Security scanning is loading');
+ expect(wrapper.text()).toContain('Security scanning is loading');
});
- it('should not render the pipeline tab anchor', () => {
- expect(findPipelinesTabAnchor().exists()).toBe(false);
+ it('renders the download dropdown', () => {
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
});
@@ -319,8 +223,8 @@ describe('Security reports app', () => {
expect(trimText(wrapper.text())).toContain(successMessage);
});
- it('should render the pipeline tab anchor', () => {
- expectPipelinesTabAnchor();
+ it('renders the download dropdown', () => {
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
});
@@ -341,125 +245,25 @@ describe('Security reports app', () => {
expect(trimText(wrapper.text())).toContain('Loading resulted in an error');
});
- it('should render the pipeline tab anchor', () => {
- expectPipelinesTabAnchor();
+ it('renders the download dropdown', () => {
+ expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
});
});
- },
- );
- });
-
- describe('given coreSecurityMrWidgetDownloads feature flag is enabled', () => {
- const createComponentWithFlagEnabled = (options) =>
- createComponent(
- merge(options, {
- provide: {
- glFeatures: {
- coreSecurityMrWidgetDownloads: true,
- },
- },
- }),
- );
-
- describe('given the query is loading', () => {
- beforeEach(() => {
- createComponentWithFlagEnabled({
- apolloProvider: createMockApolloProvider(pendingHandler),
- });
- });
-
- // TODO: Remove this assertion as part of
- // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
- it('initially renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
- });
- describe('given the query loads successfully', () => {
- beforeEach(() => {
- createComponentWithFlagEnabled({
- apolloProvider: createMockApolloProvider(successHandler),
- });
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
- });
-
- it('renders the expected message', () => {
- const text = wrapper.text();
- expect(text).not.toContain(SecurityReportsApp.i18n.scansHaveRunWithDownloadGuidance);
- expect(text).toContain(SecurityReportsApp.i18n.scansHaveRun);
- });
+ describe('when the comparison endpoint is not provided', () => {
+ beforeEach(() => {
+ mock.onGet(path).replyOnce(500);
- it('should not render the pipeline tab anchor', () => {
- expect(findPipelinesTabAnchor().exists()).toBe(false);
- });
- });
+ createComponentWithFlagEnabled();
- describe('given the query fails', () => {
- beforeEach(() => {
- createComponentWithFlagEnabled({
- apolloProvider: createMockApolloProvider(failureHandler),
- });
- });
+ return waitForPromises();
+ });
- it('calls createFlash correctly', () => {
- expect(createFlash).toHaveBeenCalledWith({
- message: SecurityReportsApp.i18n.apiError,
- captureError: true,
- error: expect.any(Error),
+ it('renders the basic scansHaveRun message', () => {
+ expect(wrapper.text()).toContain(SecurityReportsApp.i18n.scansHaveRun);
+ });
});
- });
-
- // TODO: Remove this assertion as part of
- // https://gitlab.com/gitlab-org/gitlab/-/issues/273431
- it('renders nothing', () => {
- expect(wrapper.html()).toBe('');
- });
- });
- });
-
- describe('given coreSecurityMrWidgetCounts and coreSecurityMrWidgetDownloads feature flags are enabled', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- mock.onGet(SAST_COMPARISON_PATH).replyOnce(200, sastDiffSuccessMock);
- mock.onGet(SECRET_SCANNING_COMPARISON_PATH).replyOnce(200, secretScanningDiffSuccessMock);
- createComponent({
- propsData: {
- sastComparisonPath: SAST_COMPARISON_PATH,
- secretScanningComparisonPath: SECRET_SCANNING_COMPARISON_PATH,
- },
- provide: {
- glFeatures: {
- coreSecurityMrWidgetCounts: true,
- coreSecurityMrWidgetDownloads: true,
- },
- },
- apolloProvider: createMockApolloProvider(successHandler),
- });
-
- return waitForPromises();
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('renders the download dropdown', () => {
- expect(findDownloadDropdown().props()).toEqual(expectedDownloadDropdownProps);
- });
-
- it('renders the expected counts message', () => {
- expect(trimText(wrapper.text())).toContain(
- 'Security scanning detected 3 potential vulnerabilities 2 Critical 1 High and 0 Others',
- );
- });
-
- it('should not render the pipeline tab anchor', () => {
- expect(findPipelinesTabAnchor().exists()).toBe(false);
- });
+ },
+ );
});
});
diff --git a/spec/frontend/whats_new/store/actions_spec.js b/spec/frontend/whats_new/store/actions_spec.js
index 82f17a2726f..b5e0b2a0364 100644
--- a/spec/frontend/whats_new/store/actions_spec.js
+++ b/spec/frontend/whats_new/store/actions_spec.js
@@ -1,6 +1,6 @@
+import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import actions from '~/whats_new/store/actions';
import * as types from '~/whats_new/store/mutation_types';