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:
authorGitLab Bot <gitlab-bot@gitlab.com>2023-10-19 15:57:54 +0300
committerGitLab Bot <gitlab-bot@gitlab.com>2023-10-19 15:57:54 +0300
commit419c53ec62de6e97a517abd5fdd4cbde3a942a34 (patch)
tree1f43a548b46bca8a5fb8fe0c31cef1883d49c5b6 /spec/frontend
parent1da20d9135b3ad9e75e65b028bffc921aaf8deb7 (diff)
Add latest changes from gitlab-org/gitlab@16-5-stable-eev16.5.0-rc42
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap1
-rw-r--r--spec/frontend/admin/abuse_report/components/report_actions_spec.js55
-rw-r--r--spec/frontend/admin/abuse_report/components/user_details_spec.js8
-rw-r--r--spec/frontend/alert_spec.js68
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/base_spec.js2
-rw-r--r--spec/frontend/analytics/cycle_analytics/components/value_stream_filters_spec.js190
-rw-r--r--spec/frontend/analytics/cycle_analytics/mock_data.js2
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/actions_spec.js3
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/mutations_spec.js2
-rw-r--r--spec/frontend/analytics/shared/components/date_ranges_dropdown_spec.js165
-rw-r--r--spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js66
-rw-r--r--spec/frontend/batch_comments/components/preview_dropdown_spec.js8
-rw-r--r--spec/frontend/behaviors/autosize_spec.js42
-rw-r--r--spec/frontend/behaviors/components/global_alerts_spec.js135
-rw-r--r--spec/frontend/behaviors/components/json_table_spec.js4
-rw-r--r--spec/frontend/behaviors/markdown/render_observability_spec.js43
-rw-r--r--spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap2
-rw-r--r--spec/frontend/blob/components/blob_header_default_actions_spec.js2
-rw-r--r--spec/frontend/blob/csv/csv_viewer_spec.js33
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js19
-rw-r--r--spec/frontend/boards/board_list_helper.js2
-rw-r--r--spec/frontend/boards/board_list_spec.js2
-rw-r--r--spec/frontend/boards/components/board_card_spec.js37
-rw-r--r--spec/frontend/boards/components/board_form_spec.js19
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js79
-rw-r--r--spec/frontend/boards/components/issue_board_filtered_search_spec.js5
-rw-r--r--spec/frontend/boards/mock_data.js10
-rw-r--r--spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap2
-rw-r--r--spec/frontend/branches/components/sort_dropdown_spec.js20
-rw-r--r--spec/frontend/ci/admin/jobs_table/components/cells/runner_cell_spec.js26
-rw-r--r--spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js32
-rw-r--r--spec/frontend/ci/catalog/components/ci_catalog_home_spec.js46
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js120
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js113
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js83
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js139
-rw-r--r--spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js96
-rw-r--r--spec/frontend/ci/catalog/components/list/catalog_header_spec.js86
-rw-r--r--spec/frontend/ci/catalog/components/list/catalog_list_skeleton_loader_spec.js22
-rw-r--r--spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js198
-rw-r--r--spec/frontend/ci/catalog/components/list/ci_resources_list_spec.js143
-rw-r--r--spec/frontend/ci/catalog/components/list/empty_state_spec.js27
-rw-r--r--spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js186
-rw-r--r--spec/frontend/ci/catalog/mock.js546
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js118
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js111
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js41
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js39
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js2
-rw-r--r--spec/frontend/ci/ci_variable_list/mocks.js3
-rw-r--r--spec/frontend/ci/ci_variable_list/utils_spec.js53
-rw-r--r--spec/frontend/ci/common/pipelines_table_spec.js241
-rw-r--r--spec/frontend/ci/job_details/components/job_header_spec.js37
-rw-r--r--spec/frontend/ci/job_details/components/log/collapsible_section_spec.js28
-rw-r--r--spec/frontend/ci/job_details/components/log/line_header_spec.js2
-rw-r--r--spec/frontend/ci/job_details/components/log/line_number_spec.js2
-rw-r--r--spec/frontend/ci/job_details/components/log/line_spec.js2
-rw-r--r--spec/frontend/ci/job_details/components/log/log_spec.js33
-rw-r--r--spec/frontend/ci/job_details/components/log/mock_data.js65
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/artifacts_block_spec.js16
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/sidebar_header_spec.js6
-rw-r--r--spec/frontend/ci/job_details/components/sidebar/sidebar_job_details_container_spec.js3
-rw-r--r--spec/frontend/ci/job_details/job_app_spec.js2
-rw-r--r--spec/frontend/ci/job_details/store/actions_spec.js25
-rw-r--r--spec/frontend/ci/job_details/store/mutations_spec.js22
-rw-r--r--spec/frontend/ci/job_details/store/utils_spec.js67
-rw-r--r--spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js8
-rw-r--r--spec/frontend/ci/jobs_page/components/job_cells/status_cell_spec.js (renamed from spec/frontend/ci/jobs_page/components/job_cells/duration_cell_spec.js)4
-rw-r--r--spec/frontend/ci/jobs_page/components/jobs_table_empty_state_spec.js2
-rw-r--r--spec/frontend/ci/jobs_page/components/jobs_table_spec.js7
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js26
-rw-r--r--spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js2
-rw-r--r--spec/frontend/ci/pipeline_details/mock_data.js6
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js15
-rw-r--r--spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js26
-rw-r--r--spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js8
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_empty_state_spec.js37
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js73
-rw-r--r--spec/frontend/ci/pipeline_schedules/mock_data.js27
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js40
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js69
-rw-r--r--spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js42
-rw-r--r--spec/frontend/ci/pipelines_page/pipelines_spec.js101
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js5
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js4
-rw-r--r--spec/frontend/ci/runner/components/runner_details_spec.js7
-rw-r--r--spec/frontend/ci/runner/components/runner_details_tabs_spec.js14
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js28
-rw-r--r--spec/frontend/ci/runner/components/runner_type_icon_spec.js67
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js5
-rw-r--r--spec/frontend/ci/runner/sentry_utils_spec.js21
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js37
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js10
-rw-r--r--spec/frontend/commit/commit_pipeline_status_spec.js2
-rw-r--r--spec/frontend/commit/components/commit_box_pipeline_status_spec.js16
-rw-r--r--spec/frontend/commit/pipelines/legacy_pipelines_table_wrapper_spec.js97
-rw-r--r--spec/frontend/content_editor/services/markdown_serializer_spec.js8
-rw-r--r--spec/frontend/contributors/component/contributors_spec.js15
-rw-r--r--spec/frontend/crm/crm_form_spec.js2
-rw-r--r--spec/frontend/crm/organization_form_wrapper_spec.js2
-rw-r--r--spec/frontend/design_management/components/design_description/description_form_spec.js8
-rw-r--r--spec/frontend/design_management/pages/index_spec.js2
-rw-r--r--spec/frontend/diffs/components/app_spec.js28
-rw-r--r--spec/frontend/diffs/components/diff_file_header_spec.js60
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js16
-rw-r--r--spec/frontend/diffs/store/actions_spec.js14
-rw-r--r--spec/frontend/diffs/store/mutations_spec.js10
-rw-r--r--spec/frontend/diffs/store/utils_spec.js28
-rw-r--r--spec/frontend/diffs/utils/merge_request_spec.js16
-rw-r--r--spec/frontend/diffs/utils/sort_errors_by_file_spec.js52
-rw-r--r--spec/frontend/editor/schema/ci/ci_schema_spec.js4
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/script.yml14
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/script.yml52
-rw-r--r--spec/frontend/editor/source_editor_spec.js21
-rw-r--r--spec/frontend/environments/canary_ingress_spec.js58
-rw-r--r--spec/frontend/environments/environment_form_spec.js9
-rw-r--r--spec/frontend/environments/graphql/resolvers/kubernetes_spec.js50
-rw-r--r--spec/frontend/environments/kubernetes_overview_spec.js7
-rw-r--r--spec/frontend/environments/kubernetes_pods_spec.js2
-rw-r--r--spec/frontend/environments/kubernetes_summary_spec.js6
-rw-r--r--spec/frontend/environments/kubernetes_tabs_spec.js2
-rw-r--r--spec/frontend/fixtures/autocomplete.rb20
-rw-r--r--spec/frontend/fixtures/autocomplete_sources.rb18
-rw-r--r--spec/frontend/fixtures/environments.rb34
-rw-r--r--spec/frontend/fixtures/issues.rb34
-rw-r--r--spec/frontend/fixtures/releases.rb130
-rw-r--r--spec/frontend/fixtures/search.rb7
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js38
-rw-r--r--spec/frontend/google_tag_manager/index_spec.js532
-rw-r--r--spec/frontend/helpers/startup_css_helper_spec.js67
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js43
-rw-r--r--spec/frontend/import/details/mock_data.js6
-rw-r--r--spec/frontend/import_entities/components/group_dropdown_spec.js94
-rw-r--r--spec/frontend/import_entities/components/import_target_dropdown_spec.js55
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js141
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js52
-rw-r--r--spec/frontend/integrations/gitlab_slack_application/components/projects_dropdown_spec.js54
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js76
-rw-r--r--spec/frontend/invite_members/mock_data/api_responses.js52
-rw-r--r--spec/frontend/issuable/components/hidden_badge_spec.js45
-rw-r--r--spec/frontend/issuable/components/locked_badge_spec.js45
-rw-r--r--spec/frontend/issues/dashboard/mock_data.js1
-rw-r--r--spec/frontend/issues/list/mock_data.js1
-rw-r--r--spec/frontend/issues/show/components/description_spec.js4
-rw-r--r--spec/frontend/issues/show/components/fields/description_spec.js17
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js86
-rw-r--r--spec/frontend/issues/show/components/new_header_actions_popover_spec.js77
-rw-r--r--spec/frontend/issues/show/components/sticky_header_spec.js37
-rw-r--r--spec/frontend/issues/show/mock_data/mock_data.js3
-rw-r--r--spec/frontend/lib/utils/global_alerts_spec.js80
-rw-r--r--spec/frontend/lib/utils/url_utility_spec.js74
-rw-r--r--spec/frontend/merge_requests/components/header_metadata_spec.js93
-rw-r--r--spec/frontend/merge_requests/components/merge_request_header_spec.js88
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js11
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js138
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js7
-rw-r--r--spec/frontend/ml/model_registry/apps/show_ml_model_spec.js15
-rw-r--r--spec/frontend/ml/model_registry/mock_data.js1
-rw-r--r--spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js68
-rw-r--r--spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js19
-rw-r--r--spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js42
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js201
-rw-r--r--spec/frontend/notes/components/email_participants_warning_spec.js6
-rw-r--r--spec/frontend/notes/components/note_form_spec.js26
-rw-r--r--spec/frontend/notes/mock_data.js1
-rw-r--r--spec/frontend/notes/stores/actions_spec.js4
-rw-r--r--spec/frontend/observability/client_spec.js233
-rw-r--r--spec/frontend/observability/index_spec.js64
-rw-r--r--spec/frontend/observability/observability_app_spec.js201
-rw-r--r--spec/frontend/observability/observability_container_spec.js6
-rw-r--r--spec/frontend/observability/skeleton_spec.js86
-rw-r--r--spec/frontend/organizations/index/components/app_spec.js87
-rw-r--r--spec/frontend/organizations/index/components/organizations_list_item_spec.js70
-rw-r--r--spec/frontend/organizations/index/components/organizations_list_spec.js28
-rw-r--r--spec/frontend/organizations/index/components/organizations_view_spec.js57
-rw-r--r--spec/frontend/organizations/index/mock_data.js3
-rw-r--r--spec/frontend/organizations/new/components/app_spec.js113
-rw-r--r--spec/frontend/organizations/shared/components/new_edit_form_spec.js112
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap63
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap2
-rw-r--r--spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js30
-rw-r--r--spec/frontend/pages/projects/find_file/ref_switcher/ref_switcher_utils_spec.js9
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js17
-rw-r--r--spec/frontend/performance_bar/components/request_warning_spec.js7
-rw-r--r--spec/frontend/performance_bar/index_spec.js3
-rw-r--r--spec/frontend/projects/components/__snapshots__/project_delete_button_spec.js.snap2
-rw-r--r--spec/frontend/projects/project_find_file_spec.js57
-rw-r--r--spec/frontend/projects/settings/components/new_access_dropdown_spec.js12
-rw-r--r--spec/frontend/ref/components/ambiguous_ref_modal_spec.js64
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js19
-rw-r--r--spec/frontend/ref/init_ambiguous_ref_modal_spec.js48
-rw-r--r--spec/frontend/releases/components/tag_field_new_spec.js13
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js442
-rw-r--r--spec/frontend/releases/stores/modules/detail/getters_spec.js16
-rw-r--r--spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap121
-rw-r--r--spec/frontend/repository/components/commit_info_spec.js87
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js119
-rw-r--r--spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap6
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js9
-rw-r--r--spec/frontend/search/sidebar/components/archived_filter_spec.js42
-rw-r--r--spec/frontend/search/sidebar/components/issues_filters_spec.js4
-rw-r--r--spec/frontend/search/sidebar/components/merge_requests_filters_spec.js6
-rw-r--r--spec/frontend/search/sidebar/components/milestones_filters_spec.js28
-rw-r--r--spec/frontend/search/topbar/components/app_spec.js135
-rw-r--r--spec/frontend/sentry/init_sentry_spec.js35
-rw-r--r--spec/frontend/sentry/sentry_browser_wrapper_spec.js22
-rw-r--r--spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js21
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap4
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap4
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap2
-rw-r--r--spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap6
-rw-r--r--spec/frontend/snippets/components/edit_spec.js3
-rw-r--r--spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js2
-rw-r--r--spec/frontend/snippets/components/snippet_header_spec.js2
-rw-r--r--spec/frontend/super_sidebar/components/create_menu_spec.js4
-rw-r--r--spec/frontend/super_sidebar/components/help_center_spec.js6
-rw-r--r--spec/frontend/super_sidebar/components/nav_item_spec.js45
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js26
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js42
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_profile_item_spec.js (renamed from spec/frontend/super_sidebar/components/user_name_group_spec.js)13
-rw-r--r--spec/frontend/super_sidebar/components/user_menu_spec.js10
-rw-r--r--spec/frontend/super_sidebar/utils_spec.js9
-rw-r--r--spec/frontend/tags/components/sort_dropdown_spec.js20
-rw-r--r--spec/frontend/tracking/internal_events_spec.js99
-rw-r--r--spec/frontend/users_select/test_helper.js1
-rw-r--r--spec/frontend/vue_alerts_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js90
-rw-r--r--spec/frontend/vue_merge_request_widget/components/checks/message_spec.js30
-rw-r--r--spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js92
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js94
-rw-r--r--spec/frontend/vue_shared/alert_details/alert_status_spec.js25
-rw-r--r--spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap21
-rw-r--r--spec/frontend/vue_shared/components/badges/__snapshots__/experiment_badge_spec.js.snap41
-rw-r--r--spec/frontend/vue_shared/components/badges/beta_badge_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/badges/experiment_badge_spec.js32
-rw-r--r--spec/frontend/vue_shared/components/badges/hover_badge_spec.js50
-rw-r--r--spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js1
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/clone_dropdown/clone_dropdown_item_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js17
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js40
-rw-r--r--spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js80
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js95
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js24
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js8
-rw-r--r--spec/frontend/vue_shared/components/segmented_control_button_group_spec.js31
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/__snapshots__/utils_spec.js.snap88
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js63
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/mock_data.js21
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/utils_spec.js35
-rw-r--r--spec/frontend/vue_shared/components/time_ago_tooltip_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/toggle_labels_spec.js56
-rw-r--r--spec/frontend/vue_shared/components/vuex_module_provider_spec.js10
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js3
-rw-r--r--spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js32
-rw-r--r--spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js18
-rw-r--r--spec/frontend/vue_shared/issuable/list/mock_data.js2
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js36
-rw-r--r--spec/frontend/work_items/components/notes/work_item_add_note_spec.js60
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_form_spec.js4
-rw-r--r--spec/frontend/work_items/components/notes/work_item_discussion_spec.js4
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js2
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js4
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js38
-rw-r--r--spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js5
-rw-r--r--spec/frontend/work_items/components/work_item_actions_spec.js140
-rw-r--r--spec/frontend/work_items/components/work_item_assignees_spec.js45
-rw-r--r--spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js2
-rw-r--r--spec/frontend/work_items/components/work_item_created_updated_spec.js64
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js39
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js87
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js58
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js3
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js21
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js3
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js37
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_milestone_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_parent_spec.js236
-rw-r--r--spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap6
-rw-r--r--spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js156
-rw-r--r--spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js3
-rw-r--r--spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js157
-rw-r--r--spec/frontend/work_items/components/work_item_todos_spec.js3
-rw-r--r--spec/frontend/work_items/graphql/cache_utils_spec.js8
-rw-r--r--spec/frontend/work_items/mock_data.js173
-rw-r--r--spec/frontend/work_items/pages/create_work_item_spec.js5
-rw-r--r--spec/frontend/work_items/router_spec.js1
-rw-r--r--spec/frontend/work_items/utils_spec.js13
294 files changed, 9150 insertions, 3963 deletions
diff --git a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
index 42818c14029..2bd2b17a12d 100644
--- a/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
+++ b/spec/frontend/access_tokens/components/__snapshots__/expires_at_field_spec.js.snap
@@ -21,7 +21,6 @@ exports[`~/access_tokens/components/expires_at_field should render datepicker wi
mindate="Mon Jul 06 2020 00:00:00 GMT+0000 (Greenwich Mean Time)"
placeholder="YYYY-MM-DD"
showclearbutton="true"
- size="medium"
theme=""
/>
</gl-form-group-stub>
diff --git a/spec/frontend/admin/abuse_report/components/report_actions_spec.js b/spec/frontend/admin/abuse_report/components/report_actions_spec.js
index 0e20630db14..3c366980c14 100644
--- a/spec/frontend/admin/abuse_report/components/report_actions_spec.js
+++ b/spec/frontend/admin/abuse_report/components/report_actions_spec.js
@@ -17,6 +17,9 @@ import {
ERROR_MESSAGE,
NO_ACTION,
USER_ACTION_OPTIONS,
+ TRUST_ACTION,
+ TRUST_REASON,
+ REASON_OPTIONS,
} from '~/admin/abuse_report/constants';
import { mockAbuseReport } from '../mock_data';
@@ -40,10 +43,11 @@ describe('ReportActions', () => {
const setCloseReport = (close) => wrapper.findByTestId('close').find('input').setChecked(close);
const setSelectOption = (id, value) =>
wrapper.findByTestId(`${id}-select`).find(`option[value=${value}]`).setSelected();
- const selectAction = (action) => setSelectOption('action', action);
+ const selectAction = (chosenAction) => setSelectOption('action', chosenAction);
const selectReason = (reason) => setSelectOption('reason', reason);
const setComment = (comment) => wrapper.findByTestId('comment').find('input').setValue(comment);
const submitForm = () => wrapper.findByTestId('submit-button').vm.$emit('click');
+ const findReasonOptions = () => wrapper.findByTestId('reason-select');
const createComponent = (props = {}) => {
wrapper = mountExtended(ReportActions, {
@@ -79,8 +83,8 @@ describe('ReportActions', () => {
expect(options).toHaveLength(USER_ACTION_OPTIONS.length);
- USER_ACTION_OPTIONS.forEach((action, index) => {
- expect(options.at(index).text()).toBe(action.text);
+ USER_ACTION_OPTIONS.forEach((userAction, index) => {
+ expect(options.at(index).text()).toBe(userAction.text);
});
});
});
@@ -100,6 +104,51 @@ describe('ReportActions', () => {
});
});
+ describe('reasons', () => {
+ beforeEach(() => {
+ clickActionsButton();
+ });
+
+ it('shows all non-trust reasons by default', () => {
+ const reasons = findReasonOptions().findAll('option');
+ expect(reasons).toHaveLength(REASON_OPTIONS.length);
+
+ REASON_OPTIONS.forEach((reason, index) => {
+ expect(reasons.at(index).text()).toBe(reason.text);
+ });
+ });
+
+ describe('when user selects any non-trust action', () => {
+ it('shows non-trust reasons', () => {
+ const reasonLength = REASON_OPTIONS.length;
+ let reasons;
+
+ USER_ACTION_OPTIONS.forEach((userAction) => {
+ if (userAction !== TRUST_ACTION && userAction !== NO_ACTION) {
+ selectAction(userAction.value);
+
+ reasons = findReasonOptions().findAll('option');
+ expect(reasons).toHaveLength(reasonLength);
+ }
+ });
+ });
+ });
+
+ describe('when user selects "Trust user"', () => {
+ beforeEach(() => {
+ selectAction(TRUST_ACTION.value);
+ });
+
+ it('only shows "Confirmed trusted user" reason', () => {
+ const reasons = findReasonOptions().findAll('option');
+
+ expect(reasons).toHaveLength(1);
+
+ expect(reasons.at(0).text()).toBe(TRUST_REASON.text);
+ });
+ });
+ });
+
describe('when clicking the actions button', () => {
beforeEach(() => {
clickActionsButton();
diff --git a/spec/frontend/admin/abuse_report/components/user_details_spec.js b/spec/frontend/admin/abuse_report/components/user_details_spec.js
index f3d8d5bb610..24ec0cdb1b2 100644
--- a/spec/frontend/admin/abuse_report/components/user_details_spec.js
+++ b/spec/frontend/admin/abuse_report/components/user_details_spec.js
@@ -70,14 +70,6 @@ describe('UserDetails', () => {
expect(findUserDetailLabel('credit-card-verification')).toBe(USER_DETAILS_I18N.creditCard);
});
- it('renders the users name', () => {
- expect(findUserDetail('credit-card-verification').text()).toContain(
- sprintf(USER_DETAILS_I18N.registeredWith, { ...user.creditCard }),
- );
-
- expect(findUserDetail('credit-card-verification').text()).toContain(user.creditCard.name);
- });
-
describe('similar credit cards', () => {
it('renders the number of similar records', () => {
expect(findUserDetail('credit-card-verification').text()).toContain(
diff --git a/spec/frontend/alert_spec.js b/spec/frontend/alert_spec.js
index 1ae8373016b..de3093c6c19 100644
--- a/spec/frontend/alert_spec.js
+++ b/spec/frontend/alert_spec.js
@@ -271,6 +271,74 @@ describe('Flash', () => {
expect(findTextContent()).toBe('message 1 message 2');
});
});
+
+ describe('with message links', () => {
+ const findAlertMessageLinks = () =>
+ Array.from(document.querySelectorAll('.flash-container a'));
+
+ it('creates a link', () => {
+ alert = createAlert({
+ message: 'Read more at %{exampleLinkStart}example site%{exampleLinkEnd}.',
+ messageLinks: {
+ exampleLink: 'https://example.com',
+ },
+ });
+ const messageLinks = findAlertMessageLinks();
+
+ expect(messageLinks).toHaveLength(1);
+ const link = messageLinks.at(0);
+ expect(link.textContent).toBe('example site');
+ expect(link.getAttribute('href')).toBe('https://example.com');
+ });
+
+ it('creates multiple links', () => {
+ alert = createAlert({
+ message:
+ 'Read more at %{exampleLinkStart}example site%{exampleLinkEnd}, or on %{docsLinkStart}the documentation%{docsLinkEnd}.',
+ messageLinks: {
+ exampleLink: 'https://example.com',
+ docsLink: 'https://docs.example.com',
+ },
+ });
+ const messageLinks = findAlertMessageLinks();
+
+ expect(messageLinks).toHaveLength(2);
+ const [firstLink, secondLink] = messageLinks;
+ expect(firstLink.textContent).toBe('example site');
+ expect(firstLink.getAttribute('href')).toBe('https://example.com');
+ expect(secondLink.textContent).toBe('the documentation');
+ expect(secondLink.getAttribute('href')).toBe('https://docs.example.com');
+ });
+
+ it('allows passing more props to gl-link', () => {
+ alert = createAlert({
+ message: 'Read more at %{exampleLinkStart}example site%{exampleLinkEnd}.',
+ messageLinks: {
+ exampleLink: {
+ href: 'https://example.com',
+ target: '_blank',
+ },
+ },
+ });
+ const messageLinks = findAlertMessageLinks();
+
+ expect(messageLinks).toHaveLength(1);
+ const link = messageLinks.at(0);
+ expect(link.textContent).toBe('example site');
+ expect(link.getAttribute('href')).toBe('https://example.com');
+ expect(link.getAttribute('target')).toBe('_blank');
+ });
+
+ it('does not create any links when given an empty messageLinks object', () => {
+ alert = createAlert({
+ message: 'Read more at %{exampleLinkStart}example site%{exampleLinkEnd}.',
+ messageLinks: {},
+ });
+ const messageLinks = findAlertMessageLinks();
+
+ expect(messageLinks).toHaveLength(0);
+ });
+ });
});
});
});
diff --git a/spec/frontend/analytics/cycle_analytics/components/base_spec.js b/spec/frontend/analytics/cycle_analytics/components/base_spec.js
index 653934000b3..cd477ff36aa 100644
--- a/spec/frontend/analytics/cycle_analytics/components/base_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/base_spec.js
@@ -141,9 +141,11 @@ describe('Value stream analytics component', () => {
namespacePath: groupPath,
endDate: createdBefore,
hasDateRangeFilter: true,
+ hasPredefinedDateRangesFilter: true,
hasProjectFilter: false,
selectedProjects: [],
startDate: createdAfter,
+ predefinedDateRange: null,
});
});
diff --git a/spec/frontend/analytics/cycle_analytics/components/value_stream_filters_spec.js b/spec/frontend/analytics/cycle_analytics/components/value_stream_filters_spec.js
index e3bcb0ab557..a04ffa79a68 100644
--- a/spec/frontend/analytics/cycle_analytics/components/value_stream_filters_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/components/value_stream_filters_spec.js
@@ -1,20 +1,29 @@
-import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Daterange from '~/analytics/shared/components/daterange.vue';
import ProjectsDropdownFilter from '~/analytics/shared/components/projects_dropdown_filter.vue';
import FilterBar from '~/analytics/cycle_analytics/components/filter_bar.vue';
import ValueStreamFilters from '~/analytics/cycle_analytics/components/value_stream_filters.vue';
+import DateRangesDropdown from '~/analytics/shared/components/date_ranges_dropdown.vue';
import {
- createdAfter as startDate,
- createdBefore as endDate,
- currentGroup,
- selectedProjects,
-} from '../mock_data';
+ DATE_RANGE_LAST_30_DAYS_VALUE,
+ DATE_RANGE_CUSTOM_VALUE,
+ LAST_30_DAYS,
+} from '~/analytics/shared/constants';
+import { useFakeDate } from 'helpers/fake_date';
+import { currentGroup, selectedProjects } from '../mock_data';
const { path } = currentGroup;
const groupPath = `groups/${path}`;
+const defaultFeatureFlags = {
+ vsaPredefinedDateRanges: false,
+};
-function createComponent(props = {}) {
- return shallowMount(ValueStreamFilters, {
+const startDate = LAST_30_DAYS;
+const endDate = new Date('2019-01-14T00:00:00.000Z');
+
+function createComponent({ props = {}, featureFlags = defaultFeatureFlags } = {}) {
+ return shallowMountExtended(ValueStreamFilters, {
propsData: {
selectedProjects,
groupPath,
@@ -23,15 +32,23 @@ function createComponent(props = {}) {
endDate,
...props,
},
+ provide: {
+ glFeatures: {
+ ...featureFlags,
+ },
+ },
});
}
describe('ValueStreamFilters', () => {
+ useFakeDate(2019, 0, 14, 10, 10);
+
let wrapper;
const findProjectsDropdown = () => wrapper.findComponent(ProjectsDropdownFilter);
const findDateRangePicker = () => wrapper.findComponent(Daterange);
const findFilterBar = () => wrapper.findComponent(FilterBar);
+ const findDateRangesDropdown = () => wrapper.findComponent(DateRangesDropdown);
beforeEach(() => {
wrapper = createComponent();
@@ -55,6 +72,10 @@ describe('ValueStreamFilters', () => {
expect(findDateRangePicker().exists()).toBe(true);
});
+ it('will not render the date ranges dropdown', () => {
+ expect(findDateRangesDropdown().exists()).toBe(false);
+ });
+
it('will emit `selectProject` when a project is selected', () => {
findProjectsDropdown().vm.$emit('selected');
@@ -69,21 +90,168 @@ describe('ValueStreamFilters', () => {
describe('hasDateRangeFilter = false', () => {
beforeEach(() => {
- wrapper = createComponent({ hasDateRangeFilter: false });
+ wrapper = createComponent({ props: { hasDateRangeFilter: false } });
});
- it('will not render the date range picker', () => {
+ it('should not render the date range picker', () => {
expect(findDateRangePicker().exists()).toBe(false);
});
});
describe('hasProjectFilter = false', () => {
beforeEach(() => {
- wrapper = createComponent({ hasProjectFilter: false });
+ wrapper = createComponent({ props: { hasProjectFilter: false } });
});
it('will not render the project dropdown', () => {
expect(findProjectsDropdown().exists()).toBe(false);
});
});
+
+ describe('`vsaPredefinedDateRanges` feature flag is enabled', () => {
+ const lastMonthValue = 'lastMonthValue';
+ const mockDateRange = {
+ value: lastMonthValue,
+ startDate: new Date('2023-08-08T00:00:00.000Z'),
+ endDate: new Date('2023-09-08T00:00:00.000Z'),
+ };
+
+ beforeEach(() => {
+ wrapper = createComponent({ featureFlags: { vsaPredefinedDateRanges: true } });
+ });
+
+ it('should render date ranges dropdown', () => {
+ expect(findDateRangesDropdown().exists()).toBe(true);
+ });
+
+ it('should not render date range picker', () => {
+ expect(findDateRangePicker().exists()).toBe(false);
+ });
+
+ describe('when a date range is selected from the dropdown', () => {
+ describe('predefined date range option', () => {
+ beforeEach(async () => {
+ findDateRangesDropdown().vm.$emit('selected', mockDateRange);
+
+ await nextTick();
+ });
+
+ it('should emit `setDateRange` with date range', () => {
+ const { value, ...dateRange } = mockDateRange;
+
+ expect(wrapper.emitted('setDateRange')).toEqual([[dateRange]]);
+ });
+
+ it('should emit `setPredefinedDateRange` with correct value', () => {
+ expect(wrapper.emitted('setPredefinedDateRange')).toEqual([[lastMonthValue]]);
+ });
+ });
+
+ describe('custom date range option', () => {
+ beforeEach(async () => {
+ findDateRangesDropdown().vm.$emit('customDateRangeSelected');
+
+ await nextTick();
+ });
+
+ it('should emit `setPredefinedDateRange` with custom date range value', () => {
+ expect(wrapper.emitted('setPredefinedDateRange')).toEqual([[DATE_RANGE_CUSTOM_VALUE]]);
+ });
+
+ it('should not emit `setDateRange`', () => {
+ expect(wrapper.emitted('setDateRange')).toBeUndefined();
+ });
+ });
+ });
+
+ describe.each`
+ predefinedDateRange | shouldRenderDateRangePicker | dateRangeType
+ ${DATE_RANGE_CUSTOM_VALUE} | ${true} | ${'custom date range'}
+ ${lastMonthValue} | ${false} | ${'predefined date range'}
+ `(
+ 'when the `predefinedDateRange` prop is set to a $dateRangeType',
+ ({ predefinedDateRange, shouldRenderDateRangePicker }) => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ props: { predefinedDateRange },
+ featureFlags: { vsaPredefinedDateRanges: true },
+ });
+ });
+
+ it("should be passed into the dropdown's `selected` prop", () => {
+ expect(findDateRangesDropdown().props('selected')).toBe(predefinedDateRange);
+ });
+
+ it(`should ${
+ shouldRenderDateRangePicker ? '' : 'not'
+ } render the date range picker`, () => {
+ expect(findDateRangePicker().exists()).toBe(shouldRenderDateRangePicker);
+ });
+ },
+ );
+
+ describe('when the `predefinedDateRange` prop is null', () => {
+ const laterStartDate = new Date('2018-12-01T00:00:00.000Z');
+ const earlierStartDate = new Date('2019-01-01T00:00:00.000Z');
+ const customEndDate = new Date('2019-02-01T00:00:00.000Z');
+
+ describe.each`
+ dateRange | expectedDateRangeOption | shouldRenderDateRangePicker | description
+ ${{ startDate, endDate }} | ${DATE_RANGE_LAST_30_DAYS_VALUE} | ${false} | ${'is default'}
+ ${{ startDate: laterStartDate, endDate }} | ${DATE_RANGE_CUSTOM_VALUE} | ${true} | ${'has a later start date than the default'}
+ ${{ startDate: earlierStartDate, endDate }} | ${DATE_RANGE_CUSTOM_VALUE} | ${true} | ${'has an earlier start date than the default'}
+ ${{ startDate, endDate: customEndDate }} | ${DATE_RANGE_CUSTOM_VALUE} | ${true} | ${'has an end date that is not today'}
+ `(
+ 'date range $description',
+ ({ dateRange, expectedDateRangeOption, shouldRenderDateRangePicker }) => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ props: { predefinedDateRange: null, ...dateRange },
+ featureFlags: { vsaPredefinedDateRanges: true },
+ });
+ });
+
+ it("should set the dropdown's `selected` prop to the correct value", () => {
+ expect(findDateRangesDropdown().props('selected')).toBe(expectedDateRangeOption);
+ });
+
+ it(`should ${
+ shouldRenderDateRangePicker ? '' : 'not'
+ } render the date range picker`, () => {
+ expect(findDateRangePicker().exists()).toBe(shouldRenderDateRangePicker);
+ });
+ },
+ );
+ });
+
+ describe('hasPredefinedDateRangesFilter = false', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ props: { hasPredefinedDateRangesFilter: false },
+ featureFlags: { vsaPredefinedDateRanges: true },
+ });
+ });
+
+ it('should not render the date ranges dropdown', () => {
+ expect(findDateRangesDropdown().exists()).toBe(false);
+ });
+ });
+
+ describe('hasDateRangeFilter = false', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ props: { hasDateRangeFilter: false },
+ featureFlags: { vsaPredefinedDateRanges: true },
+ });
+ });
+
+ it('should not render the date range picker', () => {
+ expect(findDateRangePicker().exists()).toBe(false);
+ });
+
+ it('should remove custom date range option from date ranges dropdown', () => {
+ expect(findDateRangesDropdown().props('includeCustomDateRangeOption')).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/analytics/cycle_analytics/mock_data.js b/spec/frontend/analytics/cycle_analytics/mock_data.js
index f9587bf1967..7ad95cab9ad 100644
--- a/spec/frontend/analytics/cycle_analytics/mock_data.js
+++ b/spec/frontend/analytics/cycle_analytics/mock_data.js
@@ -261,3 +261,5 @@ export const basePaginationResult = {
direction: PAGINATION_SORT_DIRECTION_DESC,
page: null,
};
+
+export const predefinedDateRange = 'last_week';
diff --git a/spec/frontend/analytics/cycle_analytics/store/actions_spec.js b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
index b2ce8596c22..c3551d3da6f 100644
--- a/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
@@ -14,6 +14,7 @@ import {
initialPaginationState,
reviewEvents,
projectNamespace as namespace,
+ predefinedDateRange,
} from '../mock_data';
const { path: groupPath } = currentGroup;
@@ -32,6 +33,7 @@ const defaultState = {
createdAfter,
createdBefore,
pagination: initialPaginationState,
+ predefinedDateRange,
};
describe('Project Value Stream Analytics actions', () => {
@@ -53,6 +55,7 @@ describe('Project Value Stream Analytics actions', () => {
describe.each`
action | payload | expectedActions | expectedMutations
${'setDateRange'} | ${{ createdAfter, createdBefore }} | ${[{ type: 'refetchStageData' }]} | ${[mockSetDateActionCommit]}
+ ${'setPredefinedDateRange'} | ${{ predefinedDateRange }} | ${[]} | ${[{ type: 'SET_PREDEFINED_DATE_RANGE', payload: { predefinedDateRange } }]}
${'setFilters'} | ${[]} | ${[{ type: 'refetchStageData' }]} | ${[]}
${'setSelectedStage'} | ${{ selectedStage }} | ${[{ type: 'refetchStageData' }]} | ${[{ type: 'SET_SELECTED_STAGE', payload: { selectedStage } }]}
${'setSelectedValueStream'} | ${{ selectedValueStream }} | ${[{ type: 'fetchValueStreamStages' }]} | ${[{ type: 'SET_SELECTED_VALUE_STREAM', payload: { selectedValueStream } }]}
diff --git a/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
index 70b7454f4a0..25fed2b1714 100644
--- a/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/mutations_spec.js
@@ -18,6 +18,7 @@ import {
stageCounts,
initialPaginationState as pagination,
projectNamespace as mockNamespace,
+ predefinedDateRange,
} from '../mock_data';
let state;
@@ -94,6 +95,7 @@ describe('Project Value Stream Analytics mutations', () => {
mutation | payload | stateKey | value
${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdAfter'} | ${mockCreatedAfter}
${types.SET_DATE_RANGE} | ${mockSetDatePayload} | ${'createdBefore'} | ${mockCreatedBefore}
+ ${types.SET_PREDEFINED_DATE_RANGE} | ${predefinedDateRange} | ${'predefinedDateRange'} | ${predefinedDateRange}
${types.SET_LOADING} | ${true} | ${'isLoading'} | ${true}
${types.SET_LOADING} | ${false} | ${'isLoading'} | ${false}
${types.SET_SELECTED_VALUE_STREAM} | ${selectedValueStream} | ${'selectedValueStream'} | ${selectedValueStream}
diff --git a/spec/frontend/analytics/shared/components/date_ranges_dropdown_spec.js b/spec/frontend/analytics/shared/components/date_ranges_dropdown_spec.js
new file mode 100644
index 00000000000..63407900be7
--- /dev/null
+++ b/spec/frontend/analytics/shared/components/date_ranges_dropdown_spec.js
@@ -0,0 +1,165 @@
+import { nextTick } from 'vue';
+import { GlCollapsibleListbox, GlIcon } from '@gitlab/ui';
+import DateRangesDropdown from '~/analytics/shared/components/date_ranges_dropdown.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+describe('DateRangesDropdown', () => {
+ let wrapper;
+
+ const customDateRangeValue = 'custom';
+ const lastWeekValue = 'lastWeek';
+ const last30DaysValue = 'lastMonth';
+ const mockLastWeek = {
+ text: 'Last week',
+ value: lastWeekValue,
+ startDate: new Date('2023-09-08T00:00:00.000Z'),
+ endDate: new Date('2023-09-14T00:00:00.000Z'),
+ };
+ const mockLast30Days = {
+ text: 'Last month',
+ value: last30DaysValue,
+ startDate: new Date('2023-08-16T00:00:00.000Z'),
+ endDate: new Date('2023-09-14T00:00:00.000Z'),
+ };
+ const mockCustomDateRangeItem = {
+ text: 'Custom',
+ value: customDateRangeValue,
+ };
+ const mockDateRanges = [mockLastWeek, mockLast30Days];
+ const mockItems = mockDateRanges.map(({ text, value }) => ({ text, value }));
+ const mockTooltipText = 'Max date range is 180 days';
+
+ const createComponent = ({ props = {}, dateRangeOptions = mockDateRanges } = {}) => {
+ wrapper = shallowMountExtended(DateRangesDropdown, {
+ propsData: {
+ dateRangeOptions,
+ ...props,
+ },
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ });
+ };
+
+ const findListBox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findDaysSelectedCount = () => wrapper.findByTestId('predefined-date-range-days-count');
+ const findHelpIcon = () => wrapper.findComponent(GlIcon);
+
+ describe('default state', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should pass items to listbox `items` prop in correct order', () => {
+ expect(findListBox().props('items')).toStrictEqual([...mockItems, mockCustomDateRangeItem]);
+ });
+
+ it('should display first option as selected', () => {
+ expect(findListBox().props('selected')).toBe(lastWeekValue);
+ });
+
+ it('should not display info icon', () => {
+ expect(findHelpIcon().exists()).toBe(false);
+ });
+
+ describe.each`
+ dateRangeValue | dateRangeItem
+ ${lastWeekValue} | ${mockLastWeek}
+ ${last30DaysValue} | ${mockLast30Days}
+ `('when $dateRangeValue date range is selected', ({ dateRangeValue, dateRangeItem }) => {
+ beforeEach(async () => {
+ findListBox().vm.$emit('select', dateRangeValue);
+
+ await nextTick();
+ });
+
+ it('should emit `selected` event with value and date range', () => {
+ const { text, ...dateRangeProps } = dateRangeItem;
+
+ expect(wrapper.emitted('selected')).toEqual([[dateRangeProps]]);
+ });
+
+ it('should display days selected indicator', () => {
+ expect(findDaysSelectedCount().exists()).toBe(true);
+ });
+
+ it('should not emit `customDateRangeSelected` event', () => {
+ expect(wrapper.emitted('customDateRangeSelected')).toBeUndefined();
+ });
+ });
+
+ describe('when the custom date range option is selected', () => {
+ beforeEach(async () => {
+ findListBox().vm.$emit('select', customDateRangeValue);
+
+ await nextTick();
+ });
+
+ it('should emit `customDateRangeSelected` event', () => {
+ expect(wrapper.emitted('customDateRangeSelected')).toHaveLength(1);
+ });
+
+ it('should hide days selected indicator', () => {
+ expect(findDaysSelectedCount().exists()).toBe(false);
+ });
+
+ it('should not emit `selected` event', () => {
+ expect(wrapper.emitted('selected')).toBeUndefined();
+ });
+ });
+ });
+
+ describe('when a date range is preselected', () => {
+ beforeEach(() => {
+ createComponent({ props: { selected: 'lastMonth' } });
+ });
+
+ it('should display preselected date range as selected in listbox', () => {
+ expect(findListBox().props('selected')).toBe(last30DaysValue);
+ });
+ });
+
+ describe('days selected indicator', () => {
+ it.each`
+ selected | includeEndDateInDaysSelected | expectedDaysCount
+ ${lastWeekValue} | ${true} | ${7}
+ ${last30DaysValue} | ${true} | ${30}
+ ${lastWeekValue} | ${false} | ${6}
+ ${last30DaysValue} | ${false} | ${29}
+ `(
+ 'should display correct days selected when includeEndDateInDaysSelected=$includeEndDateInDaysSelected',
+ ({ selected, includeEndDateInDaysSelected, expectedDaysCount }) => {
+ createComponent({ props: { selected, includeEndDateInDaysSelected } });
+
+ expect(wrapper.findByText(`${expectedDaysCount} days selected`).exists()).toBe(true);
+ },
+ );
+ });
+
+ describe('when the `tooltip` prop is set', () => {
+ beforeEach(() => {
+ createComponent({ props: { tooltip: mockTooltipText } });
+ });
+
+ it('should display info icon with tooltip', () => {
+ const helpIcon = findHelpIcon();
+ const tooltip = getBinding(helpIcon.element, 'gl-tooltip');
+
+ expect(helpIcon.props('name')).toBe('information-o');
+ expect(helpIcon.attributes('title')).toBe(mockTooltipText);
+
+ expect(tooltip).toBeDefined();
+ });
+ });
+
+ describe('when `includeCustomDateRangeOption` = false', () => {
+ beforeEach(() => {
+ createComponent({ props: { includeCustomDateRangeOption: false } });
+ });
+
+ it('should pass items without custom date range option to listbox `items` prop', () => {
+ expect(findListBox().props('items')).toEqual(mockItems);
+ });
+ });
+});
diff --git a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
index 802da47d6cd..15f5759752d 100644
--- a/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
+++ b/spec/frontend/analytics/shared/components/projects_dropdown_filter_spec.js
@@ -26,6 +26,7 @@ const projects = [
avatarUrl: null,
},
];
+const groupNamespace = 'gitlab-org';
const defaultMocks = {
$apollo: {
@@ -46,7 +47,7 @@ describe('ProjectsDropdownFilter component', () => {
mocks: { ...defaultMocks },
propsData: {
groupId: 1,
- groupNamespace: 'gitlab-org',
+ groupNamespace,
...props,
},
stubs: {
@@ -93,34 +94,50 @@ describe('ProjectsDropdownFilter component', () => {
const findSelectedButtonAvatarItemAtIndex = (index) =>
findSelectedDropdownAtIndex(index).find('img.gl-avatar');
- describe('queryParams are applied when fetching data', () => {
+ describe('when fetching data', () => {
+ const mockQueryParams = {
+ first: 50,
+ includeSubgroups: true,
+ };
+
+ const mockVariables = {
+ groupFullPath: groupNamespace,
+ ...mockQueryParams,
+ };
+
beforeEach(() => {
createComponent({
props: {
- queryParams: {
- first: 50,
- includeSubgroups: true,
- },
+ queryParams: mockQueryParams,
},
});
+
+ spyQuery.mockClear();
});
- it('applies the correct queryParams when making an api call', async () => {
+ it('should apply the correct queryParams when making an API call', async () => {
findDropdown().vm.$emit('search', 'gitlab');
+ await waitForPromises();
+
expect(spyQuery).toHaveBeenCalledTimes(1);
- await nextTick();
- expect(spyQuery).toHaveBeenCalledWith({
+ expect(spyQuery).toHaveBeenLastCalledWith({
query: getProjects,
variables: {
search: 'gitlab',
- groupFullPath: wrapper.vm.groupNamespace,
- first: 50,
- includeSubgroups: true,
+ ...mockVariables,
},
});
});
+
+ it('should not make an API call when search query is below minimum search length', async () => {
+ findDropdown().vm.$emit('search', 'hi');
+
+ await waitForPromises();
+
+ expect(spyQuery).toHaveBeenCalledTimes(0);
+ });
});
describe('highlighted items', () => {
@@ -230,6 +247,31 @@ describe('ProjectsDropdownFilter component', () => {
});
});
+ describe('with an array of projects passed to `defaultProjects` and a search term', () => {
+ const { name: searchQuery } = projects[2];
+
+ beforeEach(async () => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ defaultProjects: [projects[0], projects[1]],
+ multiSelect: true,
+ },
+ });
+
+ await waitForPromises();
+
+ findDropdown().vm.$emit('search', searchQuery);
+ });
+
+ it('should add search result to selected projects when selected', async () => {
+ await selectDropdownItemAtIndex([0, 1, 2]);
+
+ expect(findSelectedDropdownItems()).toHaveLength(3);
+ expect(findDropdownButton().text()).toBe('3 projects selected');
+ });
+ });
+
describe('when multiSelect is false', () => {
const blockDefaultProps = { multiSelect: false };
beforeEach(() => {
diff --git a/spec/frontend/batch_comments/components/preview_dropdown_spec.js b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
index 608e9c82961..c0ad40b75ad 100644
--- a/spec/frontend/batch_comments/components/preview_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
@@ -16,7 +16,7 @@ Vue.use(Vuex);
let wrapper;
-const setCurrentFileHash = jest.fn();
+const goToFile = jest.fn();
const scrollToDraft = jest.fn();
const findPreviewItem = () => wrapper.findComponent(PreviewItem);
@@ -27,7 +27,7 @@ function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts =
diffs: {
namespaced: true,
actions: {
- setCurrentFileHash,
+ goToFile,
},
state: {
viewDiffsFileByFile,
@@ -59,12 +59,12 @@ describe('Batch comments preview dropdown', () => {
it('toggles active file when viewDiffsFileByFile is true', async () => {
factory({
viewDiffsFileByFile: true,
- sortedDrafts: [{ id: 1, file_hash: 'hash' }],
+ sortedDrafts: [{ id: 1, file_hash: 'hash', file_path: 'foo' }],
});
findPreviewItem().trigger('click');
await nextTick();
- expect(setCurrentFileHash).toHaveBeenCalledWith(expect.anything(), 'hash');
+ expect(goToFile).toHaveBeenCalledWith(expect.anything(), { path: 'foo' });
await nextTick();
expect(scrollToDraft).toHaveBeenCalledWith(
diff --git a/spec/frontend/behaviors/autosize_spec.js b/spec/frontend/behaviors/autosize_spec.js
deleted file mode 100644
index 7008b7b2eb6..00000000000
--- a/spec/frontend/behaviors/autosize_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import '~/behaviors/autosize';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-
-jest.mock('~/helpers/startup_css_helper', () => {
- return {
- 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);
- }),
- };
-});
-
-describe('Autosize behavior', () => {
- beforeEach(() => {
- setHTMLFixture('<textarea class="js-autosize"></textarea>');
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('is applied to the textarea', () => {
- // 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 jest.runOnlyPendingTimers here, the querySelector
- // would not run and the test would fail.
- jest.runOnlyPendingTimers();
-
- const textarea = document.querySelector('textarea');
- expect(textarea.classList).toContain('js-autosize-initialized');
- });
-});
diff --git a/spec/frontend/behaviors/components/global_alerts_spec.js b/spec/frontend/behaviors/components/global_alerts_spec.js
new file mode 100644
index 00000000000..4a20805c9a6
--- /dev/null
+++ b/spec/frontend/behaviors/components/global_alerts_spec.js
@@ -0,0 +1,135 @@
+import { nextTick } from 'vue';
+import { GlAlert } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import GlobalAlerts from '~/behaviors/components/global_alerts.vue';
+import { getGlobalAlerts, setGlobalAlerts, removeGlobalAlertById } from '~/lib/utils/global_alerts';
+
+jest.mock('~/lib/utils/global_alerts');
+
+describe('GlobalAlerts', () => {
+ const alert1 = {
+ dismissible: true,
+ persistOnPages: [],
+ id: 'foo',
+ variant: 'success',
+ title: 'Foo title',
+ message: 'Foo',
+ };
+ const alert2 = {
+ dismissible: true,
+ persistOnPages: [],
+ id: 'bar',
+ variant: 'danger',
+ message: 'Bar',
+ };
+ const alert3 = {
+ dismissible: true,
+ persistOnPages: ['dashboard:groups:index'],
+ id: 'baz',
+ variant: 'info',
+ message: 'Baz',
+ };
+
+ let wrapper;
+
+ const createComponent = async () => {
+ wrapper = shallowMountExtended(GlobalAlerts);
+ await nextTick();
+ };
+
+ const findAllAlerts = () => wrapper.findAllComponents(GlAlert);
+
+ describe('when there are alerts to display', () => {
+ beforeEach(() => {
+ getGlobalAlerts.mockImplementationOnce(() => [alert1, alert2]);
+ });
+
+ it('displays alerts and removes them from session storage', async () => {
+ await createComponent();
+
+ const alerts = findAllAlerts();
+
+ expect(alerts.at(0).text()).toBe('Foo');
+ expect(alerts.at(0).props()).toMatchObject({
+ title: 'Foo title',
+ variant: 'success',
+ dismissible: true,
+ });
+
+ expect(alerts.at(1).text()).toBe('Bar');
+ expect(alerts.at(1).props()).toMatchObject({
+ variant: 'danger',
+ dismissible: true,
+ });
+
+ expect(setGlobalAlerts).toHaveBeenCalledWith([]);
+ });
+
+ describe('when alert is dismissed', () => {
+ it('removes alert', async () => {
+ await createComponent();
+
+ wrapper.findComponent(GlAlert).vm.$emit('dismiss');
+ await nextTick();
+
+ expect(findAllAlerts().length).toBe(1);
+ expect(removeGlobalAlertById).toHaveBeenCalledWith(alert1.id);
+ });
+ });
+ });
+
+ describe('when alert has `persistOnPages` key set', () => {
+ const alerts = [alert3];
+
+ beforeEach(() => {
+ getGlobalAlerts.mockImplementationOnce(() => alerts);
+ });
+
+ describe('when page matches specified page', () => {
+ beforeEach(() => {
+ document.body.dataset.page = 'dashboard:groups:index';
+ });
+
+ afterEach(() => {
+ delete document.body.dataset.page;
+ });
+
+ it('renders alert and does not remove it from session storage', async () => {
+ await createComponent();
+
+ expect(wrapper.findComponent(GlAlert).text()).toBe('Baz');
+ expect(setGlobalAlerts).toHaveBeenCalledWith(alerts);
+ });
+ });
+
+ describe('when page does not match specified page', () => {
+ beforeEach(() => {
+ document.body.dataset.page = 'dashboard:groups:show';
+ });
+
+ afterEach(() => {
+ delete document.body.dataset.page;
+ });
+
+ it('does not render alert and does not remove it from session storage', async () => {
+ await createComponent();
+
+ expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+ expect(setGlobalAlerts).toHaveBeenCalledWith(alerts);
+ });
+ });
+ });
+
+ describe('when there are no alerts to display', () => {
+ beforeEach(() => {
+ getGlobalAlerts.mockImplementationOnce(() => []);
+ });
+
+ it('renders nothing', async () => {
+ await createComponent();
+
+ expect(wrapper.html()).toBe('');
+ });
+ });
+});
diff --git a/spec/frontend/behaviors/components/json_table_spec.js b/spec/frontend/behaviors/components/json_table_spec.js
index ae62d28d6c0..3277e58669a 100644
--- a/spec/frontend/behaviors/components/json_table_spec.js
+++ b/spec/frontend/behaviors/components/json_table_spec.js
@@ -70,7 +70,7 @@ describe('behaviors/components/json_table', () => {
});
it('renders gltable', () => {
- expect(findTable().props()).toEqual({
+ expect(findTable().props()).toMatchObject({
fields: [],
items: [],
});
@@ -121,7 +121,7 @@ describe('behaviors/components/json_table', () => {
});
it('passes cleaned fields and items to table', () => {
- expect(findTable().props()).toEqual({
+ expect(findTable().props()).toMatchObject({
fields: [
'A',
{
diff --git a/spec/frontend/behaviors/markdown/render_observability_spec.js b/spec/frontend/behaviors/markdown/render_observability_spec.js
deleted file mode 100644
index f464c01ac15..00000000000
--- a/spec/frontend/behaviors/markdown/render_observability_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import Vue from 'vue';
-import { createWrapper } from '@vue/test-utils';
-import renderObservability from '~/behaviors/markdown/render_observability';
-import { INLINE_EMBED_DIMENSIONS, SKELETON_VARIANT_EMBED } from '~/observability/constants';
-import ObservabilityApp from '~/observability/components/observability_app.vue';
-
-describe('renderObservability', () => {
- let subject;
-
- beforeEach(() => {
- subject = document.createElement('div');
- subject.classList.add('js-render-observability');
- subject.dataset.frameUrl = 'https://observe.gitlab.com/';
- document.body.appendChild(subject);
- });
-
- afterEach(() => {
- subject.remove();
- });
-
- it('should return an array of Vue instances', () => {
- const vueInstances = renderObservability([
- ...document.querySelectorAll('.js-render-observability'),
- ]);
- expect(vueInstances).toEqual([expect.any(Vue)]);
- });
-
- it('should correctly pass props to the ObservabilityApp component', () => {
- const vueInstances = renderObservability([
- ...document.querySelectorAll('.js-render-observability'),
- ]);
-
- const wrapper = createWrapper(vueInstances[0]);
-
- expect(wrapper.findComponent(ObservabilityApp).props()).toMatchObject({
- observabilityIframeSrc: 'https://observe.gitlab.com/',
- skeletonVariant: SKELETON_VARIANT_EMBED,
- inlineEmbed: true,
- height: INLINE_EMBED_DIMENSIONS.HEIGHT,
- width: INLINE_EMBED_DIMENSIONS.WIDTH,
- });
- });
-});
diff --git a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
index 292a0da2bfe..f32dd902b8e 100644
--- a/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
+++ b/spec/frontend/blob/components/__snapshots__/blob_header_filepath_spec.js.snap
@@ -13,7 +13,7 @@ exports[`Blob Header Filepath rendering matches the snapshot 1`] = `
/>
<strong
class="file-title-name js-blob-header-filepath mr-1"
- data-qa-selector="file_title_content"
+ data-testid="file-title-content"
>
foo/bar/dummy.md
</strong>
diff --git a/spec/frontend/blob/components/blob_header_default_actions_spec.js b/spec/frontend/blob/components/blob_header_default_actions_spec.js
index 4c8c256121f..cc4c13060a5 100644
--- a/spec/frontend/blob/components/blob_header_default_actions_spec.js
+++ b/spec/frontend/blob/components/blob_header_default_actions_spec.js
@@ -35,7 +35,7 @@ describe('Blob Header Default Actions', () => {
});
describe('renders', () => {
- const findCopyButton = () => wrapper.findByTestId('copyContentsButton');
+ const findCopyButton = () => wrapper.findByTestId('copy-contents-button');
const findViewRawButton = () => wrapper.findByTestId('viewRawButton');
it('gl-button-group component', () => {
diff --git a/spec/frontend/blob/csv/csv_viewer_spec.js b/spec/frontend/blob/csv/csv_viewer_spec.js
index 8f105f04aa7..04d11011e70 100644
--- a/spec/frontend/blob/csv/csv_viewer_spec.js
+++ b/spec/frontend/blob/csv/csv_viewer_spec.js
@@ -1,10 +1,12 @@
-import { GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { GlLoadingIcon, GlTable, GlButton } from '@gitlab/ui';
import { getAllByRole } from '@testing-library/dom';
import { shallowMount, mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import Papa from 'papaparse';
import CsvViewer from '~/blob/csv/csv_viewer.vue';
import PapaParseAlert from '~/vue_shared/components/papa_parse_alert.vue';
+import { s__ } from '~/locale';
+import { MAX_ROWS_TO_RENDER } from '~/blob/csv/constants';
const validCsv = 'one,two,three';
const brokenCsv = '{\n "json": 1,\n "key": [1, 2, 3]\n}';
@@ -28,6 +30,8 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
const findCsvTable = () => wrapper.findComponent(GlTable);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(PapaParseAlert);
+ const findSwitchToRawViewBtn = () => wrapper.findComponent(GlButton);
+ const findLargeCsvText = () => wrapper.find('[data-testid="large-csv-text"]');
it('should render loading spinner', () => {
createComponent();
@@ -76,6 +80,33 @@ describe('app/assets/javascripts/blob/csv/csv_viewer.vue', () => {
});
});
+ describe('when the CSV is larger than 2000 lines', () => {
+ beforeEach(async () => {
+ const largeCsv = validCsv.repeat(3000);
+ jest.spyOn(Papa, 'parse').mockImplementation(() => {
+ return { data: largeCsv.split(','), errors: [] };
+ });
+ createComponent({ csv: largeCsv });
+ await nextTick();
+ });
+ it('renders not more than max rows value', () => {
+ expect(Papa.parse).toHaveBeenCalledTimes(1);
+ expect(wrapper.vm.items).toHaveLength(MAX_ROWS_TO_RENDER);
+ });
+ it('renders large csv text', () => {
+ expect(findLargeCsvText().text()).toBe(
+ s__(
+ 'CsvViewer|The file is too large to render all the rows. To see the entire file, switch to the raw view.',
+ ),
+ );
+ });
+ it('renders button with link to raw view', () => {
+ const url = 'http://test.host/?plain=1';
+ expect(findSwitchToRawViewBtn().text()).toBe(s__('CsvViewer|View raw data'));
+ expect(findSwitchToRawViewBtn().attributes('href')).toBe(url);
+ });
+ });
+
describe('when csv prop is path and indicates a remote file', () => {
it('should render call parse with download flag true', async () => {
const path = 'path/to/remote/file.csv';
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 95b5712bab0..8314cbda7a1 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -10,6 +10,7 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
+import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
import eventHub from '~/boards/eventhub';
import defaultStore from '~/boards/stores';
@@ -63,17 +64,23 @@ describe('Board card component', () => {
actions: {
performSearch: performSearchMock,
},
- state: {
- ...defaultStore.state,
- isShowingLabels: true,
- },
+ state: defaultStore.state,
});
};
+ const mockApollo = createMockApollo();
+
const createWrapper = ({ props = {}, isEpicBoard = false, isGroupBoard = true } = {}) => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: isShowingLabelsQuery,
+ data: {
+ isShowingLabels: true,
+ },
+ });
+
wrapper = mountExtended(BoardCardInner, {
store,
- apolloProvider: createMockApollo(),
+ apolloProvider: mockApollo,
propsData: {
list,
item: issue,
@@ -235,7 +242,7 @@ describe('Board card component', () => {
expect(tooltip).toBeDefined();
expect(findHiddenIssueIcon().attributes('title')).toBe(
- 'This issue is hidden because its author has been banned',
+ 'This issue is hidden because its author has been banned.',
);
});
});
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index 7367b34c4df..5bafd9a8d0e 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -122,5 +122,7 @@ export default function createComponent({
},
});
+ jest.spyOn(store, 'dispatch').mockImplementation(() => {});
+
return component;
}
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index e0a110678b1..30bb4fba4e3 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -202,8 +202,6 @@ describe('Board list component', () => {
describe('handleDragOnEnd', () => {
beforeEach(() => {
- jest.spyOn(wrapper.vm, 'moveItem').mockImplementation(() => {});
-
startDrag();
});
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index f0d40af94fe..11f9a4f6ff2 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -4,11 +4,14 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import BoardCard from '~/boards/components/board_card.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
import { inactiveId } from '~/boards/constants';
+import selectedBoardItemsQuery from '~/boards/graphql/client/selected_board_items.query.graphql';
+import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
import { mockLabelList, mockIssue, DEFAULT_COLOR } from '../mock_data';
describe('Board card', () => {
@@ -20,9 +23,11 @@ describe('Board card', () => {
Vue.use(VueApollo);
const mockSetActiveBoardItemResolver = jest.fn();
+ const mockSetSelectedBoardItemsResolver = jest.fn();
const mockApollo = createMockApollo([], {
Mutation: {
setActiveBoardItem: mockSetActiveBoardItemResolver,
+ setSelectedBoardItems: mockSetSelectedBoardItemsResolver,
},
});
@@ -49,7 +54,21 @@ describe('Board card', () => {
provide = {},
stubs = { BoardCardInner },
item = mockIssue,
+ selectedBoardItems = [],
} = {}) => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: isShowingLabelsQuery,
+ data: {
+ isShowingLabels: true,
+ },
+ });
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: selectedBoardItemsQuery,
+ data: {
+ selectedBoardItems,
+ },
+ });
+
wrapper = shallowMountExtended(BoardCard, {
apolloProvider: mockApollo,
stubs: {
@@ -99,7 +118,7 @@ describe('Board card', () => {
describe('when GlLabel is clicked in BoardCardInner', () => {
it('doesnt call toggleBoardItem', () => {
- createStore({ initialState: { isShowingLabels: true } });
+ createStore();
mountComponent();
wrapper.findComponent(GlLabel).trigger('mouseup');
@@ -132,10 +151,9 @@ describe('Board card', () => {
createStore({
initialState: {
activeId: inactiveId,
- selectedBoardItems: [mockIssue],
},
});
- mountComponent();
+ mountComponent({ selectedBoardItems: [mockIssue.id] });
expect(wrapper.classes()).toContain('multi-select');
expect(wrapper.classes()).not.toContain('is-active');
@@ -163,13 +181,17 @@ describe('Board card', () => {
window.gon = { features: { boardMultiSelect: true } };
});
- it('should call vuex action "multiSelectBoardItem" with correct parameters', async () => {
+ it('should call setSelectedBoardItemsMutation with correct parameters', async () => {
await multiSelectCard();
- expect(mockActions.toggleBoardItemMultiSelection).toHaveBeenCalledTimes(1);
- expect(mockActions.toggleBoardItemMultiSelection).toHaveBeenCalledWith(
+ expect(mockSetSelectedBoardItemsResolver).toHaveBeenCalledTimes(1);
+ expect(mockSetSelectedBoardItemsResolver).toHaveBeenCalledWith(
expect.any(Object),
- mockIssue,
+ {
+ itemId: mockIssue.id,
+ },
+ expect.anything(),
+ expect.anything(),
);
});
});
@@ -240,6 +262,7 @@ describe('Board card', () => {
it('set active board item on client when clicking on card', async () => {
await selectCard();
+ await waitForPromises();
expect(mockSetActiveBoardItemResolver).toHaveBeenCalledWith(
{},
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index 15ee3976fb1..a0dacf085e2 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -14,6 +14,7 @@ import createBoardMutation from '~/boards/graphql/board_create.mutation.graphql'
import destroyBoardMutation from '~/boards/graphql/board_destroy.mutation.graphql';
import updateBoardMutation from '~/boards/graphql/board_update.mutation.graphql';
import eventHub from '~/boards/eventhub';
+import * as cacheUpdates from '~/boards/graphql/cache_updates';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -55,12 +56,10 @@ describe('BoardForm', () => {
const findInput = () => wrapper.find('#board-new-name');
const setBoardMock = jest.fn();
- const setErrorMock = jest.fn();
const store = new Vuex.Store({
actions: {
setBoard: setBoardMock,
- setError: setErrorMock,
},
});
@@ -113,6 +112,10 @@ describe('BoardForm', () => {
});
};
+ beforeEach(() => {
+ cacheUpdates.setError = jest.fn();
+ });
+
describe('when user can not admin the board', () => {
beforeEach(() => {
createComponent({
@@ -237,7 +240,7 @@ describe('BoardForm', () => {
await waitForPromises();
expect(setBoardMock).not.toHaveBeenCalled();
- expect(setErrorMock).toHaveBeenCalled();
+ expect(cacheUpdates.setError).toHaveBeenCalled();
});
describe('when Apollo boards FF is on', () => {
@@ -353,7 +356,7 @@ describe('BoardForm', () => {
await waitForPromises();
expect(setBoardMock).not.toHaveBeenCalled();
- expect(setErrorMock).toHaveBeenCalled();
+ expect(cacheUpdates.setError).toHaveBeenCalled();
});
describe('when Apollo boards FF is on', () => {
@@ -434,9 +437,11 @@ describe('BoardForm', () => {
await waitForPromises();
expect(visitUrl).not.toHaveBeenCalled();
- expect(store.dispatch).toHaveBeenCalledWith('setError', {
- message: 'Failed to delete board. Please try again.',
- });
+ expect(cacheUpdates.setError).toHaveBeenCalledWith(
+ expect.objectContaining({
+ message: 'Failed to delete board. Please try again.',
+ }),
+ );
});
});
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index fa18b47cf54..0a628af9939 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlLoadingIcon, GlDropdownSectionHeader } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
// eslint-disable-next-line no-restricted-imports
@@ -13,7 +13,7 @@ import projectRecentBoardsQuery from '~/boards/graphql/project_recent_boards.que
import * as cacheUpdates from '~/boards/graphql/cache_updates';
import { WORKSPACE_GROUP, WORKSPACE_PROJECT } from '~/issues/constants';
import createMockApollo from 'helpers/mock_apollo_helper';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
mockBoard,
mockGroupAllBoardsResponse,
@@ -47,17 +47,11 @@ describe('BoardsSelector', () => {
});
};
- const fillSearchBox = (filterTerm) => {
- const searchBox = wrapper.findComponent({ ref: 'searchBox' });
- const searchBoxInput = searchBox.find('input');
- searchBoxInput.setValue(filterTerm);
- searchBoxInput.trigger('input');
- };
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
- const getDropdownItems = () => wrapper.findAllByTestId('dropdown-item');
- const getDropdownHeaders = () => wrapper.findAllComponents(GlDropdownSectionHeader);
- const getLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const fillSearchBox = async (filterTerm) => {
+ await findDropdown().vm.$emit('search', filterTerm);
+ };
const projectBoardsQueryHandlerSuccess = jest
.fn()
@@ -96,7 +90,7 @@ describe('BoardsSelector', () => {
[groupRecentBoardsQuery, groupRecentBoardsQueryHandlerSuccess],
]);
- wrapper = mountExtended(BoardsSelector, {
+ wrapper = shallowMountExtended(BoardsSelector, {
store,
apolloProvider: fakeApollo,
propsData: {
@@ -142,13 +136,19 @@ describe('BoardsSelector', () => {
});
it('shows loading spinner', async () => {
+ createComponent({
+ provide: {
+ isApolloBoard: true,
+ },
+ props: {
+ isCurrentBoardLoading: true,
+ },
+ });
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
- findDropdown().vm.$emit('show');
+ findDropdown().vm.$emit('shown');
await nextTick();
- expect(getLoadingIcon().exists()).toBe(true);
- expect(getDropdownHeaders()).toHaveLength(0);
- expect(getDropdownItems()).toHaveLength(0);
+ expect(findDropdown().props('loading')).toBe(true);
});
});
@@ -158,7 +158,7 @@ describe('BoardsSelector', () => {
await nextTick();
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
- findDropdown().vm.$emit('show');
+ findDropdown().vm.$emit('shown');
await nextTick();
});
@@ -167,9 +167,8 @@ describe('BoardsSelector', () => {
expect(projectBoardsQueryHandlerSuccess).toHaveBeenCalled();
});
- it('hides loading spinner', async () => {
- await nextTick();
- expect(getLoadingIcon().exists()).toBe(false);
+ it('hides loading spinner', () => {
+ expect(findDropdown().props('loading')).toBe(false);
});
describe('filtering', () => {
@@ -178,25 +177,26 @@ describe('BoardsSelector', () => {
});
it('shows all boards without filtering', () => {
- expect(getDropdownItems()).toHaveLength(boards.length + recentIssueBoards.length);
+ expect(findDropdown().props('items')[0].text).toBe('Recent');
+ expect(findDropdown().props('items')[0].options).toHaveLength(recentIssueBoards.length);
+ expect(findDropdown().props('items')[1].text).toBe('All');
+ expect(findDropdown().props('items')[1].options).toHaveLength(
+ boards.length - recentIssueBoards.length,
+ );
});
it('shows only matching boards when filtering', async () => {
const filterTerm = 'board1';
const expectedCount = boards.filter((board) => board.name.includes(filterTerm)).length;
- fillSearchBox(filterTerm);
-
- await nextTick();
- expect(getDropdownItems()).toHaveLength(expectedCount);
+ await fillSearchBox(filterTerm);
+ expect(findDropdown().props('items')).toHaveLength(expectedCount);
});
it('shows message if there are no matching boards', async () => {
- fillSearchBox('does not exist');
+ await fillSearchBox('does not exist');
- await nextTick();
- expect(getDropdownItems()).toHaveLength(0);
- expect(wrapper.text().includes('No matching boards found')).toBe(true);
+ expect(findDropdown().props('noResultsText')).toBe('No matching boards found');
});
});
@@ -204,14 +204,18 @@ describe('BoardsSelector', () => {
it('shows only when boards are greater than 10', async () => {
await nextTick();
expect(projectRecentBoardsQueryHandlerSuccess).toHaveBeenCalled();
- expect(getDropdownHeaders()).toHaveLength(2);
+
+ expect(findDropdown().props('items')).toHaveLength(2);
+ expect(findDropdown().props('items')[0].text).toBe('Recent');
+ expect(findDropdown().props('items')[1].text).toBe('All');
});
it('does not show when boards are less than 10', async () => {
createComponent({ projectBoardsQueryHandler: smallBoardsQueryHandlerSuccess });
await nextTick();
- expect(getDropdownHeaders()).toHaveLength(0);
+
+ expect(findDropdown().props('items')).toHaveLength(0);
});
it('does not show when recentIssueBoards api returns empty array', async () => {
@@ -220,14 +224,14 @@ describe('BoardsSelector', () => {
});
await nextTick();
- expect(getDropdownHeaders()).toHaveLength(0);
+ expect(findDropdown().props('items')).toHaveLength(0);
});
it('does not show when search is active', async () => {
fillSearchBox('Random string');
await nextTick();
- expect(getDropdownHeaders()).toHaveLength(0);
+ expect(findDropdown().props('items')).toHaveLength(0);
});
});
});
@@ -248,7 +252,7 @@ describe('BoardsSelector', () => {
await nextTick();
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
- findDropdown().vm.$emit('show');
+ findDropdown().vm.$emit('shown');
await nextTick();
@@ -272,7 +276,7 @@ describe('BoardsSelector', () => {
await nextTick();
// Emits gl-dropdown show event to simulate the dropdown is opened at initialization time
- findDropdown().vm.$emit('show');
+ findDropdown().vm.$emit('shown');
await waitForPromises();
@@ -286,6 +290,7 @@ describe('BoardsSelector', () => {
createStore();
createComponent({ provide: { multipleIssueBoardsAvailable: true } });
expect(findDropdown().exists()).toBe(true);
+ expect(findDropdown().props('toggleText')).toBe('Select board');
});
});
@@ -296,6 +301,7 @@ describe('BoardsSelector', () => {
provide: { multipleIssueBoardsAvailable: false, hasMissingBoards: true },
});
expect(findDropdown().exists()).toBe(true);
+ expect(findDropdown().props('toggleText')).toBe('Select board');
});
});
@@ -317,6 +323,7 @@ describe('BoardsSelector', () => {
provide: { isApolloBoard: true },
});
expect(findDropdown().props('loading')).toBe(true);
+ expect(findDropdown().props('toggleText')).toBe('Select board');
});
});
});
diff --git a/spec/frontend/boards/components/issue_board_filtered_search_spec.js b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
index 16ad54f0854..1edb6812af0 100644
--- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
@@ -26,14 +26,11 @@ describe('IssueBoardFilter', () => {
});
};
- let fetchUsersSpy;
let fetchLabelsSpy;
beforeEach(() => {
- fetchUsersSpy = jest.fn();
fetchLabelsSpy = jest.fn();
issueBoardFilters.mockReturnValue({
- fetchUsers: fetchUsersSpy,
fetchLabels: fetchLabelsSpy,
});
});
@@ -61,7 +58,7 @@ describe('IssueBoardFilter', () => {
({ isSignedIn }) => {
createComponent({ isSignedIn });
- const tokens = mockTokens(fetchLabelsSpy, fetchUsersSpy, isSignedIn);
+ const tokens = mockTokens(fetchLabelsSpy, isSignedIn);
expect(findBoardsFilteredSearch().props('tokens')).toEqual(orderBy(tokens, ['title']));
},
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index dfcdb4c05d0..dfc8b18e197 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -827,7 +827,7 @@ export const mockConfidentialToken = {
],
};
-export const mockTokens = (fetchLabels, fetchUsers, isSignedIn) => [
+export const mockTokens = (fetchLabels, isSignedIn) => [
{
icon: 'user',
title: TOKEN_TITLE_ASSIGNEE,
@@ -836,7 +836,8 @@ export const mockTokens = (fetchLabels, fetchUsers, isSignedIn) => [
token: UserToken,
dataType: 'user',
unique: true,
- fetchUsers,
+ fullPath: 'gitlab-org',
+ isProject: false,
preloadedUsers: [],
},
{
@@ -848,7 +849,8 @@ export const mockTokens = (fetchLabels, fetchUsers, isSignedIn) => [
token: UserToken,
dataType: 'user',
unique: true,
- fetchUsers,
+ fullPath: 'gitlab-org',
+ isProject: false,
preloadedUsers: [],
},
{
@@ -973,7 +975,7 @@ export const boardListQueryResponse = ({
boardList: {
__typename: 'BoardList',
id: listId,
- totalWeight: 5,
+ totalIssueWeight: '5',
issuesCount,
},
},
diff --git a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
index ee8031f2475..dfb45083c7b 100644
--- a/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
+++ b/spec/frontend/branches/components/__snapshots__/delete_merged_branches_spec.js.snap
@@ -13,7 +13,7 @@ exports[`Delete merged branches component Delete merged branches confirmation mo
size="medium"
textsronly="true"
toggleid="dropdown-toggle-btn-25"
- toggletext=""
+ toggletext="More actions"
variant="default"
>
<ul
diff --git a/spec/frontend/branches/components/sort_dropdown_spec.js b/spec/frontend/branches/components/sort_dropdown_spec.js
index 64ef30bb8a8..777e54f8e69 100644
--- a/spec/frontend/branches/components/sort_dropdown_spec.js
+++ b/spec/frontend/branches/components/sort_dropdown_spec.js
@@ -1,6 +1,7 @@
import { GlSearchBoxByClick } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import setWindowLocation from 'helpers/set_window_location_helper';
import SortDropdown from '~/branches/components/sort_dropdown.vue';
import * as urlUtils from '~/lib/utils/url_utility';
@@ -67,20 +68,33 @@ describe('Branches Sort Dropdown', () => {
});
});
+ describe('when url contains a search param', () => {
+ const branchName = 'branch-1';
+
+ beforeEach(() => {
+ setWindowLocation(`/root/ci-cd-project-demo/-/branches?search=${branchName}`);
+ wrapper = createWrapper();
+ });
+
+ it('should set the default the input value to search param', () => {
+ expect(findSearchBox().props('value')).toBe(branchName);
+ });
+ });
+
describe('when submitting a search term', () => {
beforeEach(() => {
urlUtils.visitUrl = jest.fn();
-
wrapper = createWrapper();
});
it('should call visitUrl', () => {
+ const searchTerm = 'branch-1';
const searchBox = findSearchBox();
-
+ searchBox.vm.$emit('input', searchTerm);
searchBox.vm.$emit('submit');
expect(urlUtils.visitUrl).toHaveBeenCalledWith(
- '/root/ci-cd-project-demo/-/branches?state=all&sort=updated_desc',
+ '/root/ci-cd-project-demo/-/branches?state=all&sort=updated_desc&search=branch-1',
);
});
});
diff --git a/spec/frontend/ci/admin/jobs_table/components/cells/runner_cell_spec.js b/spec/frontend/ci/admin/jobs_table/components/cells/runner_cell_spec.js
index 2f1dae71572..c9758c5ab24 100644
--- a/spec/frontend/ci/admin/jobs_table/components/cells/runner_cell_spec.js
+++ b/spec/frontend/ci/admin/jobs_table/components/cells/runner_cell_spec.js
@@ -1,5 +1,6 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import RunnerTypeIcon from '~/ci/runner/components/runner_type_icon.vue';
import RunnerCell from '~/ci/admin/jobs_table/components/cells/runner_cell.vue';
import { RUNNER_EMPTY_TEXT } from '~/ci/admin/jobs_table/constants';
import { allRunnersData } from 'jest/ci/runner/mock_data';
@@ -61,4 +62,29 @@ describe('Runner Cell', () => {
});
});
});
+
+ describe('Runner Type Icon', () => {
+ const findRunnerTypeIcon = () => wrapper.findComponent(RunnerTypeIcon);
+
+ describe('Job with runner', () => {
+ beforeEach(() => {
+ createComponent({ job: mockJobWithRunner });
+ });
+
+ it('shows the runner type icon', () => {
+ expect(findRunnerTypeIcon().exists()).toBe(true);
+ expect(findRunnerTypeIcon().props('type')).toBe(mockJobWithRunner.runner.runnerType);
+ });
+ });
+
+ describe('Job without runner', () => {
+ beforeEach(() => {
+ createComponent({ job: mockJobWithoutRunner });
+ });
+
+ it('does not show the runner type icon', () => {
+ expect(findRunnerTypeIcon().exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
index 1cbb1a714c9..3628af31aa1 100644
--- a/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
+++ b/spec/frontend/ci/artifacts/components/job_artifacts_table_spec.js
@@ -1,16 +1,8 @@
-import {
- GlLoadingIcon,
- GlTable,
- GlLink,
- GlBadge,
- GlPagination,
- GlModal,
- GlFormCheckbox,
-} from '@gitlab/ui';
+import { GlLoadingIcon, GlTable, GlLink, GlPagination, GlModal, GlFormCheckbox } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import getJobArtifactsResponse from 'test_fixtures/graphql/ci/artifacts/graphql/queries/get_job_artifacts.query.graphql.json';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import waitForPromises from 'helpers/wait_for_promises';
import JobArtifactsTable from '~/ci/artifacts/components/job_artifacts_table.vue';
import ArtifactsTableRowDetails from '~/ci/artifacts/components/artifacts_table_row_details.vue';
@@ -59,13 +51,13 @@ describe('JobArtifactsTable component', () => {
const findStatuses = () => wrapper.findAllByTestId('job-artifacts-job-status');
const findSuccessfulJobStatus = () => findStatuses().at(0);
- const findFailedJobStatus = () => findStatuses().at(1);
+ const findCiBadgeLink = () => findSuccessfulJobStatus().findComponent(CiBadgeLink);
const findLinks = () => wrapper.findAllComponents(GlLink);
const findJobLink = () => findLinks().at(0);
const findPipelineLink = () => findLinks().at(1);
- const findRefLink = () => findLinks().at(2);
- const findCommitLink = () => findLinks().at(3);
+ const findCommitLink = () => findLinks().at(2);
+ const findRefLink = () => findLinks().at(3);
const findSize = () => wrapper.findByTestId('job-artifacts-size');
const findCreated = () => wrapper.findByTestId('job-artifacts-created');
@@ -209,13 +201,13 @@ describe('JobArtifactsTable component', () => {
});
it('shows the job status as an icon for a successful job', () => {
- expect(findSuccessfulJobStatus().findComponent(CiIcon).exists()).toBe(true);
- expect(findSuccessfulJobStatus().findComponent(GlBadge).exists()).toBe(false);
- });
-
- it('shows the job status as a badge for other job statuses', () => {
- expect(findFailedJobStatus().findComponent(GlBadge).exists()).toBe(true);
- expect(findFailedJobStatus().findComponent(CiIcon).exists()).toBe(false);
+ expect(findCiBadgeLink().props()).toMatchObject({
+ status: {
+ group: 'success',
+ },
+ size: 'sm',
+ showText: false,
+ });
});
it('shows links to the job, pipeline, ref, and commit', () => {
diff --git a/spec/frontend/ci/catalog/components/ci_catalog_home_spec.js b/spec/frontend/ci/catalog/components/ci_catalog_home_spec.js
new file mode 100644
index 00000000000..1b5c86c19cb
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/ci_catalog_home_spec.js
@@ -0,0 +1,46 @@
+import { shallowMount } from '@vue/test-utils';
+import { createRouter } from '~/ci/catalog/router';
+import ciResourceDetailsPage from '~/ci/catalog/components/pages/ci_resource_details_page.vue';
+import CiCatalogHome from '~/ci/catalog/components/ci_catalog_home.vue';
+
+describe('CiCatalogHome', () => {
+ const defaultProps = {};
+ const baseRoute = '/';
+ const resourcesPageComponentStub = {
+ name: 'page-component',
+ template: '<div>Hello</div>',
+ };
+ const router = createRouter(baseRoute, resourcesPageComponentStub);
+
+ const createComponent = ({ props = {} } = {}) => {
+ shallowMount(CiCatalogHome, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ router,
+ });
+ };
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('router', () => {
+ it.each`
+ path | component
+ ${baseRoute} | ${resourcesPageComponentStub}
+ ${'/1'} | ${ciResourceDetailsPage}
+ `('when route is $path it renders the right component', async ({ path, component }) => {
+ if (path !== '/') {
+ await router.push(path);
+ }
+
+ const [root] = router.currentRoute.matched;
+
+ expect(root.components.default).toBe(component);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js
new file mode 100644
index 00000000000..658a135534b
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_about_spec.js
@@ -0,0 +1,120 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CiResourceAbout from '~/ci/catalog/components/details/ci_resource_about.vue';
+import { formatDate } from '~/lib/utils/datetime_utility';
+
+describe('CiResourceAbout', () => {
+ let wrapper;
+
+ const defaultProps = {
+ isLoadingSharedData: false,
+ isLoadingDetails: false,
+ openIssuesCount: 4,
+ openMergeRequestsCount: 9,
+ latestVersion: {
+ id: 1,
+ tagName: 'v1.0.0',
+ tagPath: 'path/to/release',
+ releasedAt: '2022-08-23T17:19:09Z',
+ },
+ webPath: 'path/to/project',
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(CiResourceAbout, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ const findProjectLink = () => wrapper.findByText('Go to the project');
+ const findIssueCount = () => wrapper.findByText(`${defaultProps.openIssuesCount} issues`);
+ const findMergeRequestCount = () =>
+ wrapper.findByText(`${defaultProps.openMergeRequestsCount} merge requests`);
+ const findLastRelease = () =>
+ wrapper.findByText(
+ `Last release at ${formatDate(defaultProps.latestVersion.releasedAt, 'yyyy-mm-dd')}`,
+ );
+ const findAllLoadingItems = () => wrapper.findAllByTestId('skeleton-loading-line');
+
+ // Shared data items are items which gets their data from the index page query.
+ const sharedDataItems = [findProjectLink, findLastRelease];
+ // additional details items gets their state only when on the details page
+ const additionalDetailsItems = [findIssueCount, findMergeRequestCount];
+ const allItems = [...sharedDataItems, ...additionalDetailsItems];
+
+ describe('when loading shared data', () => {
+ beforeEach(() => {
+ createComponent({ props: { isLoadingSharedData: true, isLoadingDetails: true } });
+ });
+
+ it('renders all server-side data as loading', () => {
+ allItems.forEach((finder) => {
+ expect(finder().exists()).toBe(false);
+ });
+
+ expect(findAllLoadingItems()).toHaveLength(allItems.length);
+ });
+ });
+
+ describe('when loading additional details', () => {
+ beforeEach(() => {
+ createComponent({ props: { isLoadingDetails: true } });
+ });
+
+ it('renders only the details query as loading', () => {
+ sharedDataItems.forEach((finder) => {
+ expect(finder().exists()).toBe(true);
+ });
+
+ additionalDetailsItems.forEach((finder) => {
+ expect(finder().exists()).toBe(false);
+ });
+
+ expect(findAllLoadingItems()).toHaveLength(additionalDetailsItems.length);
+ });
+ });
+
+ describe('when has loaded', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders project link', () => {
+ expect(findProjectLink().exists()).toBe(true);
+ });
+
+ it('renders the number of issues opened', () => {
+ expect(findIssueCount().exists()).toBe(true);
+ });
+
+ it('renders the number of merge requests opened', () => {
+ expect(findMergeRequestCount().exists()).toBe(true);
+ });
+
+ it('renders the last release date', () => {
+ expect(findLastRelease().exists()).toBe(true);
+ });
+
+ describe('links', () => {
+ it('has the correct project link', () => {
+ expect(findProjectLink().attributes('href')).toBe(defaultProps.webPath);
+ });
+
+ it('has the correct issues link', () => {
+ expect(findIssueCount().attributes('href')).toBe(`${defaultProps.webPath}/issues`);
+ });
+
+ it('has the correct merge request link', () => {
+ expect(findMergeRequestCount().attributes('href')).toBe(
+ `${defaultProps.webPath}/merge_requests`,
+ );
+ });
+
+ it('has no link for release data', () => {
+ expect(findLastRelease().attributes('href')).toBe(undefined);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js
new file mode 100644
index 00000000000..a41996d20b3
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_components_spec.js
@@ -0,0 +1,113 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { resolvers } from '~/ci/catalog/graphql/settings';
+import CiResourceComponents from '~/ci/catalog/components/details/ci_resource_components.vue';
+import getCiCatalogcomponentComponents from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_components.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import { mockComponents } from '../../mock';
+
+Vue.use(VueApollo);
+jest.mock('~/alert');
+
+describe('CiResourceComponents', () => {
+ let wrapper;
+ let mockComponentsResponse;
+
+ const components = mockComponents.data.ciCatalogResource.components.nodes;
+
+ const resourceId = 'gid://gitlab/Ci::Catalog::Resource/1';
+
+ const defaultProps = { resourceId };
+
+ const createComponent = async () => {
+ const handlers = [[getCiCatalogcomponentComponents, mockComponentsResponse]];
+ const mockApollo = createMockApollo(handlers, resolvers);
+
+ wrapper = mountExtended(CiResourceComponents, {
+ propsData: {
+ ...defaultProps,
+ },
+ apolloProvider: mockApollo,
+ });
+
+ await waitForPromises();
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findComponents = () => wrapper.findAllByTestId('component-section');
+
+ beforeEach(() => {
+ mockComponentsResponse = jest.fn();
+ mockComponentsResponse.mockResolvedValue(mockComponents);
+ });
+
+ describe('when queries are loading', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('render a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+
+ it('does not render components', () => {
+ expect(findComponents()).toHaveLength(0);
+ });
+
+ it('does not throw an error', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when components query throws an error', () => {
+ beforeEach(async () => {
+ mockComponentsResponse.mockRejectedValue();
+ await createComponent();
+ });
+
+ it('calls createAlert with the correct message', () => {
+ expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({
+ message: "There was an error fetching this resource's components",
+ });
+ });
+
+ it('does not render the loading state', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when queries have loaded', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ it('renders every component', () => {
+ expect(findComponents()).toHaveLength(components.length);
+ });
+
+ it('renders the component name, description and snippet', () => {
+ components.forEach((component) => {
+ expect(wrapper.text()).toContain(component.name);
+ expect(wrapper.text()).toContain(component.description);
+ expect(wrapper.text()).toContain(component.path);
+ });
+ });
+
+ describe('inputs', () => {
+ it('renders the component parameter attributes', () => {
+ const [firstComponent] = components;
+
+ firstComponent.inputs.nodes.forEach((input) => {
+ expect(findComponents().at(0).text()).toContain(input.name);
+ expect(findComponents().at(0).text()).toContain(input.defaultValue);
+ expect(findComponents().at(0).text()).toContain('Yes');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js
new file mode 100644
index 00000000000..1f7dcf9d4e5
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_details_spec.js
@@ -0,0 +1,83 @@
+import { GlTabs, GlTab } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import CiResourceComponents from '~/ci/catalog/components/details/ci_resource_components.vue';
+import CiResourceDetails from '~/ci/catalog/components/details/ci_resource_details.vue';
+import CiResourceReadme from '~/ci/catalog/components/details/ci_resource_readme.vue';
+
+describe('CiResourceDetails', () => {
+ let wrapper;
+
+ const defaultProps = {
+ resourceId: 'gid://gitlab/Ci::Catalog::Resource/1',
+ };
+ const defaultProvide = {
+ glFeatures: { ciCatalogComponentsTab: true },
+ };
+
+ const createComponent = ({ provide = {}, props = {} } = {}) => {
+ wrapper = shallowMount(CiResourceDetails, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ stubs: {
+ GlTabs,
+ },
+ });
+ };
+ const findAllTabs = () => wrapper.findAllComponents(GlTab);
+ const findCiResourceReadme = () => wrapper.findComponent(CiResourceReadme);
+ const findCiResourceComponents = () => wrapper.findComponent(CiResourceComponents);
+
+ describe('tabs', () => {
+ describe('when feature flag `ci_catalog_components_tab` is enabled', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the readme and components tab', () => {
+ expect(findAllTabs()).toHaveLength(2);
+ expect(findCiResourceComponents().exists()).toBe(true);
+ expect(findCiResourceReadme().exists()).toBe(true);
+ });
+ });
+
+ describe('when feature flag `ci_catalog_components_tab` is disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: { glFeatures: { ciCatalogComponentsTab: false } },
+ });
+ });
+
+ it('renders only readme tab as default', () => {
+ expect(findCiResourceReadme().exists()).toBe(true);
+ expect(findCiResourceComponents().exists()).toBe(false);
+ expect(findAllTabs()).toHaveLength(1);
+ });
+ });
+
+ describe('UI', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('passes lazy attribute to all tabs', () => {
+ findAllTabs().wrappers.forEach((tab) => {
+ expect(tab.attributes().lazy).not.toBeUndefined();
+ });
+ });
+
+ it('passes the right props to the readme component', () => {
+ expect(findCiResourceReadme().props().resourceId).toBe(defaultProps.resourceId);
+ });
+
+ it('passes the right props to the components tab', () => {
+ expect(findCiResourceComponents().props().resourceId).toBe(defaultProps.resourceId);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js
new file mode 100644
index 00000000000..6ab9520508d
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_header_spec.js
@@ -0,0 +1,139 @@
+import { GlAvatar, GlAvatarLink, GlBadge } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CiResourceHeader from '~/ci/catalog/components/details/ci_resource_header.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
+import CiResourceAbout from '~/ci/catalog/components/details/ci_resource_about.vue';
+import { catalogSharedDataMock, catalogAdditionalDetailsMock } from '../../mock';
+
+describe('CiResourceHeader', () => {
+ let wrapper;
+
+ const resource = { ...catalogSharedDataMock.data.ciCatalogResource };
+ const resourceAdditionalData = { ...catalogAdditionalDetailsMock.data.ciCatalogResource };
+
+ const defaultProps = {
+ openIssuesCount: resourceAdditionalData.openIssuesCount,
+ openMergeRequestsCount: resourceAdditionalData.openMergeRequestsCount,
+ isLoadingDetails: false,
+ isLoadingSharedData: false,
+ resource,
+ };
+
+ const findAboutComponent = () => wrapper.findComponent(CiResourceAbout);
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
+ const findVersionBadge = () => wrapper.findComponent(GlBadge);
+ const findPipelineStatusBadge = () => wrapper.findComponent(CiBadgeLink);
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(CiResourceHeader, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the project name and description', () => {
+ expect(wrapper.html()).toContain(resource.name);
+ expect(wrapper.html()).toContain(resource.description);
+ });
+
+ it('renders the namespace and project path', () => {
+ expect(wrapper.html()).toContain(resource.rootNamespace.fullPath);
+ expect(wrapper.html()).toContain(resource.rootNamespace.name);
+ });
+
+ it('renders the avatar', () => {
+ const { id, name } = resource;
+
+ expect(findAvatar().exists()).toBe(true);
+ expect(findAvatarLink().exists()).toBe(true);
+ expect(findAvatar().props()).toMatchObject({
+ entityId: getIdFromGraphQLId(id),
+ entityName: name,
+ });
+ });
+
+ it('renders the catalog about section and passes props', () => {
+ expect(findAboutComponent().exists()).toBe(true);
+ expect(findAboutComponent().props()).toEqual({
+ isLoadingDetails: false,
+ isLoadingSharedData: false,
+ openIssuesCount: defaultProps.openIssuesCount,
+ openMergeRequestsCount: defaultProps.openMergeRequestsCount,
+ latestVersion: resource.latestVersion,
+ webPath: resource.webPath,
+ });
+ });
+ });
+
+ describe('Version badge', () => {
+ describe('without a version', () => {
+ beforeEach(() => {
+ createComponent({ props: { resource: { ...resource, latestVersion: null } } });
+ });
+
+ it('does not render', () => {
+ expect(findVersionBadge().exists()).toBe(false);
+ });
+ });
+
+ describe('with a version', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders', () => {
+ expect(findVersionBadge().exists()).toBe(true);
+ });
+ });
+ });
+
+ describe('when the project has a release', () => {
+ const pipelineStatus = {
+ detailsPath: 'path/to/pipeline',
+ icon: 'status_success',
+ text: 'passed',
+ group: 'success',
+ };
+
+ describe.each`
+ hasPipelineBadge | describeText | testText | status
+ ${true} | ${'is'} | ${'renders'} | ${pipelineStatus}
+ ${false} | ${'is not'} | ${'does not render'} | ${{}}
+ `('and there $describeText a pipeline', ({ hasPipelineBadge, testText, status }) => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ pipelineStatus: status,
+ latestVersion: { tagName: '1.0.0', tagPath: 'path/to/release' },
+ },
+ });
+ });
+
+ it('renders the version badge', () => {
+ expect(findVersionBadge().exists()).toBe(true);
+ });
+
+ it(`${testText} the pipeline status badge`, () => {
+ expect(findPipelineStatusBadge().exists()).toBe(hasPipelineBadge);
+ if (hasPipelineBadge) {
+ expect(findPipelineStatusBadge().props()).toEqual({
+ showText: true,
+ size: 'sm',
+ status: pipelineStatus,
+ showTooltip: true,
+ useLink: true,
+ });
+ }
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js b/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js
new file mode 100644
index 00000000000..0dadac236a8
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/details/ci_resource_readme_spec.js
@@ -0,0 +1,96 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CiResourceReadme from '~/ci/catalog/components/details/ci_resource_readme.vue';
+import getCiCatalogResourceReadme from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_readme.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+
+const readmeHtml = '<h1>This is a readme file</h1>';
+const resourceId = 'gid://gitlab/Ci::Catalog::Resource/1';
+
+describe('CiResourceReadme', () => {
+ let wrapper;
+ let mockReadmeResponse;
+
+ const readmeMockData = {
+ data: {
+ ciCatalogResource: {
+ id: resourceId,
+ readmeHtml,
+ },
+ },
+ };
+
+ const defaultProps = { resourceId };
+
+ const createComponent = ({ props = {} } = {}) => {
+ const handlers = [[getCiCatalogResourceReadme, mockReadmeResponse]];
+
+ wrapper = shallowMountExtended(CiResourceReadme, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ apolloProvider: createMockApollo(handlers),
+ });
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+
+ beforeEach(() => {
+ mockReadmeResponse = jest.fn();
+ });
+
+ describe('when loading', () => {
+ beforeEach(() => {
+ mockReadmeResponse.mockResolvedValue(readmeMockData);
+ createComponent();
+ });
+
+ it('renders only a loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(wrapper.html()).not.toContain(readmeHtml);
+ });
+ });
+
+ describe('when mounted', () => {
+ beforeEach(async () => {
+ mockReadmeResponse.mockResolvedValue(readmeMockData);
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('renders only the received HTML', () => {
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(wrapper.html()).toContain(readmeHtml);
+ });
+
+ it('does not render an error', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when there is an error loading the readme', () => {
+ beforeEach(async () => {
+ mockReadmeResponse.mockRejectedValue({ errors: [] });
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('calls the createAlert function to show an error', () => {
+ expect(createAlert).toHaveBeenCalled();
+ expect(createAlert).toHaveBeenCalledWith({
+ message: "There was a problem loading this project's readme content.",
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/list/catalog_header_spec.js b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
new file mode 100644
index 00000000000..912fd9e1a93
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/list/catalog_header_spec.js
@@ -0,0 +1,86 @@
+import { GlBanner, GlButton } from '@gitlab/ui';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CatalogHeader from '~/ci/catalog/components/list/catalog_header.vue';
+import { CATALOG_FEEDBACK_DISMISSED_KEY } from '~/ci/catalog/constants';
+
+describe('CatalogHeader', () => {
+ useLocalStorageSpy();
+
+ let wrapper;
+
+ const defaultProps = {};
+ const defaultProvide = {
+ pageTitle: 'Catalog page',
+ pageDescription: 'This is a nice catalog page',
+ };
+
+ const findBanner = () => wrapper.findComponent(GlBanner);
+ const findFeedbackButton = () => findBanner().findComponent(GlButton);
+ const findTitle = () => wrapper.findByText(defaultProvide.pageTitle);
+ const findDescription = () => wrapper.findByText(defaultProvide.pageDescription);
+
+ const createComponent = ({ props = {}, stubs = {} } = {}) => {
+ wrapper = shallowMountExtended(CatalogHeader, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: defaultProvide,
+ stubs: {
+ ...stubs,
+ },
+ });
+ };
+
+ it('renders the Catalog title and description', () => {
+ createComponent();
+
+ expect(findTitle().exists()).toBe(true);
+ expect(findDescription().exists()).toBe(true);
+ });
+
+ describe('Feedback banner', () => {
+ describe('when user has never dismissed', () => {
+ beforeEach(() => {
+ createComponent({ stubs: { GlBanner } });
+ });
+
+ it('is visible', () => {
+ expect(findBanner().exists()).toBe(true);
+ });
+
+ it('has link to feedback issue', () => {
+ expect(findFeedbackButton().attributes().href).toBe(
+ 'https://gitlab.com/gitlab-org/gitlab/-/issues/407556',
+ );
+ });
+ });
+
+ describe('when user dismisses it', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets the local storage and removes the banner', async () => {
+ expect(findBanner().exists()).toBe(true);
+
+ await findBanner().vm.$emit('close');
+
+ expect(localStorage.setItem).toHaveBeenCalledWith(CATALOG_FEEDBACK_DISMISSED_KEY, 'true');
+ expect(findBanner().exists()).toBe(false);
+ });
+ });
+
+ describe('when user has dismissed it before', () => {
+ beforeEach(() => {
+ localStorage.setItem(CATALOG_FEEDBACK_DISMISSED_KEY, 'true');
+ createComponent();
+ });
+
+ it('does not show the banner', () => {
+ expect(findBanner().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/list/catalog_list_skeleton_loader_spec.js b/spec/frontend/ci/catalog/components/list/catalog_list_skeleton_loader_spec.js
new file mode 100644
index 00000000000..d21fd56eb2e
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/list/catalog_list_skeleton_loader_spec.js
@@ -0,0 +1,22 @@
+import { shallowMount } from '@vue/test-utils';
+import CatalogListSkeletonLoader from '~/ci/catalog/components/list/catalog_list_skeleton_loader.vue';
+
+describe('CatalogListSkeletonLoader', () => {
+ let wrapper;
+
+ const findSkeletonLoader = () => wrapper.findComponent(CatalogListSkeletonLoader);
+
+ const createComponent = () => {
+ wrapper = shallowMount(CatalogListSkeletonLoader, {});
+ };
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders skeleton item', () => {
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
new file mode 100644
index 00000000000..7f446064366
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_item_spec.js
@@ -0,0 +1,198 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import { GlAvatar, GlBadge, GlButton, GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createRouter } from '~/ci/catalog/router/index';
+import CiResourcesListItem from '~/ci/catalog/components/list/ci_resources_list_item.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { CI_RESOURCE_DETAILS_PAGE_NAME } from '~/ci/catalog/router/constants';
+import { catalogSinglePageResponse } from '../../mock';
+
+Vue.use(VueRouter);
+
+let router;
+let routerPush;
+
+describe('CiResourcesListItem', () => {
+ let wrapper;
+
+ const resource = catalogSinglePageResponse.data.ciCatalogResources.nodes[0];
+ const release = {
+ author: { name: 'author', webUrl: '/user/1' },
+ releasedAt: Date.now(),
+ tagName: '1.0.0',
+ };
+ const defaultProps = {
+ resource,
+ };
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(CiResourcesListItem, {
+ router,
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ GlSprintf,
+ RouterLink: true,
+ RouterView: true,
+ },
+ });
+ };
+
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findResourceName = () => wrapper.findComponent(GlButton);
+ const findResourceDescription = () => wrapper.findByText(defaultProps.resource.description);
+ const findUserLink = () => wrapper.findByTestId('user-link');
+ const findTimeAgoMessage = () => wrapper.findComponent(GlSprintf);
+ const findFavorites = () => wrapper.findByTestId('stats-favorites');
+ const findForks = () => wrapper.findByTestId('stats-forks');
+
+ beforeEach(() => {
+ router = createRouter();
+ routerPush = jest.spyOn(router, 'push').mockImplementation(() => {});
+ });
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the resource avatar and passes the right props', () => {
+ const { icon, id, name } = defaultProps.resource;
+
+ expect(findAvatar().exists()).toBe(true);
+ expect(findAvatar().props()).toMatchObject({
+ entityId: getIdFromGraphQLId(id),
+ entityName: name,
+ src: icon,
+ });
+ });
+
+ it('renders the resource name button', () => {
+ expect(findResourceName().exists()).toBe(true);
+ });
+
+ it('renders the resource version badge', () => {
+ expect(findBadge().exists()).toBe(true);
+ });
+
+ it('renders the resource description', () => {
+ expect(findResourceDescription().exists()).toBe(true);
+ });
+
+ describe('release time', () => {
+ describe('when there is no release data', () => {
+ beforeEach(() => {
+ createComponent({ props: { resource: { ...resource, latestVersion: null } } });
+ });
+
+ it('does not render the release', () => {
+ expect(findTimeAgoMessage().exists()).toBe(false);
+ });
+
+ it('renders the generic `unreleased` badge', () => {
+ expect(findBadge().exists()).toBe(true);
+ expect(findBadge().text()).toBe('Unreleased');
+ });
+ });
+
+ describe('when there is release data', () => {
+ beforeEach(() => {
+ createComponent({ props: { resource: { ...resource, latestVersion: { ...release } } } });
+ });
+
+ it('renders the user link', () => {
+ expect(findUserLink().exists()).toBe(true);
+ expect(findUserLink().attributes('href')).toBe(release.author.webUrl);
+ });
+
+ it('renders the time since the resource was released', () => {
+ expect(findTimeAgoMessage().exists()).toBe(true);
+ });
+
+ it('renders the version badge', () => {
+ expect(findBadge().exists()).toBe(true);
+ expect(findBadge().text()).toBe(release.tagName);
+ });
+ });
+ });
+ });
+
+ describe('when clicking on an item title', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await findResourceName().vm.$emit('click');
+ });
+
+ it('navigates to the details page', () => {
+ expect(routerPush).toHaveBeenCalledWith({
+ name: CI_RESOURCE_DETAILS_PAGE_NAME,
+ params: {
+ id: getIdFromGraphQLId(resource.id),
+ },
+ });
+ });
+ });
+
+ describe('when clicking on an item avatar', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await findAvatar().vm.$emit('click');
+ });
+
+ it('navigates to the details page', () => {
+ expect(routerPush).toHaveBeenCalledWith({
+ name: CI_RESOURCE_DETAILS_PAGE_NAME,
+ params: {
+ id: getIdFromGraphQLId(resource.id),
+ },
+ });
+ });
+ });
+
+ describe('statistics', () => {
+ describe('when there are no statistics', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ resource: {
+ forksCount: 0,
+ starCount: 0,
+ },
+ },
+ });
+ });
+
+ it('render favorites as 0', () => {
+ expect(findFavorites().exists()).toBe(true);
+ expect(findFavorites().text()).toBe('0');
+ });
+
+ it('render forks as 0', () => {
+ expect(findForks().exists()).toBe(true);
+ expect(findForks().text()).toBe('0');
+ });
+ });
+
+ describe('where there are statistics', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('render favorites', () => {
+ expect(findFavorites().exists()).toBe(true);
+ expect(findFavorites().text()).toBe(String(defaultProps.resource.starCount));
+ });
+
+ it('render forks', () => {
+ expect(findForks().exists()).toBe(true);
+ expect(findForks().text()).toBe(String(defaultProps.resource.forksCount));
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/list/ci_resources_list_spec.js b/spec/frontend/ci/catalog/components/list/ci_resources_list_spec.js
new file mode 100644
index 00000000000..aca20a83979
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/list/ci_resources_list_spec.js
@@ -0,0 +1,143 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CiResourcesList from '~/ci/catalog/components/list/ci_resources_list.vue';
+import CiResourcesListItem from '~/ci/catalog/components/list/ci_resources_list_item.vue';
+import { ciCatalogResourcesItemsCount } from '~/ci/catalog/graphql/settings';
+import { catalogResponseBody, catalogSinglePageResponse } from '../../mock';
+
+describe('CiResourcesList', () => {
+ let wrapper;
+
+ const createComponent = ({ props = {} } = {}) => {
+ const { nodes, pageInfo, count } = catalogResponseBody.data.ciCatalogResources;
+
+ const defaultProps = {
+ currentPage: 1,
+ resources: nodes,
+ pageInfo,
+ totalCount: count,
+ };
+
+ wrapper = shallowMountExtended(CiResourcesList, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ GlKeysetPagination,
+ },
+ });
+ };
+
+ const findPageCount = () => wrapper.findByTestId('pageCount');
+ const findResourcesListItems = () => wrapper.findAllComponents(CiResourcesListItem);
+ const findPrevBtn = () => wrapper.findByTestId('prevButton');
+ const findNextBtn = () => wrapper.findByTestId('nextButton');
+
+ describe('contains only one page', () => {
+ const { nodes, pageInfo, count } = catalogSinglePageResponse.data.ciCatalogResources;
+
+ beforeEach(async () => {
+ await createComponent({
+ props: { currentPage: 1, resources: nodes, pageInfo, totalCount: count },
+ });
+ });
+
+ it('shows the right number of items', () => {
+ expect(findResourcesListItems()).toHaveLength(nodes.length);
+ });
+
+ it('hides the keyset control for previous page', () => {
+ expect(findPrevBtn().exists()).toBe(false);
+ });
+
+ it('hides the keyset control for next page', () => {
+ expect(findNextBtn().exists()).toBe(false);
+ });
+
+ it('shows the correct count of current page', () => {
+ expect(findPageCount().text()).toContain('1 of 1');
+ });
+ });
+
+ describe.each`
+ hasPreviousPage | hasNextPage | pageText | expectedTotal | currentPage
+ ${false} | ${true} | ${'1 of 3'} | ${ciCatalogResourcesItemsCount} | ${1}
+ ${true} | ${true} | ${'2 of 3'} | ${ciCatalogResourcesItemsCount} | ${2}
+ ${true} | ${false} | ${'3 of 3'} | ${ciCatalogResourcesItemsCount} | ${3}
+ `(
+ 'when on page $pageText',
+ ({ currentPage, expectedTotal, pageText, hasPreviousPage, hasNextPage }) => {
+ const { nodes, pageInfo, count } = catalogResponseBody.data.ciCatalogResources;
+
+ const previousPageState = hasPreviousPage ? 'enabled' : 'disabled';
+ const nextPageState = hasNextPage ? 'enabled' : 'disabled';
+
+ beforeEach(async () => {
+ await createComponent({
+ props: {
+ currentPage,
+ resources: nodes,
+ pageInfo: { ...pageInfo, hasPreviousPage, hasNextPage },
+ totalCount: count,
+ },
+ });
+ });
+
+ it('shows the right number of items', () => {
+ expect(findResourcesListItems()).toHaveLength(expectedTotal);
+ });
+
+ it(`shows the keyset control for previous page as ${previousPageState}`, () => {
+ const disableAttr = findPrevBtn().attributes('disabled');
+
+ if (previousPageState === 'disabled') {
+ expect(disableAttr).toBeDefined();
+ } else {
+ expect(disableAttr).toBeUndefined();
+ }
+ });
+
+ it(`shows the keyset control for next page as ${nextPageState}`, () => {
+ const disableAttr = findNextBtn().attributes('disabled');
+
+ if (nextPageState === 'disabled') {
+ expect(disableAttr).toBeDefined();
+ } else {
+ expect(disableAttr).toBeUndefined();
+ }
+ });
+
+ it('shows the correct count of current page', () => {
+ expect(findPageCount().text()).toContain(pageText);
+ });
+ },
+ );
+
+ describe('when there is an error getting the page count', () => {
+ beforeEach(() => {
+ createComponent({ props: { totalCount: 0 } });
+ });
+
+ it('hides the page count', () => {
+ expect(findPageCount().exists()).toBe(false);
+ });
+ });
+
+ describe('emitted events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ btnText | elFinder | eventName
+ ${'previous'} | ${findPrevBtn} | ${'onPrevPage'}
+ ${'next'} | ${findNextBtn} | ${'onNextPage'}
+ `('emits $eventName when clicking on the $btnText button', async ({ elFinder, eventName }) => {
+ await elFinder().vm.$emit('click');
+
+ expect(wrapper.emitted(eventName)).toHaveLength(1);
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/list/empty_state_spec.js b/spec/frontend/ci/catalog/components/list/empty_state_spec.js
new file mode 100644
index 00000000000..f589ad96a9d
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/list/empty_state_spec.js
@@ -0,0 +1,27 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import EmptyState from '~/ci/catalog/components/list/empty_state.vue';
+
+describe('EmptyState', () => {
+ let wrapper;
+
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(EmptyState, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ describe('when mounted', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js b/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js
new file mode 100644
index 00000000000..40f243ed891
--- /dev/null
+++ b/spec/frontend/ci/catalog/components/pages/ci_resource_details_page_spec.js
@@ -0,0 +1,186 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import VueRouter from 'vue-router';
+import { GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { CI_CATALOG_RESOURCE_TYPE, cacheConfig } from '~/ci/catalog/graphql/settings';
+
+import getCiCatalogResourceSharedData from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_shared_data.query.graphql';
+import getCiCatalogResourceDetails from '~/ci/catalog/graphql/queries/get_ci_catalog_resource_details.query.graphql';
+
+import CiResourceDetails from '~/ci/catalog/components/details/ci_resource_details.vue';
+import CiResourceDetailsPage from '~/ci/catalog/components/pages/ci_resource_details_page.vue';
+import CiResourceHeader from '~/ci/catalog/components/details/ci_resource_header.vue';
+import CiResourceHeaderSkeletonLoader from '~/ci/catalog/components/details/ci_resource_header_skeleton_loader.vue';
+
+import { createRouter } from '~/ci/catalog/router/index';
+import { CI_RESOURCE_DETAILS_PAGE_NAME } from '~/ci/catalog/router/constants';
+import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { catalogSharedDataMock, catalogAdditionalDetailsMock } from '../../mock';
+
+Vue.use(VueApollo);
+Vue.use(VueRouter);
+
+let router;
+
+const defaultSharedData = { ...catalogSharedDataMock.data.ciCatalogResource };
+const defaultAdditionalData = { ...catalogAdditionalDetailsMock.data.ciCatalogResource };
+
+describe('CiResourceDetailsPage', () => {
+ let wrapper;
+ let sharedDataResponse;
+ let additionalDataResponse;
+
+ const defaultProps = {};
+
+ const defaultProvide = {
+ ciCatalogPath: '/ci/catalog/resources',
+ };
+
+ const findDetailsComponent = () => wrapper.findComponent(CiResourceDetails);
+ const findHeaderComponent = () => wrapper.findComponent(CiResourceHeader);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findHeaderSkeletonLoader = () => wrapper.findComponent(CiResourceHeaderSkeletonLoader);
+
+ const createComponent = ({ props = {} } = {}) => {
+ const handlers = [
+ [getCiCatalogResourceSharedData, sharedDataResponse],
+ [getCiCatalogResourceDetails, additionalDataResponse],
+ ];
+
+ const mockApollo = createMockApollo(handlers, undefined, cacheConfig);
+
+ wrapper = shallowMount(CiResourceDetailsPage, {
+ router,
+ apolloProvider: mockApollo,
+ provide: {
+ ...defaultProvide,
+ },
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ RouterView: true,
+ },
+ });
+ };
+
+ beforeEach(async () => {
+ sharedDataResponse = jest.fn();
+ additionalDataResponse = jest.fn();
+
+ router = createRouter();
+ await router.push({
+ name: CI_RESOURCE_DETAILS_PAGE_NAME,
+ params: { id: defaultSharedData.id },
+ });
+ });
+
+ describe('when the app is loading', () => {
+ describe('and shared data is pre-fetched', () => {
+ beforeEach(() => {
+ // By mocking a return value and not a promise, we skip the loading
+ // to simulate having the pre-fetched query
+ sharedDataResponse.mockReturnValueOnce(catalogSharedDataMock);
+ additionalDataResponse.mockResolvedValue(catalogAdditionalDetailsMock);
+ createComponent();
+ });
+
+ it('renders the header skeleton loader', () => {
+ expect(findHeaderSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('passes down the loading state to the header component', () => {
+ sharedDataResponse.mockReturnValueOnce(catalogSharedDataMock);
+
+ expect(findHeaderComponent().props()).toMatchObject({
+ isLoadingDetails: true,
+ isLoadingSharedData: false,
+ });
+ });
+ });
+
+ describe('and shared data is not pre-fetched', () => {
+ beforeEach(() => {
+ sharedDataResponse.mockResolvedValue(catalogSharedDataMock);
+ additionalDataResponse.mockResolvedValue(catalogAdditionalDetailsMock);
+ createComponent();
+ });
+
+ it('does not render the header skeleton', () => {
+ expect(findHeaderSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('passes all loading state to the header component as true', () => {
+ expect(findHeaderComponent().props()).toMatchObject({
+ isLoadingDetails: true,
+ isLoadingSharedData: true,
+ });
+ });
+ });
+ });
+
+ describe('and there are no resources', () => {
+ beforeEach(async () => {
+ const mockError = new Error('error');
+ sharedDataResponse.mockRejectedValue(mockError);
+ additionalDataResponse.mockRejectedValue(mockError);
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('renders the empty state', () => {
+ expect(findDetailsComponent().exists()).toBe(false);
+ expect(findEmptyState().exists()).toBe(true);
+ expect(findEmptyState().props('primaryButtonLink')).toBe(defaultProvide.ciCatalogPath);
+ });
+ });
+
+ describe('when data has loaded', () => {
+ beforeEach(async () => {
+ sharedDataResponse.mockResolvedValue(catalogSharedDataMock);
+ additionalDataResponse.mockResolvedValue(catalogAdditionalDetailsMock);
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('does not render the header skeleton loader', () => {
+ expect(findHeaderSkeletonLoader().exists()).toBe(false);
+ });
+
+ describe('Catalog header', () => {
+ it('exists', () => {
+ expect(findHeaderComponent().exists()).toBe(true);
+ });
+
+ it('passes expected props', () => {
+ expect(findHeaderComponent().props()).toEqual({
+ isLoadingDetails: false,
+ isLoadingSharedData: false,
+ openIssuesCount: defaultAdditionalData.openIssuesCount,
+ openMergeRequestsCount: defaultAdditionalData.openMergeRequestsCount,
+ pipelineStatus:
+ defaultAdditionalData.versions.nodes[0].commit.pipelines.nodes[0].detailedStatus,
+ resource: defaultSharedData,
+ });
+ });
+ });
+
+ describe('Catalog details', () => {
+ it('exists', () => {
+ expect(findDetailsComponent().exists()).toBe(true);
+ });
+
+ it('passes expected props', () => {
+ expect(findDetailsComponent().props()).toEqual({
+ resourceId: convertToGraphQLId(CI_CATALOG_RESOURCE_TYPE, defaultAdditionalData.id),
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/catalog/mock.js b/spec/frontend/ci/catalog/mock.js
new file mode 100644
index 00000000000..21fed6ac8ec
--- /dev/null
+++ b/spec/frontend/ci/catalog/mock.js
@@ -0,0 +1,546 @@
+import { componentsMockData } from '~/ci/catalog/constants';
+
+export const catalogResponseBody = {
+ data: {
+ ciCatalogResources: {
+ pageInfo: {
+ startCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjEyOSJ9',
+ endCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjExMCJ9',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ __typename: 'PageInfo',
+ },
+ count: 41,
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/129',
+ icon: null,
+ name: 'Project-42 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-42',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/128',
+ icon: null,
+ name: 'Project-41 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-41',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/127',
+ icon: null,
+ name: 'Project-40 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-40',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/126',
+ icon: null,
+ name: 'Project-39 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-39',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/125',
+ icon: null,
+ name: 'Project-38 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-38',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/124',
+ icon: null,
+ name: 'Project-37 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-37',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/123',
+ icon: null,
+ name: 'Project-36 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-36',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/122',
+ icon: null,
+ name: 'Project-35 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-35',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/121',
+ icon: null,
+ name: 'Project-34 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-34',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/120',
+ icon: null,
+ name: 'Project-33 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-33',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/119',
+ icon: null,
+ name: 'Project-32 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-32',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/118',
+ icon: null,
+ name: 'Project-31 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-31',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/117',
+ icon: null,
+ name: 'Project-30 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-30',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/116',
+ icon: null,
+ name: 'Project-29 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-29',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/115',
+ icon: null,
+ name: 'Project-28 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-28',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/114',
+ icon: null,
+ name: 'Project-27 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-27',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/113',
+ icon: null,
+ name: 'Project-26 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-26',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/112',
+ icon: null,
+ name: 'Project-25 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-25',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/111',
+ icon: null,
+ name: 'Project-24 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-24',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/110',
+ icon: null,
+ name: 'Project-23 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-23',
+ __typename: 'CiCatalogResource',
+ },
+ ],
+ __typename: 'CiCatalogResourceConnection',
+ },
+ },
+};
+
+export const catalogSinglePageResponse = {
+ data: {
+ ciCatalogResources: {
+ pageInfo: {
+ startCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjEzMiJ9',
+ endCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAxNS0wNy0wMyAxMDowMDowMC4wMDAwMDAwMDAgKzAwMDAiLCJpZCI6IjEzMCJ9',
+ hasNextPage: false,
+ hasPreviousPage: false,
+ __typename: 'PageInfo',
+ },
+ count: 3,
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/132',
+ icon: null,
+ name: 'Project-45 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-45',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/131',
+ icon: null,
+ name: 'Project-44 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-44',
+ __typename: 'CiCatalogResource',
+ },
+ {
+ id: 'gid://gitlab/Ci::Catalog::Resource/130',
+ icon: null,
+ name: 'Project-43 Name',
+ description: 'A simple component',
+ starCount: 0,
+ forksCount: 0,
+ latestVersion: null,
+ rootNamespace: {
+ id: 'gid://gitlab/Group/185',
+ fullPath: 'frontend-fixtures',
+ name: 'frontend-fixtures',
+ __typename: 'Namespace',
+ },
+ webPath: '/frontend-fixtures/project-43',
+ __typename: 'CiCatalogResource',
+ },
+ ],
+ __typename: 'CiCatalogResourceConnection',
+ },
+ },
+};
+
+export const catalogSharedDataMock = {
+ data: {
+ ciCatalogResource: {
+ __typename: 'CiCatalogResource',
+ id: `gid://gitlab/CiCatalogResource/1`,
+ icon: null,
+ description: 'This is the description of the repo',
+ name: 'Ruby',
+ rootNamespace: { id: 1, fullPath: '/group/project', name: 'my-dumb-project' },
+ starCount: 1,
+ forksCount: 2,
+ latestVersion: {
+ __typename: 'Release',
+ id: '3',
+ tagName: '1.0.0',
+ tagPath: 'path/to/release',
+ releasedAt: Date.now(),
+ author: { id: 1, webUrl: 'profile/1', name: 'username' },
+ },
+ webPath: 'path/to/project',
+ },
+ },
+};
+
+export const catalogAdditionalDetailsMock = {
+ data: {
+ ciCatalogResource: {
+ __typename: 'CiCatalogResource',
+ id: `gid://gitlab/CiCatalogResource/1`,
+ openIssuesCount: 4,
+ openMergeRequestsCount: 10,
+ readmeHtml: '<h1>Hello world</h1>',
+ versions: {
+ __typename: 'ReleaseConnection',
+ nodes: [
+ {
+ __typename: 'Release',
+ id: 'gid://gitlab/Release/3',
+ commit: {
+ __typename: 'Commit',
+ id: 'gid://gitlab/CommitPresenter/afa936495f20e08c26ed4a67130ee2166f94fa6e',
+ pipelines: {
+ __typename: 'PipelineConnection',
+ nodes: [
+ {
+ __typename: 'Pipeline',
+ id: 'gid://gitlab/Ci::Pipeline/583',
+ detailedStatus: {
+ __typename: 'DetailedStatus',
+ id: 'success-583-583',
+ detailsPath: '/root/cicd-circular/-/pipelines/583',
+ icon: 'status_success',
+ text: 'passed',
+ group: 'success',
+ },
+ },
+ ],
+ },
+ },
+ tagName: 'v1.0.2',
+ releasedAt: '2022-08-23T17:19:09Z',
+ },
+ ],
+ },
+ },
+ },
+};
+
+const generateResourcesNodes = (count = 20, startId = 0) => {
+ const nodes = [];
+ for (let i = startId; i < startId + count; i += 1) {
+ nodes.push({
+ __typename: 'CiCatalogResource',
+ id: `gid://gitlab/CiCatalogResource/${i}`,
+ description: `This is a component that does a bunch of stuff and is really just a number: ${i}`,
+ forksCount: 5,
+ icon: 'my-icon',
+ name: `My component #${i}`,
+ rootNamespace: {
+ id: 1,
+ __typename: 'Namespace',
+ name: 'namespaceName',
+ path: 'namespacePath',
+ },
+ starCount: 10,
+ latestVersion: {
+ __typename: 'Release',
+ id: '3',
+ tagName: '1.0.0',
+ tagPath: 'path/to/release',
+ releasedAt: Date.now(),
+ author: { id: 1, webUrl: 'profile/1', name: 'username' },
+ },
+ webPath: 'path/to/project',
+ });
+ }
+
+ return nodes;
+};
+
+export const mockCatalogResourceItem = generateResourcesNodes(1)[0];
+
+export const mockComponents = {
+ data: {
+ ciCatalogResource: {
+ __typename: 'CiCatalogResource',
+ id: `gid://gitlab/CiCatalogResource/1`,
+ components: {
+ ...componentsMockData,
+ },
+ },
+ },
+};
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
index 64227872af3..353b5fd3c47 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
@@ -1,10 +1,4 @@
-import {
- GlListboxItem,
- GlCollapsibleListbox,
- GlDropdownDivider,
- GlDropdownItem,
- GlIcon,
-} from '@gitlab/ui';
+import { GlListboxItem, GlCollapsibleListbox, GlDropdownDivider, GlIcon } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { allEnvironments, ENVIRONMENT_QUERY_LIMIT } from '~/ci/ci_variable_list/constants';
import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
@@ -16,7 +10,6 @@ describe('Ci environments dropdown', () => {
const defaultProps = {
areEnvironmentsLoading: false,
environments: envs,
- hasEnvScopeQuery: false,
selectedEnvironmentScope: '',
};
@@ -25,7 +18,7 @@ describe('Ci environments dropdown', () => {
const findActiveIconByIndex = (index) => findListboxItemByIndex(index).findComponent(GlIcon);
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findListboxText = () => findListbox().props('toggleText');
- const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem);
+ const findCreateWildcardButton = () => wrapper.findByTestId('create-wildcard-button');
const findDropdownDivider = () => wrapper.findComponent(GlDropdownDivider);
const findMaxEnvNote = () => wrapper.findByTestId('max-envs-notice');
@@ -57,32 +50,23 @@ describe('Ci environments dropdown', () => {
});
describe('Search term is empty', () => {
- describe.each`
- hasEnvScopeQuery | status | defaultEnvStatus | firstItemValue | envIndices
- ${true} | ${'exists'} | ${'prepends'} | ${'*'} | ${[1, 2, 3]}
- ${false} | ${'does not exist'} | ${'does not prepend'} | ${envs[0]} | ${[0, 1, 2]}
- `(
- 'when query for fetching environment scope $status',
- ({ defaultEnvStatus, firstItemValue, hasEnvScopeQuery, envIndices }) => {
- beforeEach(() => {
- createComponent({ props: { environments: envs, hasEnvScopeQuery } });
- });
-
- it(`${defaultEnvStatus} * in listbox`, () => {
- expect(findListboxItemByIndex(0).text()).toBe(firstItemValue);
- });
-
- it('renders all environments', () => {
- expect(findListboxItemByIndex(envIndices[0]).text()).toBe(envs[0]);
- expect(findListboxItemByIndex(envIndices[1]).text()).toBe(envs[1]);
- expect(findListboxItemByIndex(envIndices[2]).text()).toBe(envs[2]);
- });
-
- it('does not display active checkmark', () => {
- expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
- });
- },
- );
+ beforeEach(() => {
+ createComponent({ props: { environments: envs } });
+ });
+
+ it(`prepends * in listbox`, () => {
+ expect(findListboxItemByIndex(0).text()).toBe('*');
+ });
+
+ it('renders all environments', () => {
+ expect(findListboxItemByIndex(1).text()).toBe(envs[0]);
+ expect(findListboxItemByIndex(2).text()).toBe(envs[1]);
+ expect(findListboxItemByIndex(3).text()).toBe(envs[2]);
+ });
+
+ it('does not display active checkmark', () => {
+ expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
+ });
});
describe('when `*` is the value of selectedEnvironmentScope props', () => {
@@ -98,40 +82,13 @@ describe('Ci environments dropdown', () => {
});
});
- describe('when environments are not fetched via graphql', () => {
+ describe('when fetching environments', () => {
const currentEnv = envs[2];
beforeEach(() => {
createComponent();
});
- it('filters on the frontend and renders only the environment searched for', async () => {
- await findListbox().vm.$emit('search', currentEnv);
-
- expect(findAllListboxItems()).toHaveLength(1);
- expect(findListboxItemByIndex(0).text()).toBe(currentEnv);
- });
-
- it('does not emit event when searching', async () => {
- expect(wrapper.emitted('search-environment-scope')).toBeUndefined();
-
- await findListbox().vm.$emit('search', currentEnv);
-
- expect(wrapper.emitted('search-environment-scope')).toBeUndefined();
- });
-
- it('does not display note about max environments shown', () => {
- expect(findMaxEnvNote().exists()).toBe(false);
- });
- });
-
- describe('when fetching environments via graphql', () => {
- const currentEnv = envs[2];
-
- beforeEach(() => {
- createComponent({ props: { hasEnvScopeQuery: true } });
- });
-
it('renders dropdown divider', () => {
expect(findDropdownDivider().exists()).toBe(true);
});
@@ -143,7 +100,7 @@ describe('Ci environments dropdown', () => {
});
it('renders dropdown loading icon while fetch query is loading', () => {
- createComponent({ props: { areEnvironmentsLoading: true, hasEnvScopeQuery: true } });
+ createComponent({ props: { areEnvironmentsLoading: true } });
expect(findListbox().props('loading')).toBe(true);
expect(findListbox().props('searching')).toBe(false);
@@ -151,7 +108,7 @@ describe('Ci environments dropdown', () => {
});
it('renders search loading icon while search query is loading and dropdown is open', async () => {
- createComponent({ props: { areEnvironmentsLoading: true, hasEnvScopeQuery: true } });
+ createComponent({ props: { areEnvironmentsLoading: true } });
await findListbox().vm.$emit('shown');
expect(findListbox().props('loading')).toBe(false);
@@ -188,16 +145,35 @@ describe('Ci environments dropdown', () => {
});
});
- describe('when creating a new environment from a search term', () => {
- const search = 'new-env';
+ describe('when creating a new environment scope from a search term', () => {
+ const searchTerm = 'new-env';
beforeEach(() => {
- createComponent({ searchTerm: search });
+ createComponent({ searchTerm });
});
- it('emits create-environment-scope', () => {
- findCreateWildcardButton().vm.$emit('click');
+ it('sets new environment scope as the selected environment scope', async () => {
+ findCreateWildcardButton().trigger('click');
+
+ await findListbox().vm.$emit('search', searchTerm);
+
+ expect(findListbox().props('selected')).toBe(searchTerm);
+ });
+
+ it('includes new environment scope in search if it matches search term', async () => {
+ findCreateWildcardButton().trigger('click');
+
+ await findListbox().vm.$emit('search', searchTerm);
+
+ expect(findAllListboxItems()).toHaveLength(envs.length + 1);
+ expect(findListboxItemByIndex(1).text()).toBe(searchTerm);
+ });
+
+ it('excludes new environment scope in search if it does not match the search term', async () => {
+ findCreateWildcardButton().trigger('click');
+
+ await findListbox().vm.$emit('search', 'not-new-env');
- expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
+ expect(findAllListboxItems()).toHaveLength(envs.length);
});
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
index ab5d914a6a1..207ea7aa060 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_drawer_spec.js
@@ -1,4 +1,5 @@
-import { GlDrawer, GlFormCombobox, GlFormInput, GlFormSelect } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { GlDrawer, GlFormCombobox, GlFormInput, GlFormSelect, GlModal } from '@gitlab/ui';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
import CiVariableDrawer from '~/ci/ci_variable_list/components/ci_variable_drawer.vue';
@@ -67,6 +68,8 @@ describe('CI Variable Drawer', () => {
};
const findConfirmBtn = () => wrapper.findByTestId('ci-variable-confirm-btn');
+ const findConfirmDeleteModal = () => wrapper.findComponent(GlModal);
+ const findDeleteBtn = () => wrapper.findByTestId('ci-variable-delete-btn');
const findDisabledEnvironmentScopeDropdown = () => wrapper.findComponent(GlFormInput);
const findDrawer = () => wrapper.findComponent(GlDrawer);
const findEnvironmentScopeDropdown = () => wrapper.findComponent(CiEnvironmentsDropdown);
@@ -363,22 +366,118 @@ describe('CI Variable Drawer', () => {
});
it('title and confirm button renders the correct text', () => {
- expect(findTitle().text()).toBe('Add Variable');
- expect(findConfirmBtn().text()).toBe('Add Variable');
+ expect(findTitle().text()).toBe('Add variable');
+ expect(findConfirmBtn().text()).toBe('Add variable');
+ });
+
+ it('does not render delete button', () => {
+ expect(findDeleteBtn().exists()).toBe(false);
+ });
+
+ it('dispatches the add-variable event', async () => {
+ await findKeyField().vm.$emit('input', 'NEW_VARIABLE');
+ await findProtectedCheckbox().vm.$emit('input', false);
+ await findExpandedCheckbox().vm.$emit('input', true);
+ await findMaskedCheckbox().vm.$emit('input', true);
+ await findValueField().vm.$emit('input', 'NEW_VALUE');
+
+ findConfirmBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('add-variable')).toEqual([
+ [
+ {
+ environmentScope: '*',
+ key: 'NEW_VARIABLE',
+ masked: true,
+ protected: false,
+ raw: false, // opposite of expanded
+ value: 'NEW_VALUE',
+ variableType: 'ENV_VAR',
+ },
+ ],
+ ]);
});
});
describe('when editing a variable', () => {
beforeEach(() => {
createComponent({
- props: { mode: EDIT_VARIABLE_ACTION },
+ props: { mode: EDIT_VARIABLE_ACTION, selectedVariable: mockProjectVariableFileType },
stubs: { GlDrawer },
});
});
it('title and confirm button renders the correct text', () => {
- expect(findTitle().text()).toBe('Edit Variable');
- expect(findConfirmBtn().text()).toBe('Edit Variable');
+ expect(findTitle().text()).toBe('Edit variable');
+ expect(findConfirmBtn().text()).toBe('Edit variable');
+ });
+
+ it('dispatches the edit-variable event', async () => {
+ await findValueField().vm.$emit('input', 'EDITED_VALUE');
+
+ findConfirmBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('update-variable')).toEqual([
+ [
+ {
+ ...mockProjectVariableFileType,
+ value: 'EDITED_VALUE',
+ },
+ ],
+ ]);
+ });
+ });
+
+ describe('when deleting a variable', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: { mode: EDIT_VARIABLE_ACTION, selectedVariable: mockProjectVariableFileType },
+ });
+ });
+
+ it('bubbles up the delete-variable event', async () => {
+ findDeleteBtn().vm.$emit('click');
+
+ await nextTick();
+
+ findConfirmDeleteModal().vm.$emit('primary');
+
+ expect(wrapper.emitted('delete-variable')).toEqual([[mockProjectVariableFileType]]);
+ });
+ });
+
+ describe('environment scope events', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ mode: EDIT_VARIABLE_ACTION,
+ selectedVariable: mockProjectVariableFileType,
+ areScopedVariablesAvailable: true,
+ hideEnvironmentScope: false,
+ },
+ });
+ });
+
+ it('sets the environment scope', async () => {
+ await findEnvironmentScopeDropdown().vm.$emit('select-environment', 'staging');
+ await findConfirmBtn().vm.$emit('click');
+
+ expect(wrapper.emitted('update-variable')).toEqual([
+ [
+ {
+ ...mockProjectVariableFileType,
+ environmentScope: 'staging',
+ },
+ ],
+ ]);
+ });
+
+ it('bubbles up the search event', async () => {
+ await findEnvironmentScopeDropdown().vm.$emit('search-environment-scope', 'staging');
+
+ expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
});
});
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
index 7dce23f72c0..5ba9b3b8c20 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
@@ -12,12 +12,10 @@ import {
ENVIRONMENT_SCOPE_LINK_TITLE,
AWS_TIP_TITLE,
AWS_TIP_MESSAGE,
- groupString,
instanceString,
- projectString,
variableOptions,
} from '~/ci/ci_variable_list/constants';
-import { mockEnvs, mockVariablesWithScopes, mockVariablesWithUniqueScopes } from '../mocks';
+import { mockVariablesWithScopes } from '../mocks';
import ModalStub from '../stubs';
describe('Ci variable modal', () => {
@@ -46,7 +44,6 @@ describe('Ci variable modal', () => {
areScopedVariablesAvailable: true,
environments: [],
hideEnvironmentScope: false,
- hasEnvScopeQuery: false,
mode: ADD_VARIABLE_ACTION,
selectedVariable: {},
variables: [],
@@ -352,42 +349,6 @@ describe('Ci variable modal', () => {
expect(link.attributes('title')).toBe(ENVIRONMENT_SCOPE_LINK_TITLE);
expect(link.attributes('href')).toBe(defaultProvide.environmentScopeLink);
});
-
- describe('when query for envioronment scope exists', () => {
- beforeEach(() => {
- createComponent({
- props: {
- environments: mockEnvs,
- hasEnvScopeQuery: true,
- variables: mockVariablesWithUniqueScopes(projectString),
- },
- });
- });
-
- it('does not merge environment scope sources', () => {
- const expectedLength = mockEnvs.length;
-
- expect(findCiEnvironmentsDropdown().props('environments')).toHaveLength(expectedLength);
- });
- });
-
- describe('when feature flag is disabled', () => {
- const mockGroupVariables = mockVariablesWithUniqueScopes(groupString);
- beforeEach(() => {
- createComponent({
- props: {
- environments: mockEnvs,
- variables: mockGroupVariables,
- },
- });
- });
-
- it('merges environment scope sources', () => {
- const expectedLength = mockGroupVariables.length + mockEnvs.length;
-
- expect(findCiEnvironmentsDropdown().props('environments')).toHaveLength(expectedLength);
- });
- });
});
describe('and section is hidden', () => {
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
index 79dd638e2bd..04145c2c6aa 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
@@ -23,7 +23,6 @@ describe('Ci variable table', () => {
environments: mapEnvironmentNames(mockEnvs),
hideEnvironmentScope: false,
isLoading: false,
- hasEnvScopeQuery: false,
maxVariableLimit: 5,
pageInfo: { after: '' },
variables: mockVariablesWithScopes(projectString),
@@ -70,7 +69,6 @@ describe('Ci variable table', () => {
areEnvironmentsLoading: defaultProps.areEnvironmentsLoading,
areScopedVariablesAvailable: defaultProps.areScopedVariablesAvailable,
environments: defaultProps.environments,
- hasEnvScopeQuery: defaultProps.hasEnvScopeQuery,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
variables: defaultProps.variables,
mode: ADD_VARIABLE_ACTION,
@@ -142,7 +140,7 @@ describe('Ci variable table', () => {
});
});
- describe('variable events', () => {
+ describe('variable events for modal', () => {
beforeEach(() => {
createComponent();
});
@@ -161,6 +159,25 @@ describe('Ci variable table', () => {
});
});
+ describe('variable events for drawer', () => {
+ beforeEach(() => {
+ createComponent({ featureFlags: { ciVariableDrawer: true } });
+ });
+
+ it.each`
+ eventName
+ ${'add-variable'}
+ ${'update-variable'}
+ ${'delete-variable'}
+ `('bubbles up the $eventName event', async ({ eventName }) => {
+ await findCiVariableTable().vm.$emit('set-selected-variable');
+
+ await findCiVariableDrawer().vm.$emit(eventName, newVariable);
+
+ expect(wrapper.emitted(eventName)).toEqual([[newVariable]]);
+ });
+ });
+
describe('pages events', () => {
beforeEach(() => {
createComponent();
@@ -178,7 +195,7 @@ describe('Ci variable table', () => {
});
});
- describe('environment events', () => {
+ describe('environment events for modal', () => {
beforeEach(() => {
createComponent();
});
@@ -191,4 +208,18 @@ describe('Ci variable table', () => {
expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
});
});
+
+ describe('environment events for drawer', () => {
+ beforeEach(() => {
+ createComponent({ featureFlags: { ciVariableDrawer: true } });
+ });
+
+ it('bubbles up the search event', async () => {
+ await findCiVariableTable().vm.$emit('set-selected-variable');
+
+ await findCiVariableDrawer().vm.$emit('search-environment-scope', 'staging');
+
+ expect(wrapper.emitted('search-environment-scope')).toEqual([['staging']]);
+ });
+ });
});
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
index 6fa1915f3c1..c90ff4cc682 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
@@ -52,7 +52,6 @@ const mockProvide = {
const defaultProps = {
areScopedVariablesAvailable: true,
- hasEnvScopeQuery: false,
pageInfo: {},
hideEnvironmentScope: false,
refetchAfterMutation: false,
@@ -514,7 +513,6 @@ describe('Ci Variable Shared Component', () => {
areEnvironmentsLoading: false,
areScopedVariablesAvailable: wrapper.props().areScopedVariablesAvailable,
hideEnvironmentScope: defaultProps.hideEnvironmentScope,
- hasEnvScopeQuery: props.hasEnvScopeQuery,
pageInfo: defaultProps.pageInfo,
isLoading: false,
maxVariableLimit,
diff --git a/spec/frontend/ci/ci_variable_list/mocks.js b/spec/frontend/ci/ci_variable_list/mocks.js
index 41dfc0ebfda..9c9c99ad5ea 100644
--- a/spec/frontend/ci/ci_variable_list/mocks.js
+++ b/spec/frontend/ci/ci_variable_list/mocks.js
@@ -189,7 +189,6 @@ export const createProjectProps = () => {
componentName: 'ProjectVariable',
entity: 'project',
fullPath: '/namespace/project/',
- hasEnvScopeQuery: true,
id: 'gid://gitlab/Project/20',
mutationData: {
[ADD_MUTATION_ACTION]: addProjectVariable,
@@ -214,7 +213,6 @@ export const createGroupProps = () => {
componentName: 'GroupVariable',
entity: 'group',
fullPath: '/my-group',
- hasEnvScopeQuery: false,
id: 'gid://gitlab/Group/20',
mutationData: {
[ADD_MUTATION_ACTION]: addGroupVariable,
@@ -233,7 +231,6 @@ export const createGroupProps = () => {
export const createInstanceProps = () => {
return {
componentName: 'InstanceVariable',
- hasEnvScopeQuery: false,
entity: '',
mutationData: {
[ADD_MUTATION_ACTION]: addAdminVariable,
diff --git a/spec/frontend/ci/ci_variable_list/utils_spec.js b/spec/frontend/ci/ci_variable_list/utils_spec.js
index beeae71376a..fbcf0e7c5a5 100644
--- a/spec/frontend/ci/ci_variable_list/utils_spec.js
+++ b/spec/frontend/ci/ci_variable_list/utils_spec.js
@@ -1,58 +1,7 @@
-import {
- createJoinedEnvironments,
- convertEnvironmentScope,
- mapEnvironmentNames,
-} from '~/ci/ci_variable_list/utils';
+import { convertEnvironmentScope, mapEnvironmentNames } from '~/ci/ci_variable_list/utils';
import { allEnvironments } from '~/ci/ci_variable_list/constants';
describe('utils', () => {
- const environments = ['dev', 'prod'];
- const newEnvironments = ['staging'];
-
- describe('createJoinedEnvironments', () => {
- it('returns only `environments` if `variables` argument is undefined', () => {
- const variables = undefined;
-
- expect(createJoinedEnvironments(variables, environments, [])).toEqual(environments);
- });
-
- it('returns a list of environments and environment scopes taken from variables in alphabetical order', () => {
- const envScope1 = 'new1';
- const envScope2 = 'new2';
-
- const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
-
- expect(createJoinedEnvironments(variables, environments, [])).toEqual([
- environments[0],
- envScope1,
- envScope2,
- environments[1],
- ]);
- });
-
- it('returns combined list with new environments included', () => {
- const variables = undefined;
-
- expect(createJoinedEnvironments(variables, environments, newEnvironments)).toEqual([
- ...environments,
- ...newEnvironments,
- ]);
- });
-
- it('removes duplicate environments', () => {
- const envScope1 = environments[0];
- const envScope2 = 'new2';
-
- const variables = [{ environmentScope: envScope1 }, { environmentScope: envScope2 }];
-
- expect(createJoinedEnvironments(variables, environments, [])).toEqual([
- environments[0],
- envScope2,
- environments[1],
- ]);
- });
- });
-
describe('convertEnvironmentScope', () => {
it('converts the * to the `All environments` text', () => {
expect(convertEnvironmentScope('*')).toBe(allEnvironments.text);
diff --git a/spec/frontend/ci/common/pipelines_table_spec.js b/spec/frontend/ci/common/pipelines_table_spec.js
index 26dd1a2fcc5..6cf391d72ca 100644
--- a/spec/frontend/ci/common/pipelines_table_spec.js
+++ b/spec/frontend/ci/common/pipelines_table_spec.js
@@ -1,9 +1,7 @@
-import '~/commons';
import { GlTableLite } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import fixture from 'test_fixtures/pipelines/pipelines.json';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import LegacyPipelineMiniGraph from '~/ci/pipeline_mini_graph/legacy_pipeline_mini_graph.vue';
import PipelineFailedJobsWidget from '~/ci/pipelines_page/components/failure_widget/pipeline_failed_jobs_widget.vue';
import PipelineOperations from '~/ci/pipelines_page/components/pipeline_operations.vue';
@@ -12,7 +10,7 @@ import PipelineUrl from '~/ci/pipelines_page/components/pipeline_url.vue';
import PipelinesTable from '~/ci/common/pipelines_table.vue';
import PipelinesTimeago from '~/ci/pipelines_page/components/time_ago.vue';
import {
- PipelineKeyOptions,
+ PIPELINE_ID_KEY,
BUTTON_TOOLTIP_RETRY,
BUTTON_TOOLTIP_CANCEL,
TRACKING_CATEGORIES,
@@ -20,51 +18,43 @@ import {
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
-jest.mock('~/ci/event_hub');
-
describe('Pipelines Table', () => {
- let pipeline;
let wrapper;
let trackingSpy;
const defaultProvide = {
- glFeatures: {},
- withFailedJobsDetails: false,
+ fullPath: '/my-project/',
+ useFailedJobsWidget: false,
};
- const provideWithDetails = {
- glFeatures: {
- ciJobFailuresInMr: true,
- },
- withFailedJobsDetails: true,
+ const provideWithFailedJobsWidget = {
+ useFailedJobsWidget: true,
};
- const defaultProps = {
- pipelines: [],
- viewType: 'root',
- pipelineKeyOption: PipelineKeyOptions[0],
- };
+ const { pipelines } = fixture;
- const createMockPipeline = () => {
- // Clone fixture as it could be modified by tests
- const { pipelines } = JSON.parse(JSON.stringify(fixture));
- return pipelines.find((p) => p.user !== null && p.commit !== null);
+ const defaultProps = {
+ pipelines,
+ pipelineIdType: PIPELINE_ID_KEY,
};
- const createComponent = (props = {}, provide = {}) => {
- wrapper = extendedWrapper(
- mount(PipelinesTable, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- provide: {
- ...defaultProvide,
- ...provide,
- },
- stubs: ['PipelineFailedJobsWidget'],
- }),
- );
+ const [firstPipeline] = pipelines;
+
+ const createComponent = ({ props = {}, provide = {}, stubs = {} } = {}) => {
+ wrapper = mountExtended(PipelinesTable, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ stubs: {
+ PipelineOperations: true,
+ ...stubs,
+ },
+ });
};
const findGlTableLite = () => wrapper.findComponent(GlTableLite);
@@ -84,13 +74,9 @@ describe('Pipelines Table', () => {
const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button');
- beforeEach(() => {
- pipeline = createMockPipeline();
- });
-
describe('Pipelines Table', () => {
beforeEach(() => {
- createComponent({ pipelines: [pipeline], viewType: 'root' });
+ createComponent({ props: { viewType: 'root' } });
});
it('displays table', () => {
@@ -105,7 +91,7 @@ describe('Pipelines Table', () => {
});
it('should display a table row', () => {
- expect(findTableRows()).toHaveLength(1);
+ expect(findTableRows()).toHaveLength(pipelines.length);
});
describe('status cell', () => {
@@ -120,7 +106,7 @@ describe('Pipelines Table', () => {
});
it('should display the pipeline id', () => {
- expect(findPipelineInfo().text()).toContain(`#${pipeline.id}`);
+ expect(findPipelineInfo().text()).toContain(`#${firstPipeline.id}`);
});
});
@@ -130,24 +116,33 @@ describe('Pipelines Table', () => {
});
it('should render the right number of stages', () => {
- const stagesLength = pipeline.details.stages.length;
- expect(findLegacyPipelineMiniGraph().props('stages').length).toBe(stagesLength);
+ const stagesLength = firstPipeline.details.stages.length;
+ expect(findLegacyPipelineMiniGraph().props('stages')).toHaveLength(stagesLength);
});
it('should render the latest downstream pipelines only', () => {
// component receives two downstream pipelines. one of them is already outdated
// because we retried the trigger job, so the mini pipeline graph will only
// render the newly created downstream pipeline instead
- expect(pipeline.triggered).toHaveLength(2);
+ expect(firstPipeline.triggered).toHaveLength(2);
expect(findLegacyPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
});
describe('when pipeline does not have stages', () => {
beforeEach(() => {
- pipeline = createMockPipeline();
- pipeline.details.stages = [];
-
- createComponent({ pipelines: [pipeline] });
+ createComponent({
+ props: {
+ pipelines: [
+ {
+ ...firstPipeline,
+ details: {
+ ...firstPipeline.details,
+ stages: [],
+ },
+ },
+ ],
+ },
+ });
});
it('stages are not rendered', () => {
@@ -163,6 +158,10 @@ describe('Pipelines Table', () => {
});
describe('operations cell', () => {
+ beforeEach(() => {
+ createComponent({ stubs: { PipelineOperations } });
+ });
+
it('should render pipeline operations', () => {
expect(findActions().exists()).toBe(true);
});
@@ -183,97 +182,101 @@ describe('Pipelines Table', () => {
});
describe('failed jobs details', () => {
- describe('row', () => {
- describe('when the FF is disabled', () => {
- beforeEach(() => {
- createComponent({ pipelines: [pipeline] });
- });
+ describe('when `useFailedJobsWidget` value is provided', () => {
+ beforeEach(() => {
+ createComponent({ provide: provideWithFailedJobsWidget });
+ });
- it('does not render', () => {
- expect(findTableRows()).toHaveLength(1);
- expect(findPipelineFailureWidget().exists()).toBe(false);
- });
+ it('renders', () => {
+ // We have 2 rows per pipeline with the widget
+ expect(findTableRows()).toHaveLength(pipelines.length * 2);
+ expect(findPipelineFailureWidget().exists()).toBe(true);
});
- describe('when the FF is enabled', () => {
- describe('and `withFailedJobsDetails` value is provided', () => {
- beforeEach(() => {
- createComponent({ pipelines: [pipeline] }, provideWithDetails);
- });
-
- it('renders', () => {
- expect(findTableRows()).toHaveLength(2);
- expect(findPipelineFailureWidget().exists()).toBe(true);
- });
-
- it('passes the expected props', () => {
- expect(findPipelineFailureWidget().props()).toStrictEqual({
- failedJobsCount: pipeline.failed_builds.length,
- isPipelineActive: pipeline.active,
- pipelineIid: pipeline.iid,
- pipelinePath: pipeline.path,
- // Make sure the forward slash was removed
- projectPath: 'frontend-fixtures/pipelines-project',
- });
- });
+ it('passes the expected props', () => {
+ expect(findPipelineFailureWidget().props()).toStrictEqual({
+ failedJobsCount: firstPipeline.failed_builds_count,
+ isPipelineActive: firstPipeline.active,
+ pipelineIid: firstPipeline.iid,
+ pipelinePath: firstPipeline.path,
+ // Make sure the forward slash was removed
+ projectPath: 'frontend-fixtures/pipelines-project',
});
+ });
+ });
- describe('and `withFailedJobsDetails` value is not provided', () => {
- beforeEach(() => {
- createComponent(
- { pipelines: [pipeline] },
- { glFeatures: { ciJobFailuresInMr: true } },
- );
- });
-
- it('does not render', () => {
- expect(findTableRows()).toHaveLength(1);
- expect(findPipelineFailureWidget().exists()).toBe(false);
- });
- });
+ describe('and `useFailedJobsWidget` value is not provided', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not render', () => {
+ expect(findTableRows()).toHaveLength(pipelines.length);
+ expect(findPipelineFailureWidget().exists()).toBe(false);
});
});
});
+ });
- describe('tracking', () => {
- beforeEach(() => {
- trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ describe('events', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('when confirming to cancel a pipeline', () => {
+ beforeEach(async () => {
+ await findActions().vm.$emit('cancel-pipeline', firstPipeline);
});
- afterEach(() => {
- unmockTracking();
+ it('emits the `cancel-pipeline` event', () => {
+ expect(wrapper.emitted('cancel-pipeline')).toEqual([[firstPipeline]]);
});
+ });
- it('tracks status badge click', () => {
- findCiBadgeLink().vm.$emit('ciStatusBadgeClick');
+ describe('when retrying a pipeline', () => {
+ beforeEach(() => {
+ findActions().vm.$emit('retry-pipeline', firstPipeline);
+ });
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
- label: TRACKING_CATEGORIES.table,
- });
+ it('emits the `retry-pipeline` event', () => {
+ expect(wrapper.emitted('retry-pipeline')).toEqual([[firstPipeline]]);
});
+ });
- it('tracks retry pipeline button click', () => {
- findRetryBtn().vm.$emit('click');
+ describe('when refreshing pipelines', () => {
+ beforeEach(() => {
+ findActions().vm.$emit('refresh-pipelines-table');
+ });
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_retry_button', {
- label: TRACKING_CATEGORIES.table,
- });
+ it('emits the `refresh-pipelines-table` event', () => {
+ expect(wrapper.emitted('refresh-pipelines-table')).toEqual([[]]);
});
+ });
+ });
- it('tracks cancel pipeline button click', () => {
- findCancelBtn().vm.$emit('click');
+ describe('tracking', () => {
+ beforeEach(() => {
+ createComponent();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_cancel_button', {
- label: TRACKING_CATEGORIES.table,
- });
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks status badge click', () => {
+ findCiBadgeLink().vm.$emit('ciStatusBadgeClick');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
+ label: TRACKING_CATEGORIES.table,
});
+ });
- it('tracks pipeline mini graph stage click', () => {
- findLegacyPipelineMiniGraph().vm.$emit('miniGraphStageClick');
+ it('tracks pipeline mini graph stage click', () => {
+ findLegacyPipelineMiniGraph().vm.$emit('miniGraphStageClick');
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_minigraph', {
- label: TRACKING_CATEGORIES.table,
- });
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_minigraph', {
+ label: TRACKING_CATEGORIES.table,
});
});
});
diff --git a/spec/frontend/ci/job_details/components/job_header_spec.js b/spec/frontend/ci/job_details/components/job_header_spec.js
index 6fc55732353..609369316f5 100644
--- a/spec/frontend/ci/job_details/components/job_header_spec.js
+++ b/spec/frontend/ci/job_details/components/job_header_spec.js
@@ -16,7 +16,7 @@ describe('Header CI Component', () => {
text: 'failed',
details_path: 'path',
},
- name: 'Job build_job',
+ name: 'build_job',
time: '2017-05-08T14:57:39.781Z',
user: {
id: 1234,
@@ -34,17 +34,15 @@ describe('Header CI Component', () => {
const findUserLink = () => wrapper.findComponent(GlAvatarLink);
const findSidebarToggleBtn = () => wrapper.findComponent(GlButton);
const findStatusTooltip = () => wrapper.findComponent(GlTooltip);
- const findActionButtons = () => wrapper.findByTestId('job-header-action-buttons');
const findJobName = () => wrapper.findByTestId('job-name');
- const createComponent = (props, slots) => {
+ const createComponent = (props) => {
wrapper = extendedWrapper(
shallowMount(JobHeader, {
propsData: {
...defaultProps,
...props,
},
- ...slots,
}),
);
};
@@ -54,6 +52,10 @@ describe('Header CI Component', () => {
createComponent();
});
+ it('renders the correct job name', () => {
+ expect(findJobName().text()).toBe(defaultProps.name);
+ });
+
it('should render status badge', () => {
expect(findCiBadgeLink().exists()).toBe(true);
});
@@ -65,10 +67,6 @@ describe('Header CI Component', () => {
it('should render sidebar toggle button', () => {
expect(findSidebarToggleBtn().exists()).toBe(true);
});
-
- it('should not render header action buttons when slot is empty', () => {
- expect(findActionButtons().exists()).toBe(false);
- });
});
describe('user avatar', () => {
@@ -124,31 +122,12 @@ describe('Header CI Component', () => {
});
});
- describe('job name', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('should render the job name', () => {
- expect(findJobName().text()).toBe('Job build_job');
- });
- });
-
- describe('slot', () => {
- it('should render header action buttons', () => {
- createComponent({}, { slots: { default: 'Test Actions' } });
-
- expect(findActionButtons().exists()).toBe(true);
- expect(findActionButtons().text()).toBe('Test Actions');
- });
- });
-
describe('shouldRenderTriggeredLabel', () => {
it('should render created keyword when the shouldRenderTriggeredLabel is false', () => {
createComponent({ shouldRenderTriggeredLabel: false });
- expect(wrapper.text()).toContain('created');
- expect(wrapper.text()).not.toContain('started');
+ expect(wrapper.text()).toContain('Created');
+ expect(wrapper.text()).not.toContain('Started');
});
});
});
diff --git a/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js b/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js
index e3d5c448338..5abf2a5ce53 100644
--- a/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js
+++ b/spec/frontend/ci/job_details/components/log/collapsible_section_spec.js
@@ -1,6 +1,7 @@
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
import CollapsibleSection from '~/ci/job_details/components/log/collapsible_section.vue';
+import LogLine from '~/ci/job_details/components/log/line.vue';
import LogLineHeader from '~/ci/job_details/components/log/line_header.vue';
import { collapsibleSectionClosed, collapsibleSectionOpened } from './mock_data';
@@ -9,9 +10,9 @@ describe('Job Log Collapsible Section', () => {
const jobLogEndpoint = 'jobs/335';
- const findCollapsibleLine = () => wrapper.find('.collapsible-line');
- const findCollapsibleLineSvg = () => wrapper.find('.collapsible-line svg');
const findLogLineHeader = () => wrapper.findComponent(LogLineHeader);
+ const findLogLineHeaderSvg = () => findLogLineHeader().find('svg');
+ const findLogLines = () => wrapper.findAllComponents(LogLine);
const createComponent = (props = {}) => {
wrapper = mount(CollapsibleSection, {
@@ -30,11 +31,16 @@ describe('Job Log Collapsible Section', () => {
});
it('renders clickable header line', () => {
- expect(findCollapsibleLine().attributes('role')).toBe('button');
+ expect(findLogLineHeader().text()).toBe('1 foo');
+ expect(findLogLineHeader().attributes('role')).toBe('button');
});
- it('renders an icon with the closed state', () => {
- expect(findCollapsibleLineSvg().attributes('data-testid')).toBe('chevron-lg-right-icon');
+ it('renders an icon with a closed state', () => {
+ expect(findLogLineHeaderSvg().attributes('data-testid')).toBe('chevron-lg-right-icon');
+ });
+
+ it('does not render collapsed lines', () => {
+ expect(findLogLines()).toHaveLength(0);
});
});
@@ -47,15 +53,17 @@ describe('Job Log Collapsible Section', () => {
});
it('renders clickable header line', () => {
- expect(findCollapsibleLine().attributes('role')).toBe('button');
+ expect(findLogLineHeader().text()).toContain('foo');
+ expect(findLogLineHeader().attributes('role')).toBe('button');
});
it('renders an icon with the open state', () => {
- expect(findCollapsibleLineSvg().attributes('data-testid')).toBe('chevron-lg-down-icon');
+ expect(findLogLineHeaderSvg().attributes('data-testid')).toBe('chevron-lg-down-icon');
});
- it('renders collapsible lines content', () => {
- expect(wrapper.findAll('.js-line').length).toEqual(collapsibleSectionOpened.lines.length);
+ it('renders collapsible lines', () => {
+ expect(findLogLines().at(0).text()).toContain('this is a collapsible nested section');
+ expect(findLogLines()).toHaveLength(collapsibleSectionOpened.lines.length);
});
});
@@ -65,7 +73,7 @@ describe('Job Log Collapsible Section', () => {
jobLogEndpoint,
});
- findCollapsibleLine().trigger('click');
+ findLogLineHeader().trigger('click');
await nextTick();
expect(wrapper.emitted('onClickCollapsibleLine').length).toBe(1);
diff --git a/spec/frontend/ci/job_details/components/log/line_header_spec.js b/spec/frontend/ci/job_details/components/log/line_header_spec.js
index 7d1b05346f2..45296e4b6c2 100644
--- a/spec/frontend/ci/job_details/components/log/line_header_spec.js
+++ b/spec/frontend/ci/job_details/components/log/line_header_spec.js
@@ -16,7 +16,7 @@ describe('Job Log Header Line', () => {
style: 'term-fg-l-green',
},
],
- lineNumber: 76,
+ lineNumber: 77,
},
isClosed: true,
path: '/jashkenas/underscore/-/jobs/335',
diff --git a/spec/frontend/ci/job_details/components/log/line_number_spec.js b/spec/frontend/ci/job_details/components/log/line_number_spec.js
index d5c1d0fd985..db964e341b7 100644
--- a/spec/frontend/ci/job_details/components/log/line_number_spec.js
+++ b/spec/frontend/ci/job_details/components/log/line_number_spec.js
@@ -5,7 +5,7 @@ describe('Job Log Line Number', () => {
let wrapper;
const data = {
- lineNumber: 0,
+ lineNumber: 1,
path: '/jashkenas/underscore/-/jobs/335',
};
diff --git a/spec/frontend/ci/job_details/components/log/line_spec.js b/spec/frontend/ci/job_details/components/log/line_spec.js
index b6f3a2b68df..dad41d0cd7f 100644
--- a/spec/frontend/ci/job_details/components/log/line_spec.js
+++ b/spec/frontend/ci/job_details/components/log/line_spec.js
@@ -224,7 +224,7 @@ describe('Job Log Line', () => {
offset: 24526,
content: [{ text: 'job log content' }],
section: 'custom-section',
- lineNumber: 76,
+ lineNumber: 77,
},
path: '/root/ci-project/-/jobs/6353',
});
diff --git a/spec/frontend/ci/job_details/components/log/log_spec.js b/spec/frontend/ci/job_details/components/log/log_spec.js
index cc1621b87d6..1931d5046dc 100644
--- a/spec/frontend/ci/job_details/components/log/log_spec.js
+++ b/spec/frontend/ci/job_details/components/log/log_spec.js
@@ -7,7 +7,7 @@ import { scrollToElement } from '~/lib/utils/common_utils';
import Log from '~/ci/job_details/components/log/log.vue';
import LogLineHeader from '~/ci/job_details/components/log/line_header.vue';
import { logLinesParser } from '~/ci/job_details/store/utils';
-import { jobLog } from './mock_data';
+import { mockJobLog, mockJobLogLineCount } from './mock_data';
jest.mock('~/lib/utils/common_utils', () => ({
...jest.requireActual('~/lib/utils/common_utils'),
@@ -39,7 +39,7 @@ describe('Job Log', () => {
};
state = {
- jobLog: logLinesParser(jobLog),
+ jobLog: logLinesParser(mockJobLog),
jobLogEndpoint: 'jobs/id',
};
@@ -57,15 +57,18 @@ describe('Job Log', () => {
createComponent();
});
- it('renders a line number for each open line', () => {
- expect(wrapper.find('#L1').text()).toBe('1');
- expect(wrapper.find('#L2').text()).toBe('2');
- expect(wrapper.find('#L3').text()).toBe('3');
- });
+ it.each([...Array(mockJobLogLineCount).keys()])(
+ 'renders a line number for each line %d',
+ (index) => {
+ const lineNumber = wrapper
+ .findAll('.js-log-line')
+ .at(index)
+ .find(`#L${index + 1}`);
- it('links to the provided path and correct line number', () => {
- expect(wrapper.find('#L1').attributes('href')).toBe(`${state.jobLogEndpoint}#L1`);
- });
+ expect(lineNumber.text()).toBe(`${index + 1}`);
+ expect(lineNumber.attributes('href')).toBe(`${state.jobLogEndpoint}#L${index + 1}`);
+ },
+ );
});
describe('collapsible sections', () => {
@@ -103,7 +106,7 @@ describe('Job Log', () => {
await waitForPromises();
- expect(wrapper.find('#L6').exists()).toBe(false);
+ expect(wrapper.find('#L9').exists()).toBe(false);
expect(scrollToElement).not.toHaveBeenCalled();
});
});
@@ -116,19 +119,19 @@ describe('Job Log', () => {
it('scrolls to line number', async () => {
createComponent();
- state.jobLog = logLinesParser(jobLog, [], '#L6');
+ state.jobLog = logLinesParser(mockJobLog, [], '#L6');
await waitForPromises();
expect(scrollToElement).toHaveBeenCalledTimes(1);
- state.jobLog = logLinesParser(jobLog, [], '#L7');
+ state.jobLog = logLinesParser(mockJobLog, [], '#L7');
await waitForPromises();
expect(scrollToElement).toHaveBeenCalledTimes(1);
});
it('line number within collapsed section is visible', () => {
- state.jobLog = logLinesParser(jobLog, [], '#L6');
+ state.jobLog = logLinesParser(mockJobLog, [], '#L6');
createComponent();
@@ -148,7 +151,7 @@ describe('Job Log', () => {
],
section: 'prepare-executor',
section_header: true,
- lineNumber: 2,
+ lineNumber: 3,
},
];
diff --git a/spec/frontend/ci/job_details/components/log/mock_data.js b/spec/frontend/ci/job_details/components/log/mock_data.js
index fa51b92a044..14669872cc1 100644
--- a/spec/frontend/ci/job_details/components/log/mock_data.js
+++ b/spec/frontend/ci/job_details/components/log/mock_data.js
@@ -1,4 +1,4 @@
-export const jobLog = [
+export const mockJobLog = [
{
offset: 1000,
content: [{ text: 'Running with gitlab-runner 12.1.0 (de7731dd)' }],
@@ -19,69 +19,50 @@ export const jobLog = [
},
{
offset: 1003,
- content: [{ text: 'Starting service postgres:9.6.14 ...', style: 'text-green' }],
+ content: [{ text: 'Docker executor with image registry.gitlab.com ...' }],
section: 'prepare-executor',
},
{
offset: 1004,
- content: [
- {
- text: 'Restore cache',
- style: 'term-fg-l-cyan term-bold',
- },
- ],
- section: 'restore-cache',
- section_header: true,
- section_options: {
- collapsed: 'true',
- },
+ content: [{ text: 'Starting service ...', style: 'term-fg-l-green' }],
+ section: 'prepare-executor',
},
{
offset: 1005,
- content: [
- {
- text: 'Checking cache for ruby-gems-debian-bullseye-ruby-3.0-16...',
- style: 'term-fg-l-green term-bold',
- },
- ],
- section: 'restore-cache',
- },
-];
-
-export const utilsMockData = [
- {
- offset: 1001,
- content: [{ text: ' on docker-auto-scale-com 8a6210b8' }],
+ content: [],
+ section: 'prepare-executor',
+ section_duration: '00:09',
},
{
- offset: 1002,
+ offset: 1006,
content: [
{
- text:
- 'Using Docker executor with image dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.6.6-golang-1.14-git-2.28-lfs-2.9-chrome-84-node-12.x-yarn-1.21-postgresql-11-graphicsmagick-1.3.34',
+ text: 'Getting source from Git repository',
},
],
- section: 'prepare-executor',
+ section: 'get-sources',
section_header: true,
},
{
- offset: 1003,
- content: [{ text: 'Starting service postgres:9.6.14 ...' }],
- section: 'prepare-executor',
+ offset: 1007,
+ content: [{ text: 'Fetching changes with git depth set to 20...' }],
+ section: 'get-sources',
},
{
- offset: 1004,
- content: [{ text: 'Pulling docker image postgres:9.6.14 ...', style: 'term-fg-l-green' }],
- section: 'prepare-executor',
+ offset: 1008,
+ content: [{ text: 'Initialized empty Git repository', style: 'term-fg-l-green' }],
+ section: 'get-sources',
},
{
- offset: 1005,
+ offset: 1009,
content: [],
- section: 'prepare-executor',
- section_duration: '10:00',
+ section: 'get-sources',
+ section_duration: '00:19',
},
];
+export const mockJobLogLineCount = 8; // `text` entries in mockJobLog
+
export const originalTrace = [
{
offset: 1,
@@ -191,7 +172,7 @@ export const collapsibleSectionClosed = {
offset: 80,
content: [{ text: 'this is a collapsible nested section' }],
section: 'prepare-script',
- lineNumber: 3,
+ lineNumber: 2,
},
],
};
@@ -212,7 +193,7 @@ export const collapsibleSectionOpened = {
offset: 80,
content: [{ text: 'this is a collapsible nested section' }],
section: 'prepare-script',
- lineNumber: 3,
+ lineNumber: 2,
},
],
};
diff --git a/spec/frontend/ci/job_details/components/sidebar/artifacts_block_spec.js b/spec/frontend/ci/job_details/components/sidebar/artifacts_block_spec.js
index 1d61bf3243f..e539be2b220 100644
--- a/spec/frontend/ci/job_details/components/sidebar/artifacts_block_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/artifacts_block_spec.js
@@ -30,31 +30,31 @@ describe('Artifacts block', () => {
'These artifacts are the latest. They will not be deleted (even if expired) until newer artifacts are available.';
const expiredArtifact = {
- expire_at: expireAt,
+ expireAt,
expired: true,
locked: false,
};
const nonExpiredArtifact = {
- download_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/download',
- browse_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/browse',
- keep_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/keep',
- expire_at: expireAt,
+ downloadPath: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/download',
+ browsePath: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/browse',
+ keepPath: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/keep',
+ expireAt,
expired: false,
locked: false,
};
const lockedExpiredArtifact = {
...expiredArtifact,
- download_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/download',
- browse_path: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/browse',
+ downloadPath: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/download',
+ browsePath: '/gitlab-org/gitlab-foss/-/jobs/98314558/artifacts/browse',
expired: true,
locked: true,
};
const lockedNonExpiredArtifact = {
...nonExpiredArtifact,
- keep_path: undefined,
+ keepPath: undefined,
locked: true,
};
diff --git a/spec/frontend/ci/job_details/components/sidebar/sidebar_header_spec.js b/spec/frontend/ci/job_details/components/sidebar/sidebar_header_spec.js
index 1063bec6f3b..81181fc71b2 100644
--- a/spec/frontend/ci/job_details/components/sidebar/sidebar_header_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/sidebar_header_spec.js
@@ -55,15 +55,9 @@ describe('Sidebar Header', () => {
const findEraseButton = () => wrapper.findByTestId('job-log-erase-link');
const findNewIssueButton = () => wrapper.findByTestId('job-new-issue');
const findTerminalLink = () => wrapper.findByTestId('terminal-link');
- const findJobName = () => wrapper.findByTestId('job-name');
const findRetryButton = () => wrapper.findComponent(JobRetryButton);
describe('when rendering contents', () => {
- it('renders the correct job name', async () => {
- await createComponentWithApollo();
- expect(findJobName().text()).toBe(mockJobResponse.data.project.job.name);
- });
-
it('does not render buttons with no paths', async () => {
await createComponentWithApollo();
expect(findCancelButton().exists()).toBe(false);
diff --git a/spec/frontend/ci/job_details/components/sidebar/sidebar_job_details_container_spec.js b/spec/frontend/ci/job_details/components/sidebar/sidebar_job_details_container_spec.js
index e188d99b8b1..37a2ca75df0 100644
--- a/spec/frontend/ci/job_details/components/sidebar/sidebar_job_details_container_spec.js
+++ b/spec/frontend/ci/job_details/components/sidebar/sidebar_job_details_container_spec.js
@@ -53,7 +53,6 @@ describe('Job Sidebar Details Container', () => {
['erased_at', 'Erased: 3 weeks ago'],
['finished_at', 'Finished: 3 weeks ago'],
['queued_duration', 'Queued: 9 seconds'],
- ['id', 'Job ID: #4757'],
['runner', 'Runner: #1 (ABCDEFGH) local ci runner'],
['coverage', 'Coverage: 20%'],
])('uses %s to render job-%s', async (detail, value) => {
@@ -78,7 +77,7 @@ describe('Job Sidebar Details Container', () => {
createWrapper();
await store.dispatch('receiveJobSuccess', job);
- expect(findAllDetailsRow()).toHaveLength(8);
+ expect(findAllDetailsRow()).toHaveLength(7);
});
describe('duration row', () => {
diff --git a/spec/frontend/ci/job_details/job_app_spec.js b/spec/frontend/ci/job_details/job_app_spec.js
index c2d91771495..ff84b2d0283 100644
--- a/spec/frontend/ci/job_details/job_app_spec.js
+++ b/spec/frontend/ci/job_details/job_app_spec.js
@@ -31,8 +31,6 @@ describe('Job App', () => {
const initSettings = {
endpoint: `${TEST_HOST}jobs/123.json`,
pagePath: `${TEST_HOST}jobs/123`,
- logState:
- 'eyJvZmZzZXQiOjE3NDUxLCJuX29wZW5fdGFncyI6MCwiZmdfY29sb3IiOm51bGwsImJnX2NvbG9yIjpudWxsLCJzdHlsZV9tYXNrIjowfQ%3D%3D',
};
const props = {
diff --git a/spec/frontend/ci/job_details/store/actions_spec.js b/spec/frontend/ci/job_details/store/actions_spec.js
index bb5c1fe32bd..2799bc9578c 100644
--- a/spec/frontend/ci/job_details/store/actions_spec.js
+++ b/spec/frontend/ci/job_details/store/actions_spec.js
@@ -2,7 +2,6 @@ import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import {
- setJobEndpoint,
setJobLogOptions,
clearEtagPoll,
stopPolling,
@@ -39,25 +38,21 @@ describe('Job State actions', () => {
mockedState = state();
});
- describe('setJobEndpoint', () => {
- it('should commit SET_JOB_ENDPOINT mutation', () => {
- return testAction(
- setJobEndpoint,
- 'job/872324.json',
- mockedState,
- [{ type: types.SET_JOB_ENDPOINT, payload: 'job/872324.json' }],
- [],
- );
- });
- });
-
describe('setJobLogOptions', () => {
it('should commit SET_JOB_LOG_OPTIONS mutation', () => {
return testAction(
setJobLogOptions,
- { pagePath: 'job/872324/trace.json' },
+ { endpoint: '/group1/project1/-/jobs/99.json', pagePath: '/group1/project1/-/jobs/99' },
mockedState,
- [{ type: types.SET_JOB_LOG_OPTIONS, payload: { pagePath: 'job/872324/trace.json' } }],
+ [
+ {
+ type: types.SET_JOB_LOG_OPTIONS,
+ payload: {
+ endpoint: '/group1/project1/-/jobs/99.json',
+ pagePath: '/group1/project1/-/jobs/99',
+ },
+ },
+ ],
[],
);
});
diff --git a/spec/frontend/ci/job_details/store/mutations_spec.js b/spec/frontend/ci/job_details/store/mutations_spec.js
index 0835c534fb9..78b29efed68 100644
--- a/spec/frontend/ci/job_details/store/mutations_spec.js
+++ b/spec/frontend/ci/job_details/store/mutations_spec.js
@@ -12,11 +12,17 @@ describe('Jobs Store Mutations', () => {
stateCopy = state();
});
- describe('SET_JOB_ENDPOINT', () => {
+ describe('SET_JOB_LOG_OPTIONS', () => {
it('should set jobEndpoint', () => {
- mutations[types.SET_JOB_ENDPOINT](stateCopy, 'job/21312321.json');
+ mutations[types.SET_JOB_LOG_OPTIONS](stateCopy, {
+ endpoint: '/group1/project1/-/jobs/99.json',
+ pagePath: '/group1/project1/-/jobs/99',
+ });
- expect(stateCopy.jobEndpoint).toEqual('job/21312321.json');
+ expect(stateCopy).toMatchObject({
+ jobLogEndpoint: '/group1/project1/-/jobs/99',
+ jobEndpoint: '/group1/project1/-/jobs/99.json',
+ });
});
});
@@ -39,13 +45,13 @@ describe('Jobs Store Mutations', () => {
describe('RECEIVE_JOB_LOG_SUCCESS', () => {
describe('when job log has state', () => {
it('sets jobLogState', () => {
- const stateLog =
+ const logState =
'eyJvZmZzZXQiOjczNDQ1MSwibl9vcGVuX3RhZ3MiOjAsImZnX2NvbG9yIjpudWxsLCJiZ19jb2xvciI6bnVsbCwic3R5bGVfbWFzayI6MH0=';
mutations[types.RECEIVE_JOB_LOG_SUCCESS](stateCopy, {
- state: stateLog,
+ state: logState,
});
- expect(stateCopy.jobLogState).toEqual(stateLog);
+ expect(stateCopy.jobLogState).toEqual(logState);
});
});
@@ -100,7 +106,7 @@ describe('Jobs Store Mutations', () => {
{
offset: 1,
content: [{ text: 'Running with gitlab-runner 11.12.1 (5a147c92)' }],
- lineNumber: 0,
+ lineNumber: 1,
},
]);
});
@@ -121,7 +127,7 @@ describe('Jobs Store Mutations', () => {
{
offset: 0,
content: [{ text: 'Running with gitlab-runner 11.11.1 (5a147c92)' }],
- lineNumber: 0,
+ lineNumber: 1,
},
]);
});
diff --git a/spec/frontend/ci/job_details/store/utils_spec.js b/spec/frontend/ci/job_details/store/utils_spec.js
index 4ffba35761e..394ce0ab737 100644
--- a/spec/frontend/ci/job_details/store/utils_spec.js
+++ b/spec/frontend/ci/job_details/store/utils_spec.js
@@ -6,10 +6,10 @@ import {
addDurationToHeader,
isCollapsibleSection,
findOffsetAndRemove,
- getIncrementalLineNumber,
+ getNextLineNumber,
} from '~/ci/job_details/store/utils';
import {
- utilsMockData,
+ mockJobLog,
originalTrace,
regularIncremental,
regularIncrementalRepeated,
@@ -187,39 +187,49 @@ describe('Jobs Store Utils', () => {
let result;
beforeEach(() => {
- result = logLinesParser(utilsMockData);
+ result = logLinesParser(mockJobLog);
});
describe('regular line', () => {
it('adds a lineNumber property with correct index', () => {
- expect(result[0].lineNumber).toEqual(0);
- expect(result[1].line.lineNumber).toEqual(1);
+ expect(result[0].lineNumber).toEqual(1);
+ expect(result[1].lineNumber).toEqual(2);
+ expect(result[2].line.lineNumber).toEqual(3);
+ expect(result[2].lines[0].lineNumber).toEqual(4);
+ expect(result[2].lines[1].lineNumber).toEqual(5);
+ expect(result[3].line.lineNumber).toEqual(6);
+ expect(result[3].lines[0].lineNumber).toEqual(7);
+ expect(result[3].lines[1].lineNumber).toEqual(8);
});
});
describe('collapsible section', () => {
it('adds a `isClosed` property', () => {
- expect(result[1].isClosed).toEqual(false);
+ expect(result[2].isClosed).toEqual(false);
+ expect(result[3].isClosed).toEqual(false);
});
it('adds a `isHeader` property', () => {
- expect(result[1].isHeader).toEqual(true);
+ expect(result[2].isHeader).toEqual(true);
+ expect(result[3].isHeader).toEqual(true);
});
it('creates a lines array property with the content of the collapsible section', () => {
- expect(result[1].lines.length).toEqual(2);
- expect(result[1].lines[0].content).toEqual(utilsMockData[2].content);
- expect(result[1].lines[1].content).toEqual(utilsMockData[3].content);
+ expect(result[2].lines.length).toEqual(2);
+ expect(result[2].lines[0].content).toEqual(mockJobLog[3].content);
+ expect(result[2].lines[1].content).toEqual(mockJobLog[4].content);
});
});
describe('section duration', () => {
it('adds the section information to the header section', () => {
- expect(result[1].line.section_duration).toEqual(utilsMockData[4].section_duration);
+ expect(result[2].line.section_duration).toEqual(mockJobLog[5].section_duration);
+ expect(result[3].line.section_duration).toEqual(mockJobLog[9].section_duration);
});
it('does not add section duration as a line', () => {
- expect(result[1].lines.includes(utilsMockData[4])).toEqual(false);
+ expect(result[2].lines.includes(mockJobLog[5])).toEqual(false);
+ expect(result[3].lines.includes(mockJobLog[9])).toEqual(false);
});
});
});
@@ -316,17 +326,24 @@ describe('Jobs Store Utils', () => {
});
});
- describe('getIncrementalLineNumber', () => {
- describe('when last line is 0', () => {
+ describe('getNextLineNumber', () => {
+ describe('when there is no previous log', () => {
+ it('returns 1', () => {
+ expect(getNextLineNumber([])).toEqual(1);
+ expect(getNextLineNumber(undefined)).toEqual(1);
+ });
+ });
+
+ describe('when last line is 1', () => {
it('returns 1', () => {
const log = [
{
content: [],
- lineNumber: 0,
+ lineNumber: 1,
},
];
- expect(getIncrementalLineNumber(log)).toEqual(1);
+ expect(getNextLineNumber(log)).toEqual(2);
});
});
@@ -343,7 +360,7 @@ describe('Jobs Store Utils', () => {
},
];
- expect(getIncrementalLineNumber(log)).toEqual(102);
+ expect(getNextLineNumber(log)).toEqual(102);
});
});
@@ -364,7 +381,7 @@ describe('Jobs Store Utils', () => {
},
];
- expect(getIncrementalLineNumber(log)).toEqual(102);
+ expect(getNextLineNumber(log)).toEqual(102);
});
});
@@ -391,7 +408,7 @@ describe('Jobs Store Utils', () => {
},
];
- expect(getIncrementalLineNumber(log)).toEqual(104);
+ expect(getNextLineNumber(log)).toEqual(104);
});
});
});
@@ -410,7 +427,7 @@ describe('Jobs Store Utils', () => {
text: 'Downloading',
},
],
- lineNumber: 0,
+ lineNumber: 1,
},
{
offset: 2,
@@ -419,7 +436,7 @@ describe('Jobs Store Utils', () => {
text: 'log line',
},
],
- lineNumber: 1,
+ lineNumber: 2,
},
]);
});
@@ -438,7 +455,7 @@ describe('Jobs Store Utils', () => {
text: 'log line',
},
],
- lineNumber: 0,
+ lineNumber: 1,
},
]);
});
@@ -462,7 +479,7 @@ describe('Jobs Store Utils', () => {
},
],
section: 'section',
- lineNumber: 0,
+ lineNumber: 1,
},
lines: [],
},
@@ -488,7 +505,7 @@ describe('Jobs Store Utils', () => {
},
],
section: 'section',
- lineNumber: 0,
+ lineNumber: 1,
},
lines: [
{
@@ -499,7 +516,7 @@ describe('Jobs Store Utils', () => {
},
],
section: 'section',
- lineNumber: 1,
+ lineNumber: 2,
},
],
},
diff --git a/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js b/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
index cb8f6ed8f9b..bb44d970bd7 100644
--- a/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
+++ b/spec/frontend/ci/jobs_page/components/job_cells/job_cell_spec.js
@@ -40,20 +40,20 @@ describe('Job Cell', () => {
};
describe('Job Id', () => {
- it('displays the job id and links to the job', () => {
+ it('displays the job id, job name and links to the job', () => {
createComponent();
- const expectedJobId = `#${getIdFromGraphQLId(mockJob.id)}`;
+ const expectedJobId = `#${getIdFromGraphQLId(mockJob.id)}: ${mockJob.name}`;
expect(findJobIdLink().text()).toBe(expectedJobId);
expect(findJobIdLink().attributes('href')).toBe(mockJob.detailedStatus.detailsPath);
expect(findJobIdNoLink().exists()).toBe(false);
});
- it('display the job id with no link', () => {
+ it('display the job id and job name with no link', () => {
createComponent(jobAsGuest);
- const expectedJobId = `#${getIdFromGraphQLId(jobAsGuest.id)}`;
+ const expectedJobId = `#${getIdFromGraphQLId(jobAsGuest.id)}: ${jobAsGuest.name}`;
expect(findJobIdNoLink().text()).toBe(expectedJobId);
expect(findJobIdNoLink().exists()).toBe(true);
diff --git a/spec/frontend/ci/jobs_page/components/job_cells/duration_cell_spec.js b/spec/frontend/ci/jobs_page/components/job_cells/status_cell_spec.js
index 21f14ba0c98..e66942cc730 100644
--- a/spec/frontend/ci/jobs_page/components/job_cells/duration_cell_spec.js
+++ b/spec/frontend/ci/jobs_page/components/job_cells/status_cell_spec.js
@@ -1,6 +1,6 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import DurationCell from '~/ci/jobs_page/components/job_cells/duration_cell.vue';
+import StatusCell from '~/ci/jobs_page/components/job_cells/status_cell.vue';
describe('Duration Cell', () => {
let wrapper;
@@ -12,7 +12,7 @@ describe('Duration Cell', () => {
const createComponent = (props) => {
wrapper = extendedWrapper(
- shallowMount(DurationCell, {
+ shallowMount(StatusCell, {
propsData: {
job: {
...props,
diff --git a/spec/frontend/ci/jobs_page/components/jobs_table_empty_state_spec.js b/spec/frontend/ci/jobs_page/components/jobs_table_empty_state_spec.js
index f4893c4077f..0f85c4590ec 100644
--- a/spec/frontend/ci/jobs_page/components/jobs_table_empty_state_spec.js
+++ b/spec/frontend/ci/jobs_page/components/jobs_table_empty_state_spec.js
@@ -6,7 +6,7 @@ describe('Jobs table empty state', () => {
let wrapper;
const pipelineEditorPath = '/root/project/-/ci/editor';
- const emptyStateSvgPath = 'assets/jobs-empty-state.svg';
+ const emptyStateSvgPath = 'illustrations/empty-state/empty-pipeline-md.svg';
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
diff --git a/spec/frontend/ci/jobs_page/components/jobs_table_spec.js b/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
index 3adb95bf371..d4e0ce92bc2 100644
--- a/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
+++ b/spec/frontend/ci/jobs_page/components/jobs_table_spec.js
@@ -2,6 +2,7 @@ import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JobsTable from '~/ci/jobs_page/components/jobs_table.vue';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { DEFAULT_FIELDS_ADMIN } from '~/ci/admin/jobs_table/constants';
import ProjectCell from '~/ci/admin/jobs_table/components/cells/project_cell.vue';
@@ -47,11 +48,11 @@ describe('Jobs Table', () => {
expect(findCiBadgeLink().exists()).toBe(true);
});
- it('displays the job stage and name', () => {
+ it('displays the job stage, id and name', () => {
const [firstJob] = mockJobsNodes;
- expect(findJobStage().text()).toBe(firstJob.stage.name);
- expect(findJobName().text()).toBe(firstJob.name);
+ expect(findJobStage().text()).toBe(`Stage: ${firstJob.stage.name}`);
+ expect(findJobName().text()).toBe(`#${getIdFromGraphQLId(firstJob.id)}: ${firstJob.name}`);
});
it('displays the coverage for only jobs that have coverage', () => {
diff --git a/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js b/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
index 107f0df5c02..de9ee8a16bf 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/job_item_spec.js
@@ -1,10 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
-import { GlBadge, GlModal, GlToast } from '@gitlab/ui';
+import { GlModal, GlToast } from '@gitlab/ui';
import JobItem from '~/ci/pipeline_details/graph/components/job_item.vue';
import axios from '~/lib/utils/axios_utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import ActionComponent from '~/ci/common/private/job_action_component.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import {
@@ -27,9 +28,10 @@ describe('pipeline graph job item', () => {
const findJobWithoutLink = () => wrapper.findByTestId('job-without-link');
const findJobWithLink = () => wrapper.findByTestId('job-with-link');
const findActionVueComponent = () => wrapper.findComponent(ActionComponent);
- const findActionComponent = () => wrapper.findByTestId('ci-action-component');
- const findBadge = () => wrapper.findComponent(GlBadge);
+ const findActionComponent = () => wrapper.findByTestId('ci-action-button');
+ const findBadge = () => wrapper.findByTestId('job-bridge-badge');
const findJobLink = () => wrapper.findByTestId('job-with-link');
+ const findJobCiBadge = () => wrapper.findComponent(CiBadgeLink);
const findModal = () => wrapper.findComponent(GlModal);
const clickOnModalPrimaryBtn = () => findModal().vm.$emit('primary');
@@ -57,6 +59,9 @@ describe('pipeline graph job item', () => {
mocks: {
...mocks,
},
+ stubs: {
+ CiBadgeLink,
+ },
});
};
@@ -81,7 +86,8 @@ describe('pipeline graph job item', () => {
expect(link.attributes('title')).toBe(`${mockJob.name} - ${mockJob.status.label}`);
- expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
+ expect(findJobCiBadge().exists()).toBe(true);
+ expect(findJobCiBadge().find('.ci-status-icon-success').exists()).toBe(true);
expect(wrapper.text()).toBe(mockJob.name);
});
@@ -99,7 +105,8 @@ describe('pipeline graph job item', () => {
});
it('should render status and name', () => {
- expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
+ expect(findJobCiBadge().exists()).toBe(true);
+ expect(findJobCiBadge().find('.ci-status-icon-success').exists()).toBe(true);
expect(findJobLink().exists()).toBe(false);
expect(wrapper.text()).toBe(mockJobWithoutDetails.name);
@@ -110,6 +117,15 @@ describe('pipeline graph job item', () => {
});
});
+ describe('CiBadgeLink', () => {
+ it('should not render a link', () => {
+ createWrapper();
+
+ expect(findJobCiBadge().exists()).toBe(true);
+ expect(findJobCiBadge().props('useLink')).toBe(false);
+ });
+ });
+
describe('action icon', () => {
it('should render the action icon', () => {
createWrapper();
diff --git a/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js b/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
index 5541b0db54a..5fe8581e81b 100644
--- a/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_details/graph/components/linked_pipeline_spec.js
@@ -37,7 +37,7 @@ describe('Linked pipeline', () => {
const findButton = () => wrapper.findComponent(GlButton);
const findCancelButton = () => wrapper.findByLabelText('Cancel downstream pipeline');
const findCardTooltip = () => wrapper.findComponent(GlTooltip);
- const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title');
+ const findDownstreamPipelineTitle = () => wrapper.findByTestId('downstream-title-content');
const findExpandButton = () => wrapper.findByTestId('expand-pipeline-button');
const findLinkedPipeline = () => wrapper.findComponent({ ref: 'linkedPipeline' });
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
diff --git a/spec/frontend/ci/pipeline_details/mock_data.js b/spec/frontend/ci/pipeline_details/mock_data.js
index e32d0a0df47..56365622544 100644
--- a/spec/frontend/ci/pipeline_details/mock_data.js
+++ b/spec/frontend/ci/pipeline_details/mock_data.js
@@ -640,7 +640,7 @@ export const mockPipeline = (projectPath) => {
triggered_by: null,
triggered: [],
},
- pipelineScheduleUrl: 'foo',
+ pipelineSchedulesPath: 'foo',
pipelineKey: 'id',
viewType: 'root',
};
@@ -865,7 +865,7 @@ export const mockPipelineTag = () => {
triggered_by: null,
triggered: [],
},
- pipelineScheduleUrl: 'foo',
+ pipelineSchedulesPath: 'foo',
pipelineKey: 'id',
viewType: 'root',
};
@@ -1072,7 +1072,7 @@ export const mockPipelineBranch = () => {
triggered_by: null,
triggered: [],
},
- pipelineScheduleUrl: 'foo',
+ pipelineSchedulesPath: 'foo',
pipelineKey: 'id',
viewType: 'root',
};
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
index 1a2ed60a6f4..9bb0618b758 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_status_spec.js
@@ -1,4 +1,4 @@
-import { GlIcon, GlLink, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
+import { GlIcon, GlLoadingIcon, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
@@ -43,7 +43,7 @@ describe('Pipeline Status', () => {
},
projectFullPath: mockProjectFullPath,
},
- stubs: { GlLink, GlSprintf },
+ stubs: { GlSprintf },
});
};
diff --git a/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
index 30a0b868c5f..4b357a9fc7c 100644
--- a/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/legacy_pipeline_stage_spec.js
@@ -2,7 +2,7 @@ import { GlDropdown } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import LegacyPipelineStage from '~/ci/pipeline_mini_graph/legacy_pipeline_stage.vue';
@@ -52,7 +52,7 @@ describe('Pipelines stage component', () => {
});
const findCiActionBtn = () => wrapper.find('.js-ci-action');
- const findCiIcon = () => wrapper.findComponent(CiIcon);
+ const findCiIcon = () => wrapper.findComponent(CiBadgeLink);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
const findDropdownMenu = () =>
@@ -106,17 +106,6 @@ describe('Pipelines stage component', () => {
expect(findDropdownToggle().exists()).toBe(true);
expect(findCiIcon().exists()).toBe(true);
});
-
- it('renders a borderless ci-icon', () => {
- expect(findCiIcon().exists()).toBe(true);
- expect(findCiIcon().props('isBorderless')).toBe(true);
- expect(findCiIcon().classes('borderless')).toBe(true);
- });
-
- it('renders a ci-icon with a custom border class', () => {
- expect(findCiIcon().exists()).toBe(true);
- expect(findCiIcon().classes('gl-border')).toBe(true);
- });
});
describe('when user opens dropdown and stage request is successful', () => {
diff --git a/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js b/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
index 0396029cdaf..3c9d235bfcc 100644
--- a/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
+++ b/spec/frontend/ci/pipeline_mini_graph/linked_pipelines_mini_list_spec.js
@@ -50,19 +50,6 @@ describe('Linked pipeline mini list', () => {
expect(findCiIcon().exists()).toBe(true);
});
- it('should render a borderless ci-icon', () => {
- expect(findCiIcon().exists()).toBe(true);
-
- expect(findCiIcon().props('isBorderless')).toBe(true);
- expect(findCiIcon().classes('borderless')).toBe(true);
- });
-
- it('should render a ci-icon with a custom border class', () => {
- expect(findCiIcon().exists()).toBe(true);
-
- expect(findCiIcon().classes('gl-border')).toBe(true);
- });
-
it('should render the correct ci status icon', () => {
expect(findCiIcon().classes('ci-status-icon-running')).toBe(true);
});
@@ -124,19 +111,6 @@ describe('Linked pipeline mini list', () => {
expect(findLinkedPipelineMiniList().classes('is-downstream')).toBe(true);
});
- it('should render a borderless ci-icon', () => {
- expect(findCiIcon().exists()).toBe(true);
-
- expect(findCiIcon().props('isBorderless')).toBe(true);
- expect(findCiIcon().classes('borderless')).toBe(true);
- });
-
- it('should render a ci-icon with a custom border class', () => {
- expect(findCiIcon().exists()).toBe(true);
-
- expect(findCiIcon().classes('gl-border')).toBe(true);
- });
-
it('should render the pipeline counter', () => {
expect(findLinkedPipelineCounter().exists()).toBe(true);
});
diff --git a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
index 1d4ae33c667..2807cc0f2a1 100644
--- a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
@@ -55,12 +55,12 @@ describe('Pipeline New Form', () => {
const findForm = () => wrapper.findComponent(GlForm);
const findRefsDropdown = () => wrapper.findComponent(RefsDropdown);
- const findSubmitButton = () => wrapper.findByTestId('run_pipeline_button');
- const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row');
+ const findSubmitButton = () => wrapper.findByTestId('run-pipeline-button');
+ const findVariableRows = () => wrapper.findAllByTestId('ci-variable-row-container');
const findRemoveIcons = () => wrapper.findAllByTestId('remove-ci-variable-row');
const findVariableTypes = () => wrapper.findAllByTestId('pipeline-form-ci-variable-type');
- const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key');
- const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value');
+ const findKeyInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-key-field');
+ const findValueInputs = () => wrapper.findAllByTestId('pipeline-form-ci-variable-value-field');
const findValueDropdowns = () =>
wrapper.findAllByTestId('pipeline-form-ci-variable-value-dropdown');
const findValueDropdownItems = (dropdown) => dropdown.findAllComponents(GlDropdownItem);
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_empty_state_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_empty_state_spec.js
new file mode 100644
index 00000000000..5ad0f915f62
--- /dev/null
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_empty_state_spec.js
@@ -0,0 +1,37 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlEmptyState, GlLink, GlSprintf } from '@gitlab/ui';
+import PipelineSchedulesEmptyState from '~/ci/pipeline_schedules/components/pipeline_schedules_empty_state.vue';
+
+describe('Pipeline Schedules Empty State', () => {
+ let wrapper;
+
+ const mockSchedulePath = 'root/test/-/pipeline_schedules/new"';
+
+ const createComponent = () => {
+ wrapper = shallowMount(PipelineSchedulesEmptyState, {
+ provide: {
+ newSchedulePath: mockSchedulePath,
+ },
+ stubs: { GlSprintf },
+ });
+ };
+
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findLink = () => wrapper.findComponent(GlLink);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('shows empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('has link to create new schedule', () => {
+ expect(findEmptyState().props('primaryButtonLink')).toBe(mockSchedulePath);
+ });
+
+ it('has link to help documentation', () => {
+ expect(findLink().attributes('href')).toBe('/help/ci/pipelines/schedules');
+ });
+});
diff --git a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
index eb76b0bfbb4..d1844d609f2 100644
--- a/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlEmptyState, GlLink, GlLoadingIcon, GlTabs } from '@gitlab/ui';
+import { GlAlert, GlEmptyState, GlLink, GlLoadingIcon, GlPagination, GlTabs } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { trimText } from 'helpers/text_helper';
@@ -14,6 +14,7 @@ import deletePipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/muta
import playPipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/play_pipeline_schedule.mutation.graphql';
import takeOwnershipMutation from '~/ci/pipeline_schedules/graphql/mutations/take_ownership.mutation.graphql';
import getPipelineSchedulesQuery from '~/ci/pipeline_schedules/graphql/queries/get_pipeline_schedules.query.graphql';
+import { SCHEDULES_PER_PAGE } from '~/ci/pipeline_schedules/constants';
import {
mockGetPipelineSchedulesGraphQLResponse,
mockPipelineScheduleNodes,
@@ -22,6 +23,7 @@ import {
playMutationResponse,
takeOwnershipMutationResponse,
emptyPipelineSchedulesResponse,
+ mockPipelineSchedulesResponseWithPagination,
} from '../mock_data';
Vue.use(VueApollo);
@@ -34,6 +36,9 @@ describe('Pipeline schedules app', () => {
let wrapper;
const successHandler = jest.fn().mockResolvedValue(mockGetPipelineSchedulesGraphQLResponse);
+ const successHandlerWithPagination = jest
+ .fn()
+ .mockResolvedValue(mockPipelineSchedulesResponseWithPagination);
const successEmptyHandler = jest.fn().mockResolvedValue(emptyPipelineSchedulesResponse);
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
@@ -81,6 +86,11 @@ describe('Pipeline schedules app', () => {
const findInactiveTab = () => wrapper.findByTestId('pipeline-schedules-inactive-tab');
const findSchedulesCharacteristics = () =>
wrapper.findByTestId('pipeline-schedules-characteristics');
+ const findPagination = () => wrapper.findComponent(GlPagination);
+ const setPage = async (page) => {
+ findPagination().vm.$emit('input', page);
+ await waitForPromises();
+ };
describe('default', () => {
beforeEach(() => {
@@ -107,6 +117,10 @@ describe('Pipeline schedules app', () => {
it('new schedule button links to new schedule path', () => {
expect(findNewButton().attributes('href')).toBe('/root/ci-project/-/pipeline_schedules/new');
});
+
+ it('does not display pagination when no next page exists', () => {
+ expect(findPagination().exists()).toBe(false);
+ });
});
describe('fetching pipeline schedules', () => {
@@ -333,6 +347,10 @@ describe('Pipeline schedules app', () => {
ids: null,
projectPath: 'gitlab-org/gitlab',
status: null,
+ first: SCHEDULES_PER_PAGE,
+ last: null,
+ nextPageCursor: '',
+ prevPageCursor: '',
});
});
});
@@ -370,4 +388,57 @@ describe('Pipeline schedules app', () => {
});
});
});
+
+ describe('pagination', () => {
+ const { pageInfo } = mockPipelineSchedulesResponseWithPagination.data.project.pipelineSchedules;
+
+ beforeEach(async () => {
+ createComponent([[getPipelineSchedulesQuery, successHandlerWithPagination]]);
+
+ await waitForPromises();
+ });
+
+ it('displays pagination', () => {
+ expect(findPagination().exists()).toBe(true);
+ expect(findPagination().props()).toMatchObject({
+ value: 1,
+ prevPage: Number(pageInfo.hasPreviousPage),
+ nextPage: Number(pageInfo.hasNextPage),
+ });
+ expect(successHandlerWithPagination).toHaveBeenCalledWith({
+ projectPath: 'gitlab-org/gitlab',
+ ids: null,
+ first: SCHEDULES_PER_PAGE,
+ last: null,
+ nextPageCursor: '',
+ prevPageCursor: '',
+ });
+ });
+
+ it('updates query variables when going to next page', async () => {
+ await setPage(2);
+
+ expect(successHandlerWithPagination).toHaveBeenCalledWith({
+ projectPath: 'gitlab-org/gitlab',
+ ids: null,
+ first: SCHEDULES_PER_PAGE,
+ last: null,
+ prevPageCursor: '',
+ nextPageCursor: pageInfo.endCursor,
+ });
+ expect(findPagination().props('value')).toEqual(2);
+ });
+
+ it('when switching tabs pagination should reset', async () => {
+ await setPage(2);
+
+ expect(findPagination().props('value')).toEqual(2);
+
+ await findInactiveTab().trigger('click');
+
+ await waitForPromises();
+
+ expect(findPagination().props('value')).toEqual(1);
+ });
+ });
});
diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 711b120c61e..1bff296305d 100644
--- a/spec/frontend/ci/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -48,6 +48,26 @@ export const mockSinglePipelineScheduleNodeNoVars = {
},
};
+export const mockPipelineSchedulesResponseWithPagination = {
+ data: {
+ currentUser: mockGetPipelineSchedulesGraphQLResponse.data.currentUser,
+ project: {
+ id: mockGetPipelineSchedulesGraphQLResponse.data.project.id,
+ pipelineSchedules: {
+ count: 3,
+ nodes: mockGetPipelineSchedulesGraphQLResponse.data.project.pipelineSchedules.nodes,
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: 'eyJpZCI6IjQ0In0',
+ endCursor: 'eyJpZCI6IjI4In0',
+ __typename: 'PageInfo',
+ },
+ },
+ },
+ },
+};
+
export const emptyPipelineSchedulesResponse = {
data: {
currentUser: {
@@ -59,6 +79,13 @@ export const emptyPipelineSchedulesResponse = {
pipelineSchedules: {
count: 0,
nodes: [],
+ pageInfo: {
+ hasNextPage: false,
+ hasPreviousPage: false,
+ startCursor: '',
+ endCursor: '',
+ __typename: 'PageInfo',
+ },
},
},
},
diff --git a/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
index b5c9a3030e0..6b0d5b18f7d 100644
--- a/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_labels_spec.js
@@ -15,6 +15,7 @@ describe('Pipeline label component', () => {
const findAutoDevopsTag = () => wrapper.findByTestId('pipeline-url-autodevops');
const findAutoDevopsTagLink = () => wrapper.findByTestId('pipeline-url-autodevops-link');
const findDetachedTag = () => wrapper.findByTestId('pipeline-url-detached');
+ const findMergedResultsTag = () => wrapper.findByTestId('pipeline-url-merged-results');
const findFailureTag = () => wrapper.findByTestId('pipeline-url-failure');
const findForkTag = () => wrapper.findByTestId('pipeline-url-fork');
const findTrainTag = () => wrapper.findByTestId('pipeline-url-train');
@@ -25,6 +26,7 @@ describe('Pipeline label component', () => {
wrapper = shallowMountExtended(PipelineLabelsComponent, {
propsData: { ...defaultProps, ...props },
provide: {
+ pipelineSchedulesPath: 'group/project/-/schedules',
targetProjectFullPath: projectPath,
},
});
@@ -41,6 +43,7 @@ describe('Pipeline label component', () => {
expect(findScheduledTag().exists()).toBe(false);
expect(findForkTag().exists()).toBe(false);
expect(findTrainTag().exists()).toBe(false);
+ expect(findMergedResultsTag().exists()).toBe(false);
});
it('should render the stuck tag when flag is provided', () => {
@@ -140,9 +143,33 @@ describe('Pipeline label component', () => {
expect(findForkTag().text()).toBe('fork');
});
+ it('should render the merged results badge when the pipeline is a merged results pipeline', () => {
+ const mergedResultsPipeline = defaultProps.pipeline;
+ mergedResultsPipeline.flags.merged_result_pipeline = true;
+
+ createComponent({
+ ...mergedResultsPipeline,
+ });
+
+ expect(findMergedResultsTag().text()).toBe('merged results');
+ });
+
+ it('should not render the merged results badge when the pipeline is not a merged results pipeline', () => {
+ const mergedResultsPipeline = defaultProps.pipeline;
+ mergedResultsPipeline.flags.merged_result_pipeline = false;
+
+ createComponent({
+ ...mergedResultsPipeline,
+ });
+
+ expect(findMergedResultsTag().exists()).toBe(false);
+ });
+
it('should render the train badge when the pipeline is a merge train pipeline', () => {
const mergeTrainPipeline = defaultProps.pipeline;
mergeTrainPipeline.flags.merge_train_pipeline = true;
+ // a merge train pipeline is also a merged results pipeline
+ mergeTrainPipeline.flags.merged_result_pipeline = true;
createComponent({
...mergeTrainPipeline,
@@ -161,4 +188,17 @@ describe('Pipeline label component', () => {
expect(findTrainTag().exists()).toBe(false);
});
+
+ it('should not render the merged results badge when the pipeline is a merge train pipeline', () => {
+ const mergeTrainPipeline = defaultProps.pipeline;
+ mergeTrainPipeline.flags.merge_train_pipeline = true;
+ // a merge train pipeline is also a merged results pipeline
+ mergeTrainPipeline.flags.merged_result_pipeline = true;
+
+ createComponent({
+ ...mergeTrainPipeline,
+ });
+
+ expect(findMergedResultsTag().exists()).toBe(false);
+ });
});
diff --git a/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js
index d2eab64b317..6205a37e291 100644
--- a/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_operations_spec.js
@@ -1,10 +1,13 @@
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PipelinesManualActions from '~/ci/pipelines_page/components/pipelines_manual_actions.vue';
import PipelineMultiActions from '~/ci/pipelines_page/components/pipeline_multi_actions.vue';
import PipelineOperations from '~/ci/pipelines_page/components/pipeline_operations.vue';
-import eventHub from '~/ci/event_hub';
+import PipelineStopModal from '~/ci/pipelines_page/components/pipeline_stop_modal.vue';
+import { TRACKING_CATEGORIES } from '~/ci/constants';
describe('Pipeline operations', () => {
+ let trackingSpy;
let wrapper;
const defaultProps = {
@@ -36,6 +39,7 @@ describe('Pipeline operations', () => {
const findMultiActions = () => wrapper.findComponent(PipelineMultiActions);
const findRetryBtn = () => wrapper.findByTestId('pipelines-retry-button');
const findCancelBtn = () => wrapper.findByTestId('pipelines-cancel-button');
+ const findPipelineStopModal = () => wrapper.findComponent(PipelineStopModal);
it('should display pipeline manual actions', () => {
createComponent();
@@ -49,28 +53,71 @@ describe('Pipeline operations', () => {
expect(findMultiActions().exists()).toBe(true);
});
+ it('does not show the confirmation modal', () => {
+ createComponent();
+
+ expect(findPipelineStopModal().props().showConfirmationModal).toBe(false);
+ });
+
+ describe('when cancelling a pipeline', () => {
+ beforeEach(async () => {
+ createComponent();
+ await findCancelBtn().vm.$emit('click');
+ });
+
+ it('should show a confirmation modal', () => {
+ expect(findPipelineStopModal().props().showConfirmationModal).toBe(true);
+ });
+
+ it('should emit cancel-pipeline event when confirming', async () => {
+ await findPipelineStopModal().vm.$emit('submit');
+
+ expect(wrapper.emitted('cancel-pipeline')).toEqual([[defaultProps.pipeline]]);
+ expect(findPipelineStopModal().props().showConfirmationModal).toBe(false);
+ });
+
+ it('should hide the modal when closing', async () => {
+ await findPipelineStopModal().vm.$emit('close-modal');
+
+ expect(findPipelineStopModal().props().showConfirmationModal).toBe(false);
+ });
+ });
+
describe('events', () => {
beforeEach(() => {
createComponent();
-
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
});
it('should emit retryPipeline event', () => {
findRetryBtn().vm.$emit('click');
- expect(eventHub.$emit).toHaveBeenCalledWith(
- 'retryPipeline',
- defaultProps.pipeline.retry_path,
- );
+ expect(wrapper.emitted('retry-pipeline')).toEqual([[defaultProps.pipeline]]);
+ });
+ });
+
+ describe('tracking', () => {
+ beforeEach(() => {
+ createComponent();
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ });
+
+ it('tracks retry pipeline button click', () => {
+ findRetryBtn().vm.$emit('click');
+
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_retry_button', {
+ label: TRACKING_CATEGORIES.table,
+ });
});
- it('should emit openConfirmationModal event', () => {
+ it('tracks cancel pipeline button click', () => {
findCancelBtn().vm.$emit('click');
- expect(eventHub.$emit).toHaveBeenCalledWith('openConfirmationModal', {
- pipeline: defaultProps.pipeline,
- endpoint: defaultProps.pipeline.cancel_path,
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_cancel_button', {
+ label: TRACKING_CATEGORIES.table,
});
});
});
diff --git a/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js b/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js
index 4d78a923542..1e276840c07 100644
--- a/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js
+++ b/spec/frontend/ci/pipelines_page/components/pipeline_stop_modal_spec.js
@@ -1,15 +1,17 @@
import { shallowMount } from '@vue/test-utils';
-import { GlSprintf } from '@gitlab/ui';
+import { GlModal, GlSprintf } from '@gitlab/ui';
import { mockPipelineHeader } from 'jest/ci/pipeline_details/mock_data';
import PipelineStopModal from '~/ci/pipelines_page/components/pipeline_stop_modal.vue';
describe('PipelineStopModal', () => {
let wrapper;
- const createComponent = () => {
+ const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMount(PipelineStopModal, {
propsData: {
pipeline: mockPipelineHeader,
+ showConfirmationModal: false,
+ ...props,
},
stubs: {
GlSprintf,
@@ -17,11 +19,43 @@ describe('PipelineStopModal', () => {
});
};
+ const findModal = () => wrapper.findComponent(GlModal);
+
beforeEach(() => {
createComponent();
});
- it('should render "stop pipeline" warning', () => {
- expect(wrapper.text()).toMatch(`You’re about to stop pipeline #${mockPipelineHeader.id}.`);
+ describe('when `showConfirmationModal` is false', () => {
+ it('passes the visiblity value to the modal', () => {
+ expect(findModal().props().visible).toBe(false);
+ });
+ });
+
+ describe('when `showConfirmationModal` is true', () => {
+ beforeEach(() => {
+ createComponent({ props: { showConfirmationModal: true } });
+ });
+
+ it('passes the visiblity value to the modal', () => {
+ expect(findModal().props().visible).toBe(true);
+ });
+
+ it('renders "stop pipeline" warning', () => {
+ expect(wrapper.text()).toMatch(`You're about to stop pipeline #${mockPipelineHeader.id}.`);
+ });
+ });
+
+ describe('events', () => {
+ beforeEach(() => {
+ createComponent({ props: { showConfirmationModal: true } });
+ });
+
+ it('emits the close-modal event when the visiblity changes', async () => {
+ expect(wrapper.emitted('close-modal')).toBeUndefined();
+
+ await findModal().vm.$emit('change', false);
+
+ expect(wrapper.emitted('close-modal')).toEqual([[]]);
+ });
});
});
diff --git a/spec/frontend/ci/pipelines_page/pipelines_spec.js b/spec/frontend/ci/pipelines_page/pipelines_spec.js
index 5d1f431e57c..fd95f98e7f8 100644
--- a/spec/frontend/ci/pipelines_page/pipelines_spec.js
+++ b/spec/frontend/ci/pipelines_page/pipelines_spec.js
@@ -28,7 +28,7 @@ import NavigationControls from '~/ci/pipelines_page/components/nav_controls.vue'
import PipelinesComponent from '~/ci/pipelines_page/pipelines.vue';
import PipelinesCiTemplates from '~/ci/pipelines_page/components/empty_state/pipelines_ci_templates.vue';
import PipelinesTableComponent from '~/ci/common/pipelines_table.vue';
-import { RAW_TEXT_WARNING, TRACKING_CATEGORIES } from '~/ci/constants';
+import { PIPELINE_IID_KEY, RAW_TEXT_WARNING, TRACKING_CATEGORIES } from '~/ci/constants';
import Store from '~/ci/pipeline_details/stores/pipelines_store';
import NavigationTabs from '~/vue_shared/components/navigation_tabs.vue';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
@@ -57,28 +57,23 @@ describe('Pipelines', () => {
let mockApollo;
let mock;
let trackingSpy;
+ let mutationMock;
- const paths = {
- emptyStateSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg',
- errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
- noPipelinesSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg',
+ const withPermissionsProps = {
ciLintPath: '/ci/lint',
resetCachePath: `${mockProjectPath}/settings/ci_cd/reset_cache`,
newPipelinePath: `${mockProjectPath}/pipelines/new`,
-
ciRunnerSettingsPath: `${mockProjectPath}/-/settings/ci_cd#js-runners-settings`,
- };
-
- const noPermissions = {
- emptyStateSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg',
- errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
- noPipelinesSvgPath: '/assets/illustrations/empty-state/empty-pipeline-md.svg',
+ canCreatePipeline: true,
};
const defaultProps = {
hasGitlabCi: true,
- canCreatePipeline: true,
- ...paths,
+ canCreatePipeline: false,
+ projectId: mockProjectId,
+ defaultBranchName: mockDefaultBranchName,
+ endpoint: mockPipelinesEndpoint,
+ params: {},
};
const findFilteredSearch = () => wrapper.findComponent(GlFilteredSearch);
@@ -87,10 +82,9 @@ describe('Pipelines', () => {
const findNavigationControls = () => wrapper.findComponent(NavigationControls);
const findPipelinesTable = () => wrapper.findComponent(PipelinesTableComponent);
const findTablePagination = () => wrapper.findComponent(TablePagination);
- const findPipelineKeyCollapsibleBoxVue = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findPipelineKeyCollapsibleBox = () => wrapper.findComponent(GlCollapsibleListbox);
const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`);
- const findPipelineKeyCollapsibleBox = () => wrapper.findByTestId('pipeline-key-collapsible-box');
const findRunPipelineButton = () => wrapper.findByTestId('run-pipeline-button');
const findCiLintButton = () => wrapper.findByTestId('ci-lint-button');
const findCleanCacheButton = () => wrapper.findByTestId('clear-cache-button');
@@ -98,25 +92,23 @@ describe('Pipelines', () => {
wrapper.find('[data-testid="mini-pipeline-graph-dropdown"] .dropdown-toggle');
const findPipelineUrlLinks = () => wrapper.findAll('[data-testid="pipeline-url-link"]');
- const createComponent = (props = defaultProps) => {
- const { mutationMock, ...restProps } = props;
+ const createComponent = ({ props = {}, withPermissions = true } = {}) => {
mockApollo = createMockApollo([[setSortPreferenceMutation, mutationMock]]);
+ const permissionsProps = withPermissions ? { ...withPermissionsProps } : {};
wrapper = extendedWrapper(
mount(PipelinesComponent, {
provide: {
pipelineEditorPath: '',
suggestedCiTemplates: [],
- ciRunnerSettingsPath: paths.ciRunnerSettingsPath,
+ ciRunnerSettingsPath: defaultProps.ciRunnerSettingsPath,
anyRunnersAvailable: true,
},
propsData: {
+ ...defaultProps,
+ ...permissionsProps,
+ ...props,
store: new Store(),
- projectId: mockProjectId,
- defaultBranchName: mockDefaultBranchName,
- endpoint: mockPipelinesEndpoint,
- params: {},
- ...restProps,
},
apolloProvider: mockApollo,
}),
@@ -124,12 +116,11 @@ describe('Pipelines', () => {
};
beforeEach(() => {
- setWindowLocation(TEST_HOST);
- });
-
- beforeEach(() => {
mock = new MockAdapter(axios);
+ setWindowLocation(TEST_HOST);
+ mutationMock = jest.fn();
+
jest.spyOn(window.history, 'pushState');
jest.spyOn(Api, 'projectUsers').mockResolvedValue(users);
jest.spyOn(Api, 'branches').mockResolvedValue({ data: branches });
@@ -169,7 +160,9 @@ describe('Pipelines', () => {
describe('when user has no permissions', () => {
beforeEach(async () => {
- createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
+ createComponent({
+ withPermissions: false,
+ });
await waitForPromises();
});
@@ -225,11 +218,13 @@ describe('Pipelines', () => {
});
it('renders Run pipeline link', () => {
- expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ expect(findRunPipelineButton().attributes('href')).toBe(
+ withPermissionsProps.newPipelinePath,
+ );
});
it('renders CI lint link', () => {
- expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
+ expect(findCiLintButton().attributes('href')).toBe(withPermissionsProps.ciLintPath);
});
it('renders Clear runner cache button', () => {
@@ -382,7 +377,7 @@ describe('Pipelines', () => {
it('should change the text to Show Pipeline IID', async () => {
expect(findPipelineKeyCollapsibleBox().exists()).toBe(true);
expect(findPipelineUrlLinks().at(0).text()).toBe(`#${mockFilteredPipeline.id}`);
- findPipelineKeyCollapsibleBoxVue().vm.$emit('select', 'iid');
+ findPipelineKeyCollapsibleBox().vm.$emit('select', PIPELINE_IID_KEY);
await waitForPromises();
@@ -390,21 +385,21 @@ describe('Pipelines', () => {
});
it('calls mutation to save idType preference', () => {
- const mutationMock = jest.fn().mockResolvedValue(setIdTypePreferenceMutationResponse);
- createComponent({ ...defaultProps, mutationMock });
+ mutationMock = jest.fn().mockResolvedValue(setIdTypePreferenceMutationResponse);
+ createComponent();
- findPipelineKeyCollapsibleBoxVue().vm.$emit('select', 'iid');
+ findPipelineKeyCollapsibleBox().vm.$emit('select', PIPELINE_IID_KEY);
- expect(mutationMock).toHaveBeenCalledWith({ input: { visibilityPipelineIdType: 'IID' } });
+ expect(mutationMock).toHaveBeenCalledWith({
+ input: { visibilityPipelineIdType: PIPELINE_IID_KEY.toUpperCase() },
+ });
});
it('captures error when mutation response has errors', async () => {
- const mutationMock = jest
- .fn()
- .mockResolvedValue(setIdTypePreferenceMutationResponseWithErrors);
- createComponent({ ...defaultProps, mutationMock });
+ mutationMock = jest.fn().mockResolvedValue(setIdTypePreferenceMutationResponseWithErrors);
+ createComponent();
- findPipelineKeyCollapsibleBoxVue().vm.$emit('select', 'iid');
+ findPipelineKeyCollapsibleBox().vm.$emit('select', PIPELINE_IID_KEY);
await waitForPromises();
expect(Sentry.captureException).toHaveBeenCalledWith(new Error('oh no!'));
@@ -610,11 +605,13 @@ describe('Pipelines', () => {
});
it('renders Run pipeline link', () => {
- expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ expect(findRunPipelineButton().attributes('href')).toBe(
+ withPermissionsProps.newPipelinePath,
+ );
});
it('renders CI lint link', () => {
- expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
+ expect(findCiLintButton().attributes('href')).toBe(withPermissionsProps.ciLintPath);
});
it('renders Clear runner cache button', () => {
@@ -651,7 +648,7 @@ describe('Pipelines', () => {
describe('when CI is not enabled and user has permissions', () => {
beforeEach(async () => {
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...paths });
+ createComponent({ props: { hasGitlabCi: false } });
await waitForPromises();
});
@@ -678,7 +675,7 @@ describe('Pipelines', () => {
describe('when CI is not enabled and user has no permissions', () => {
beforeEach(async () => {
- createComponent({ hasGitlabCi: false, canCreatePipeline: false, ...noPermissions });
+ createComponent({ props: { hasGitlabCi: false }, withPermissions: false });
await waitForPromises();
});
@@ -700,7 +697,7 @@ describe('Pipelines', () => {
describe('when CI is enabled and user has no permissions', () => {
beforeEach(() => {
- createComponent({ hasGitlabCi: true, canCreatePipeline: false, ...noPermissions });
+ createComponent({ props: { hasGitlabCi: true }, withPermissions: false });
return waitForPromises();
});
@@ -798,8 +795,10 @@ describe('Pipelines', () => {
describe('when user has no permissions', () => {
beforeEach(async () => {
- createComponent({ hasGitlabCi: false, canCreatePipeline: true, ...noPermissions });
-
+ createComponent({
+ props: { hasGitlabCi: false },
+ withPermissions: false,
+ });
await waitForPromises();
});
@@ -834,9 +833,11 @@ describe('Pipelines', () => {
});
it('renders buttons', () => {
- expect(findRunPipelineButton().attributes('href')).toBe(paths.newPipelinePath);
+ expect(findRunPipelineButton().attributes('href')).toBe(
+ withPermissionsProps.newPipelinePath,
+ );
- expect(findCiLintButton().attributes('href')).toBe(paths.ciLintPath);
+ expect(findCiLintButton().attributes('href')).toBe(withPermissionsProps.ciLintPath);
expect(findCleanCacheButton().text()).toBe('Clear runner caches');
});
diff --git a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
index c9349c64bfb..4a75c353487 100644
--- a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -103,11 +103,6 @@ describe('AdminRunnerShowApp', () => {
it('shows basic runner details', () => {
const expected = `Description My Runner
Last contact Never contacted
- Version 1.0.0
- IP Address None
- Executor None
- Architecture None
- Platform darwin
Configuration Runs untagged jobs
Maximum job timeout None
Token expiry
diff --git a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
index 1bbcb991619..bc28147db27 100644
--- a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
@@ -156,9 +156,7 @@ describe('AdminRunnersApp', () => {
await createComponent({ mountFn: mountExtended });
});
- // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/414975
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip('fetches counts', () => {
+ it('fetches counts', () => {
expect(mockRunnersCountHandler).toHaveBeenCalledTimes(COUNT_QUERIES);
});
diff --git a/spec/frontend/ci/runner/components/runner_details_spec.js b/spec/frontend/ci/runner/components/runner_details_spec.js
index cc91340655b..9d5f89a2642 100644
--- a/spec/frontend/ci/runner/components/runner_details_spec.js
+++ b/spec/frontend/ci/runner/components/runner_details_spec.js
@@ -49,13 +49,6 @@ describe('RunnerDetails', () => {
${'Description'} | ${{ description: null }} | ${'None'}
${'Last contact'} | ${{ contactedAt: mockOneHourAgo }} | ${'1 hour ago'}
${'Last contact'} | ${{ contactedAt: null }} | ${'Never contacted'}
- ${'Version'} | ${{ version: '12.3' }} | ${'12.3'}
- ${'Version'} | ${{ version: null }} | ${'None'}
- ${'Executor'} | ${{ executorName: 'shell' }} | ${'shell'}
- ${'Architecture'} | ${{ architectureName: 'amd64' }} | ${'amd64'}
- ${'Platform'} | ${{ platformName: 'darwin' }} | ${'darwin'}
- ${'IP Address'} | ${{ ipAddress: '127.0.0.1' }} | ${'127.0.0.1'}
- ${'IP Address'} | ${{ ipAddress: null }} | ${'None'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: true }} | ${'Protected, Runs untagged jobs'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_REF_PROTECTED, runUntagged: false }} | ${'Protected'}
${'Configuration'} | ${{ accessLevel: ACCESS_LEVEL_NOT_PROTECTED, runUntagged: true }} | ${'Runs untagged jobs'}
diff --git a/spec/frontend/ci/runner/components/runner_details_tabs_spec.js b/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
index 689d0575726..516209794ad 100644
--- a/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
+++ b/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
@@ -54,7 +54,7 @@ describe('RunnerDetailsTabs', () => {
...options,
});
- routerPush = jest.spyOn(wrapper.vm.$router, 'push').mockImplementation(() => {});
+ routerPush = jest.spyOn(wrapper.vm.$router, 'push');
return waitForPromises();
};
@@ -67,9 +67,8 @@ describe('RunnerDetailsTabs', () => {
});
it('shows runner jobs', async () => {
- setWindowLocation(`#${JOBS_ROUTE_PATH}`);
-
- await createComponent({ mountFn: mountExtended });
+ createComponent({ mountFn: mountExtended });
+ await wrapper.vm.$router.push({ path: JOBS_ROUTE_PATH });
expect(findRunnerDetails().exists()).toBe(false);
expect(findRunnerJobs().props('runner')).toBe(mockRunner);
@@ -101,10 +100,9 @@ describe('RunnerDetailsTabs', () => {
}
});
- it.each(['#/', '#/unknown-tab'])('shows details when location hash is `%s`', async (hash) => {
- setWindowLocation(hash);
-
- await createComponent({ mountFn: mountExtended });
+ it.each(['#/', '#/unknown-tab'])('shows details when location hash is `%s`', async (path) => {
+ createComponent({ mountFn: mountExtended });
+ await wrapper.vm.$router.push({ path });
expect(findTabs().props('value')).toBe(0);
expect(findRunnerDetails().exists()).toBe(true);
diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index 9da640afeb7..7c00aa48d31 100644
--- a/spec/frontend/ci/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -1,14 +1,11 @@
import { GlTableLite, GlSkeletonLoader } from '@gitlab/ui';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
-import {
- extendedWrapper,
- shallowMountExtended,
- mountExtended,
-} from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { s__ } from '~/locale';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import { createLocalState } from '~/ci/runner/graphql/list/local_state';
+import { stubComponent } from 'helpers/stub_component';
import RunnerList from '~/ci/runner/components/runner_list.vue';
import RunnerBulkDelete from '~/ci/runner/components/runner_bulk_delete.vue';
@@ -29,14 +26,11 @@ describe('RunnerList', () => {
const findHeaders = () => wrapper.findAll('th');
const findRows = () => wrapper.findAll('[data-testid^="runner-row-"]');
const findCell = ({ row = 0, fieldKey }) =>
- extendedWrapper(findRows().at(row).find(`[data-testid="td-${fieldKey}"]`));
+ findRows().at(row).find(`[data-testid="td-${fieldKey}"]`);
const findRunnerBulkDelete = () => wrapper.findComponent(RunnerBulkDelete);
const findRunnerBulkDeleteCheckbox = () => wrapper.findComponent(RunnerBulkDeleteCheckbox);
- const createComponent = (
- { props = {}, provide = {}, ...options } = {},
- mountFn = shallowMountExtended,
- ) => {
+ const createComponent = ({ props = {}, ...options } = {}, mountFn = shallowMountExtended) => {
({ cacheConfig, localMutations } = createLocalState());
wrapper = mountFn(RunnerList, {
@@ -49,7 +43,6 @@ describe('RunnerList', () => {
localMutations,
onlineContactTimeoutSecs,
staleTimeoutSecs,
- ...provide,
},
...options,
});
@@ -81,7 +74,11 @@ describe('RunnerList', () => {
});
it('Sets runner id as a row key', () => {
- createComponent();
+ createComponent({
+ stubs: {
+ GlTableLite: stubComponent(GlTableLite),
+ },
+ });
expect(findTable().attributes('primary-key')).toBe('id');
});
@@ -220,7 +217,12 @@ describe('RunnerList', () => {
describe('When data is loading', () => {
it('shows a busy state', () => {
- createComponent({ props: { runners: [], loading: true } });
+ createComponent({
+ props: { runners: [], loading: true },
+ stubs: {
+ GlTableLite: stubComponent(GlTableLite),
+ },
+ });
expect(findTable().classes('gl-opacity-6')).toBe(true);
});
diff --git a/spec/frontend/ci/runner/components/runner_type_icon_spec.js b/spec/frontend/ci/runner/components/runner_type_icon_spec.js
new file mode 100644
index 00000000000..01f3de10aa6
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_type_icon_spec.js
@@ -0,0 +1,67 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import RunnerTypeIcon from '~/ci/runner/components/runner_type_icon.vue';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { assertProps } from 'helpers/assert_props';
+import {
+ INSTANCE_TYPE,
+ GROUP_TYPE,
+ PROJECT_TYPE,
+ I18N_INSTANCE_TYPE,
+ I18N_GROUP_TYPE,
+ I18N_PROJECT_TYPE,
+} from '~/ci/runner/constants';
+
+describe('RunnerTypeIcon', () => {
+ let wrapper;
+
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const getTooltip = () => getBinding(findIcon().element, 'gl-tooltip');
+
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMount(RunnerTypeIcon, {
+ propsData: {
+ ...props,
+ },
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ });
+ };
+
+ describe.each`
+ type | tooltipText
+ ${INSTANCE_TYPE} | ${I18N_INSTANCE_TYPE}
+ ${GROUP_TYPE} | ${I18N_GROUP_TYPE}
+ ${PROJECT_TYPE} | ${I18N_PROJECT_TYPE}
+ `('displays $type runner', ({ type, tooltipText }) => {
+ beforeEach(() => {
+ createComponent({ props: { type } });
+ });
+
+ it(`with no text`, () => {
+ expect(findIcon().text()).toBe('');
+ });
+
+ it(`with aria-label`, () => {
+ expect(findIcon().props('ariaLabel')).toBeDefined();
+ });
+
+ it('with a tooltip', () => {
+ expect(getTooltip().value).toBeDefined();
+ expect(getTooltip().value).toContain(tooltipText);
+ });
+ });
+
+ it('validation fails for an incorrect type', () => {
+ expect(() => {
+ assertProps(RunnerTypeIcon, { type: 'AN_UNKNOWN_VALUE' });
+ }).toThrow();
+ });
+
+ it('does not render content when type is missing', () => {
+ createComponent({ props: { type: undefined } });
+
+ expect(findIcon().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
index 7438c47e32c..8258bd1d507 100644
--- a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
@@ -108,11 +108,6 @@ describe('GroupRunnerShowApp', () => {
it('shows basic runner details', () => {
const expected = `Description My Runner
Last contact Never contacted
- Version 1.0.0
- IP Address None
- Executor None
- Architecture None
- Platform darwin
Configuration Runs untagged jobs
Maximum job timeout None
Token expiry
diff --git a/spec/frontend/ci/runner/sentry_utils_spec.js b/spec/frontend/ci/runner/sentry_utils_spec.js
index 2f17cc43ac5..59d386a5899 100644
--- a/spec/frontend/ci/runner/sentry_utils_spec.js
+++ b/spec/frontend/ci/runner/sentry_utils_spec.js
@@ -4,24 +4,12 @@ import { captureException } from '~/ci/runner/sentry_utils';
jest.mock('@sentry/browser');
describe('~/ci/runner/sentry_utils', () => {
- let mockSetTag;
-
- beforeEach(() => {
- mockSetTag = jest.fn();
-
- Sentry.withScope.mockImplementation((fn) => {
- const scope = { setTag: mockSetTag };
- fn(scope);
- });
- });
-
describe('captureException', () => {
const mockError = new Error('Something went wrong!');
it('error is reported to sentry', () => {
captureException({ error: mockError });
- expect(Sentry.withScope).toHaveBeenCalled();
expect(Sentry.captureException).toHaveBeenCalledWith(mockError);
});
@@ -30,10 +18,11 @@ describe('~/ci/runner/sentry_utils', () => {
captureException({ error: mockError, component: mockComponentName });
- expect(Sentry.withScope).toHaveBeenCalled();
- expect(Sentry.captureException).toHaveBeenCalledWith(mockError);
-
- expect(mockSetTag).toHaveBeenCalledWith('vue_component', mockComponentName);
+ expect(Sentry.captureException).toHaveBeenCalledWith(mockError, {
+ tags: {
+ vue_component: mockComponentName,
+ },
+ });
});
});
});
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index 207bfddcb4f..d4474b1c643 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -8,8 +8,15 @@ import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.
import ClusterStore from '~/clusters_list/store';
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import {
+ SET_LOADING_NODES,
+ SET_CLUSTERS_DATA,
+ SET_LOADING_CLUSTERS,
+} from '~/clusters_list/store/mutation_types';
import { apiData } from '../mock_data';
+jest.mock('@sentry/browser');
+
describe('Clusters', () => {
let mock;
let store;
@@ -59,15 +66,7 @@ describe('Clusters', () => {
};
};
- let captureException;
-
beforeEach(() => {
- jest.spyOn(Sentry, 'withScope').mockImplementation((fn) => {
- const mockScope = { setTag: () => {} };
- fn(mockScope);
- });
- captureException = jest.spyOn(Sentry, 'captureException');
-
mock = new MockAdapter(axios);
mockPollingApi(HTTP_STATUS_OK, apiData, paginationHeader());
@@ -76,13 +75,12 @@ describe('Clusters', () => {
afterEach(() => {
mock.restore();
- captureException.mockRestore();
});
describe('clusters table', () => {
describe('when data is loading', () => {
beforeEach(() => {
- wrapper.vm.$store.state.loadingClusters = true;
+ store.commit(SET_LOADING_CLUSTERS, true);
});
it('displays a loader instead of the table while loading', () => {
@@ -99,7 +97,12 @@ describe('Clusters', () => {
describe('when there are no clusters', () => {
beforeEach(() => {
- wrapper.vm.$store.state.totalClusters = 0;
+ store.commit(SET_CLUSTERS_DATA, {
+ data: {},
+ paginationInformation: {
+ total: 0,
+ },
+ });
});
it('should render empty state', () => {
expect(findEmptyState().exists()).toBe(true);
@@ -175,7 +178,7 @@ describe('Clusters', () => {
describe('nodes finish loading', () => {
beforeEach(async () => {
- wrapper.vm.$store.state.loadingNodes = false;
+ store.commit(SET_LOADING_NODES, false);
await nextTick();
});
@@ -198,19 +201,23 @@ describe('Clusters', () => {
describe('nodes with unknown quantity', () => {
it('notifies Sentry about all missing quantity types', () => {
- expect(captureException).toHaveBeenCalledTimes(8);
+ expect(Sentry.captureException).toHaveBeenCalledTimes(8);
});
it('notifies Sentry about CPU missing quantity types', () => {
const missingCpuTypeError = new Error('UnknownK8sCpuQuantity:1missingCpuUnit');
- expect(captureException).toHaveBeenCalledWith(missingCpuTypeError);
+ expect(Sentry.captureException).toHaveBeenCalledWith(missingCpuTypeError, {
+ tags: { javascript_clusters_list: 'totalCpuAndUsageError' },
+ });
});
it('notifies Sentry about Memory missing quantity types', () => {
const missingMemoryTypeError = new Error('UnknownK8sMemoryQuantity:1missingMemoryUnit');
- expect(captureException).toHaveBeenCalledWith(missingMemoryTypeError);
+ expect(Sentry.captureException).toHaveBeenCalledWith(missingMemoryTypeError, {
+ tags: { javascript_clusters_list: 'totalMemoryAndUsageError' },
+ });
});
});
});
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index 6d23db0517d..9e6da595a75 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -18,10 +18,6 @@ describe('Clusters store actions', () => {
describe('reportSentryError', () => {
beforeEach(() => {
- jest.spyOn(Sentry, 'withScope').mockImplementation((fn) => {
- const mockScope = { setTag: () => {} };
- fn(mockScope);
- });
captureException = jest.spyOn(Sentry, 'captureException');
});
@@ -34,7 +30,11 @@ describe('Clusters store actions', () => {
const tag = 'sentryErrorTag';
await testAction(actions.reportSentryError, { error: sentryError, tag }, {}, [], []);
- expect(captureException).toHaveBeenCalledWith(sentryError);
+ expect(captureException).toHaveBeenCalledWith(sentryError, {
+ tags: {
+ javascript_clusters_list: tag,
+ },
+ });
});
});
diff --git a/spec/frontend/commit/commit_pipeline_status_spec.js b/spec/frontend/commit/commit_pipeline_status_spec.js
index 73031724b12..08a7ec17785 100644
--- a/spec/frontend/commit/commit_pipeline_status_spec.js
+++ b/spec/frontend/commit/commit_pipeline_status_spec.js
@@ -137,7 +137,7 @@ describe('Commit pipeline status component', () => {
});
it('renders CI icon with the correct title and status', () => {
- expect(findCiIcon().attributes('title')).toEqual('Pipeline: passed');
+ expect(findCiIcon().attributes('title')).toEqual('Pipeline: Passed');
expect(findCiIcon().props('status')).toEqual(mockCiStatus);
});
});
diff --git a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
index 80b75a0a65e..844a2d81832 100644
--- a/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
+++ b/spec/frontend/commit/components/commit_box_pipeline_status_spec.js
@@ -1,11 +1,11 @@
-import { GlLoadingIcon, GlLink } from '@gitlab/ui';
+import { GlLoadingIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
-import CiIcon from '~/vue_shared/components/ci_icon.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import CommitBoxPipelineStatus from '~/projects/commit_box/info/components/commit_box_pipeline_status.vue';
import {
COMMIT_BOX_POLL_INTERVAL,
@@ -32,8 +32,7 @@ describe('Commit box pipeline status', () => {
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findStatusIcon = () => wrapper.findComponent(CiIcon);
- const findPipelineLink = () => wrapper.findComponent(GlLink);
+ const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
const advanceToNextFetch = () => {
jest.advanceTimersByTime(COMMIT_BOX_POLL_INTERVAL);
@@ -50,6 +49,9 @@ describe('Commit box pipeline status', () => {
provide: {
...mockProvide,
},
+ stubs: {
+ CiBadgeLink,
+ },
apolloProvider: createMockApolloProvider(handler),
});
};
@@ -59,7 +61,7 @@ describe('Commit box pipeline status', () => {
createComponent();
expect(findLoadingIcon().exists()).toBe(true);
- expect(findStatusIcon().exists()).toBe(false);
+ expect(findCiBadgeLink().exists()).toBe(false);
});
});
@@ -71,7 +73,7 @@ describe('Commit box pipeline status', () => {
});
it('should display pipeline status after the query is resolved successfully', () => {
- expect(findStatusIcon().exists()).toBe(true);
+ expect(findCiBadgeLink().exists()).toBe(true);
expect(findLoadingIcon().exists()).toBe(false);
expect(createAlert).toHaveBeenCalledTimes(0);
@@ -88,7 +90,7 @@ describe('Commit box pipeline status', () => {
},
} = mockPipelineStatusResponse;
- expect(findPipelineLink().attributes('href')).toBe(detailsPath);
+ expect(findCiBadgeLink().attributes('href')).toBe(detailsPath);
});
});
diff --git a/spec/frontend/commit/pipelines/legacy_pipelines_table_wrapper_spec.js b/spec/frontend/commit/pipelines/legacy_pipelines_table_wrapper_spec.js
index 4af292e3588..d58b139dae3 100644
--- a/spec/frontend/commit/pipelines/legacy_pipelines_table_wrapper_spec.js
+++ b/spec/frontend/commit/pipelines/legacy_pipelines_table_wrapper_spec.js
@@ -1,13 +1,13 @@
import { GlLoadingIcon, GlModal, GlTableLite } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import fixture from 'test_fixtures/pipelines/pipelines.json';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
-import LegacyPipelinesTableWraper from '~/commit/pipelines/legacy_pipelines_table_wrapper.vue';
+import LegacyPipelinesTableWrapper from '~/commit/pipelines/legacy_pipelines_table_wrapper.vue';
+import PipelinesTable from '~/ci/common/pipelines_table.vue';
import {
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_INTERNAL_SERVER_ERROR,
@@ -39,27 +39,26 @@ describe('Pipelines table in Commits and Merge requests', () => {
const findTableRows = () => wrapper.findAllByTestId('pipeline-table-row');
const findModal = () => wrapper.findComponent(GlModal);
const findMrPipelinesDocsLink = () => wrapper.findByTestId('mr-pipelines-docs-link');
-
- const createComponent = ({ props = {} } = {}) => {
- wrapper = extendedWrapper(
- mount(LegacyPipelinesTableWraper, {
- propsData: {
- endpoint: 'endpoint.json',
- emptyStateSvgPath: 'foo',
- errorStateSvgPath: 'foo',
- ...props,
- },
- mocks: {
- $toast,
- },
- stubs: {
- GlModal: stubComponent(GlModal, {
- template: '<div />',
- methods: { show: showMock },
- }),
- },
- }),
- );
+ const findPipelinesTable = () => wrapper.findComponent(PipelinesTable);
+
+ const createComponent = ({ props = {}, mountFn = mountExtended } = {}) => {
+ wrapper = mountFn(LegacyPipelinesTableWrapper, {
+ propsData: {
+ endpoint: 'endpoint.json',
+ emptyStateSvgPath: 'foo',
+ errorStateSvgPath: 'foo',
+ ...props,
+ },
+ mocks: {
+ $toast,
+ },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template: '<div />',
+ methods: { show: showMock },
+ }),
+ },
+ });
};
beforeEach(() => {
@@ -116,7 +115,6 @@ describe('Pipelines table in Commits and Merge requests', () => {
it('should make an API request when using pagination', async () => {
expect(mock.history.get).toHaveLength(1);
- expect(mock.history.get[0].params.page).toBe('1');
wrapper.find('.next-page-item').trigger('click');
@@ -359,4 +357,53 @@ describe('Pipelines table in Commits and Merge requests', () => {
);
});
});
+
+ describe('events', () => {
+ beforeEach(async () => {
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipeline]);
+
+ createComponent({ mountFn: shallowMountExtended });
+
+ await waitForPromises();
+ });
+
+ describe('When cancelling a pipeline', () => {
+ it('sends the cancel action', async () => {
+ expect(mock.history.post).toHaveLength(0);
+
+ findPipelinesTable().vm.$emit('cancel-pipeline', pipeline);
+
+ await waitForPromises();
+
+ expect(mock.history.post).toHaveLength(1);
+ expect(mock.history.post[0].url).toContain('cancel.json');
+ });
+ });
+
+ describe('When retrying a pipeline', () => {
+ it('sends the retry action', async () => {
+ expect(mock.history.post).toHaveLength(0);
+
+ findPipelinesTable().vm.$emit('retry-pipeline', pipeline);
+
+ await waitForPromises();
+
+ expect(mock.history.post).toHaveLength(1);
+ expect(mock.history.post[0].url).toContain('retry.json');
+ });
+ });
+
+ describe('When refreshing a pipeline', () => {
+ it('calls the pipelines endpoint again', async () => {
+ expect(mock.history.get).toHaveLength(1);
+
+ findPipelinesTable().vm.$emit('refresh-pipelines-table');
+
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(2);
+ expect(mock.history.get[1].url).toContain('endpoint.json');
+ });
+ });
+ });
});
diff --git a/spec/frontend/content_editor/services/markdown_serializer_spec.js b/spec/frontend/content_editor/services/markdown_serializer_spec.js
index 3eb00f69345..548c6030ed7 100644
--- a/spec/frontend/content_editor/services/markdown_serializer_spec.js
+++ b/spec/frontend/content_editor/services/markdown_serializer_spec.js
@@ -206,6 +206,14 @@ describe('markdownSerializer', () => {
);
});
+ it('correctly serializes a malformed URL-encoded link', () => {
+ expect(
+ serialize(
+ paragraph(link({ href: 'https://example.com/%E0%A4%A' }, 'https://example.com/%E0%A4%A')),
+ ),
+ ).toBe('https://example.com/%E0%A4%A');
+ });
+
it('correctly serializes a link with a title', () => {
expect(
serialize(
diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js
index f915b834aff..7d863a8eb78 100644
--- a/spec/frontend/contributors/component/contributors_spec.js
+++ b/spec/frontend/contributors/component/contributors_spec.js
@@ -8,6 +8,7 @@ import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import RefSelector from '~/ref/components/ref_selector.vue';
import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
+import { SET_CHART_DATA, SET_LOADING_STATE } from '~/contributors/stores/mutation_types';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
@@ -66,14 +67,14 @@ describe('Contributors charts', () => {
});
it('should display loader whiled loading data', async () => {
- wrapper.vm.$store.state.loading = true;
+ store.commit(SET_LOADING_STATE, true);
await nextTick();
expect(findLoadingIcon().exists()).toBe(true);
});
it('should render charts and a RefSelector when loading completed and there is chart data', async () => {
- wrapper.vm.$store.state.loading = false;
- wrapper.vm.$store.state.chartData = chartData;
+ store.commit(SET_LOADING_STATE, false);
+ store.commit(SET_CHART_DATA, chartData);
await nextTick();
expect(findLoadingIcon().exists()).toBe(false);
@@ -92,8 +93,8 @@ describe('Contributors charts', () => {
});
it('should have a history button with a set href attribute', async () => {
- wrapper.vm.$store.state.loading = false;
- wrapper.vm.$store.state.chartData = chartData;
+ store.commit(SET_LOADING_STATE, false);
+ store.commit(SET_CHART_DATA, chartData);
await nextTick();
const historyButton = findHistoryButton();
@@ -102,8 +103,8 @@ describe('Contributors charts', () => {
});
it('visits a URL when clicking on a branch/tag', async () => {
- wrapper.vm.$store.state.loading = false;
- wrapper.vm.$store.state.chartData = chartData;
+ store.commit(SET_LOADING_STATE, false);
+ store.commit(SET_CHART_DATA, chartData);
await nextTick();
findRefSelector().vm.$emit('input', branch);
diff --git a/spec/frontend/crm/crm_form_spec.js b/spec/frontend/crm/crm_form_spec.js
index fabf43ceb9d..083b49b7c30 100644
--- a/spec/frontend/crm/crm_form_spec.js
+++ b/spec/frontend/crm/crm_form_spec.js
@@ -10,7 +10,7 @@ import routes from '~/crm/contacts/routes';
import createContactMutation from '~/crm/contacts/components/graphql/create_contact.mutation.graphql';
import updateContactMutation from '~/crm/contacts/components/graphql/update_contact.mutation.graphql';
import getGroupContactsQuery from '~/crm/contacts/components/graphql/get_group_contacts.query.graphql';
-import createOrganizationMutation from '~/crm/organizations/components/graphql/create_organization.mutation.graphql';
+import createOrganizationMutation from '~/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql';
import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql';
import {
createContactMutationErrorResponse,
diff --git a/spec/frontend/crm/organization_form_wrapper_spec.js b/spec/frontend/crm/organization_form_wrapper_spec.js
index 8408c1920a9..f15fcac71d5 100644
--- a/spec/frontend/crm/organization_form_wrapper_spec.js
+++ b/spec/frontend/crm/organization_form_wrapper_spec.js
@@ -2,7 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import OrganizationFormWrapper from '~/crm/organizations/components/organization_form_wrapper.vue';
import CrmForm from '~/crm/components/crm_form.vue';
import getGroupOrganizationsQuery from '~/crm/organizations/components/graphql/get_group_organizations.query.graphql';
-import createOrganizationMutation from '~/crm/organizations/components/graphql/create_organization.mutation.graphql';
+import createOrganizationMutation from '~/crm/organizations/components/graphql/create_customer_relations_organization.mutation.graphql';
import updateOrganizationMutation from '~/crm/organizations/components/graphql/update_organization.mutation.graphql';
describe('Customer relations organization form wrapper', () => {
diff --git a/spec/frontend/design_management/components/design_description/description_form_spec.js b/spec/frontend/design_management/components/design_description/description_form_spec.js
index f7feff98da3..7d68a3b80d5 100644
--- a/spec/frontend/design_management/components/design_description/description_form_spec.js
+++ b/spec/frontend/design_management/components/design_description/description_form_spec.js
@@ -42,7 +42,6 @@ describe('Design description form', () => {
showEditor = false,
isSubmitting = false,
designVariables = mockDesignVariables,
- contentEditorOnIssues = false,
designUpdateMutationHandler = mockDesignUpdateMutationHandler,
} = {}) => {
mockApollo = createMockApollo([[updateDesignDescriptionMutation, designUpdateMutationHandler]]);
@@ -52,11 +51,6 @@ describe('Design description form', () => {
markdownPreviewPath: '/gitlab-org/gitlab-test/preview_markdown?target_type=Issue',
designVariables,
},
- provide: {
- glFeatures: {
- contentEditorOnIssues,
- },
- },
apolloProvider: mockApollo,
data() {
return {
@@ -131,7 +125,7 @@ describe('Design description form', () => {
expect(findMarkdownEditor().props()).toMatchObject({
value: 'Test description',
renderMarkdownPath: '/gitlab-org/gitlab-test/preview_markdown?target_type=Issue',
- enableContentEditor: false,
+ enableContentEditor: true,
formFieldProps,
autofocus: true,
enableAutocomplete: true,
diff --git a/spec/frontend/design_management/pages/index_spec.js b/spec/frontend/design_management/pages/index_spec.js
index 961ea27f0f4..9b5e812c021 100644
--- a/spec/frontend/design_management/pages/index_spec.js
+++ b/spec/frontend/design_management/pages/index_spec.js
@@ -191,7 +191,7 @@ describe('Design management index page', () => {
[moveDesignMutation, moveDesignHandler],
];
- fakeApollo = createMockApollo(requestHandlers, {}, { addTypename: true });
+ fakeApollo = createMockApollo(requestHandlers, {});
wrapper = shallowMountExtended(Index, {
apolloProvider: fakeApollo,
router,
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index e10aad6214c..212def72b90 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -6,6 +6,7 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'spec/test_constants';
+
import App from '~/diffs/components/app.vue';
import CommitWidget from '~/diffs/components/commit_widget.vue';
import CompareVersions from '~/diffs/components/compare_versions.vue';
@@ -17,6 +18,8 @@ import DiffsFileTree from '~/diffs/components/diffs_file_tree.vue';
import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vue';
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
+import eventHub from '~/diffs/event_hub';
+
import axios from '~/lib/utils/axios_utils';
import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { Mousetrap } from '~/lib/mousetrap';
@@ -760,4 +763,29 @@ describe('diffs/components/app', () => {
);
});
});
+
+ describe('autoscroll', () => {
+ let loadSpy;
+
+ beforeEach(() => {
+ createComponent();
+ loadSpy = jest.spyOn(wrapper.vm, 'loadCollapsedDiff').mockResolvedValue('resolved');
+ });
+
+ it('does nothing if the location hash does not include a file hash', () => {
+ window.location.hash = 'not_a_file_hash';
+
+ eventHub.$emit('doneLoadingBatches');
+
+ expect(loadSpy).not.toHaveBeenCalled();
+ });
+
+ it('requests that the correct file be loaded', () => {
+ window.location.hash = '1c497fbb3a46b78edf04cc2a2fa33f67e3ffbe2a_0_1';
+
+ eventHub.$emit('doneLoadingBatches');
+
+ expect(loadSpy).toHaveBeenCalledWith({ file: store.state.diffs.diffFiles[0] });
+ });
+ });
});
diff --git a/spec/frontend/diffs/components/diff_file_header_spec.js b/spec/frontend/diffs/components/diff_file_header_spec.js
index b089825090b..b0d98e0e4a6 100644
--- a/spec/frontend/diffs/components/diff_file_header_spec.js
+++ b/spec/frontend/diffs/components/diff_file_header_spec.js
@@ -8,8 +8,12 @@ import { mockTracking, triggerEvent } from 'helpers/tracking_helper';
import DiffFileHeader from '~/diffs/components/diff_file_header.vue';
import { DIFF_FILE_AUTOMATIC_COLLAPSE, DIFF_FILE_MANUAL_COLLAPSE } from '~/diffs/constants';
-import { reviewFile } from '~/diffs/store/actions';
-import { SET_DIFF_FILE_VIEWED, SET_MR_FILE_REVIEWS } from '~/diffs/store/mutation_types';
+import { reviewFile, setFileForcedOpen } from '~/diffs/store/actions';
+import {
+ SET_DIFF_FILE_VIEWED,
+ SET_MR_FILE_REVIEWS,
+ SET_FILE_FORCED_OPEN,
+} from '~/diffs/store/mutation_types';
import { diffViewerModes } from '~/ide/constants';
import { scrollToElement } from '~/lib/utils/common_utils';
import { truncateSha } from '~/lib/utils/text_utility';
@@ -67,6 +71,7 @@ describe('DiffFileHeader component', () => {
toggleFullDiff: jest.fn(),
setCurrentFileHash: jest.fn(),
setFileCollapsedByUser: jest.fn(),
+ setFileForcedOpen: jest.fn(),
reviewFile: jest.fn(),
},
},
@@ -138,6 +143,19 @@ describe('DiffFileHeader component', () => {
expect(wrapper.emitted().toggleFile).toBeDefined();
});
+ it('when header is clicked it triggers the action that removes the value that forces a file to be uncollapsed', () => {
+ createComponent();
+ findHeader().trigger('click');
+
+ return testAction(
+ setFileForcedOpen,
+ { filePath: diffFile.file_path, forced: false },
+ {},
+ [{ type: SET_FILE_FORCED_OPEN, payload: { filePath: diffFile.file_path, forced: false } }],
+ [],
+ );
+ });
+
it('when collapseIcon is clicked emits toggleFile', async () => {
createComponent({ props: { collapsible: true } });
findCollapseButton().vm.$emit('click', new Event('click'));
@@ -643,6 +661,44 @@ describe('DiffFileHeader component', () => {
expect(Boolean(wrapper.emitted().toggleFile)).toBe(fires);
},
);
+
+ it('removes the property that forces a file to be shown when the file review is toggled', () => {
+ createComponent({
+ props: {
+ diffFile: {
+ ...diffFile,
+ viewer: {
+ ...diffFile.viewer,
+ automaticallyCollapsed: false,
+ manuallyCollapsed: null,
+ },
+ },
+ showLocalFileReviews: true,
+ addMergeRequestButtons: true,
+ expanded: false,
+ },
+ });
+
+ findReviewFileCheckbox().vm.$emit('change', true);
+
+ testAction(
+ setFileForcedOpen,
+ { filePath: diffFile.file_path, forced: false },
+ {},
+ [{ type: SET_FILE_FORCED_OPEN, payload: { filePath: diffFile.file_path, forced: false } }],
+ [],
+ );
+
+ findReviewFileCheckbox().vm.$emit('change', false);
+
+ testAction(
+ setFileForcedOpen,
+ { filePath: diffFile.file_path, forced: false },
+ {},
+ [{ type: SET_FILE_FORCED_OPEN, payload: { filePath: diffFile.file_path, forced: false } }],
+ [],
+ );
+ });
});
it('should render the comment on files button', () => {
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index 53f135471b7..13efd3584b4 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -324,6 +324,22 @@ describe('DiffFile', () => {
});
describe('collapsing', () => {
+ describe('forced open', () => {
+ it('should have content even when it is automatically collapsed', () => {
+ makeFileAutomaticallyCollapsed(store);
+
+ expect(findDiffContentArea(wrapper).element.children.length).toBe(1);
+ expect(wrapper.classes('has-body')).toBe(true);
+ });
+
+ it('should have content even when it is manually collapsed', () => {
+ makeFileManuallyCollapsed(store);
+
+ expect(findDiffContentArea(wrapper).element.children.length).toBe(1);
+ expect(wrapper.classes('has-body')).toBe(true);
+ });
+ });
+
describe(`\`${EVT_EXPAND_ALL_FILES}\` event`, () => {
beforeEach(() => {
jest.spyOn(wrapper.vm, 'handleToggle').mockImplementation(() => {});
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 387407a7e4d..18e81232b5c 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -1627,6 +1627,7 @@ describe('DiffsStoreActions', () => {
name: updatedViewerName,
automaticallyCollapsed: false,
manuallyCollapsed: false,
+ forceOpen: false,
};
const testData = [{ rich_text: 'test' }, { rich_text: 'file2' }];
let renamedFile;
@@ -1673,7 +1674,7 @@ describe('DiffsStoreActions', () => {
});
});
- describe('setFileUserCollapsed', () => {
+ describe('setFileCollapsedByUser', () => {
it('commits SET_FILE_COLLAPSED', () => {
return testAction(
diffActions.setFileCollapsedByUser,
@@ -1690,6 +1691,17 @@ describe('DiffsStoreActions', () => {
});
});
+ describe('setFileForcedOpen', () => {
+ it('commits SET_FILE_FORCED_OPEN', () => {
+ return testAction(diffActions.setFileForcedOpen, { filePath: 'test', forced: true }, null, [
+ {
+ type: types.SET_FILE_FORCED_OPEN,
+ payload: { filePath: 'test', forced: true },
+ },
+ ]);
+ });
+ });
+
describe('setExpandedDiffLines', () => {
beforeEach(() => {
utils.idleCallback.mockImplementation((cb) => {
diff --git a/spec/frontend/diffs/store/mutations_spec.js b/spec/frontend/diffs/store/mutations_spec.js
index e87c5d0a9b1..fdcf7c3eeab 100644
--- a/spec/frontend/diffs/store/mutations_spec.js
+++ b/spec/frontend/diffs/store/mutations_spec.js
@@ -1055,4 +1055,14 @@ describe('DiffsStoreMutations', () => {
expect(state.diffFiles[0].drafts[0]).toEqual('test');
});
});
+
+ describe('SET_FILE_FORCED_OPEN', () => {
+ it('sets the forceOpen property of a diff file viewer correctly', () => {
+ const state = { diffFiles: [{ file_path: 'abc', viewer: { forceOpen: 'not-a-boolean' } }] };
+
+ mutations[types.SET_FILE_FORCED_OPEN](state, { filePath: 'abc', force: true });
+
+ expect(state.diffFiles[0].viewer.forceOpen).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/diffs/store/utils_spec.js b/spec/frontend/diffs/store/utils_spec.js
index 24cb8158739..720b72f4965 100644
--- a/spec/frontend/diffs/store/utils_spec.js
+++ b/spec/frontend/diffs/store/utils_spec.js
@@ -927,19 +927,21 @@ describe('DiffsStoreUtils', () => {
describe('parseUrlHashAsFileHash', () => {
it.each`
- input | currentDiffId | resultId
- ${'#note_12345'} | ${'1A2B3C'} | ${'1A2B3C'}
- ${'note_12345'} | ${'1A2B3C'} | ${'1A2B3C'}
- ${'#note_12345'} | ${undefined} | ${null}
- ${'note_12345'} | ${undefined} | ${null}
- ${'#diff-content-12345'} | ${undefined} | ${'12345'}
- ${'diff-content-12345'} | ${undefined} | ${'12345'}
- ${'#diff-content-12345'} | ${'98765'} | ${'12345'}
- ${'diff-content-12345'} | ${'98765'} | ${'12345'}
- ${'#e334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
- ${'e334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
- ${'#Z334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${null}
- ${'Z334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${null}
+ input | currentDiffId | resultId
+ ${'#note_12345'} | ${'1A2B3C'} | ${'1A2B3C'}
+ ${'note_12345'} | ${'1A2B3C'} | ${'1A2B3C'}
+ ${'#note_12345'} | ${undefined} | ${null}
+ ${'note_12345'} | ${undefined} | ${null}
+ ${'#diff-content-12345'} | ${undefined} | ${'12345'}
+ ${'diff-content-12345'} | ${undefined} | ${'12345'}
+ ${'#diff-content-12345'} | ${'98765'} | ${'12345'}
+ ${'diff-content-12345'} | ${'98765'} | ${'12345'}
+ ${'#e334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
+ ${'e334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
+ ${'#Z334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${null}
+ ${'Z334a2a10f036c00151a04cea7938a5d4213a818'} | ${undefined} | ${null}
+ ${'#e334a2a10f036c00151a04cea7938a5d4213a818_0_42'} | ${undefined} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
+ ${'e334a2a10f036c00151a04cea7938a5d4213a818_0_42'} | ${undefined} | ${'e334a2a10f036c00151a04cea7938a5d4213a818'}
`('returns $resultId for $input and $currentDiffId', ({ input, currentDiffId, resultId }) => {
expect(utils.parseUrlHashAsFileHash(input, currentDiffId)).toBe(resultId);
});
diff --git a/spec/frontend/diffs/utils/merge_request_spec.js b/spec/frontend/diffs/utils/merge_request_spec.js
index 11c0efb9a9c..f5145b3c4c7 100644
--- a/spec/frontend/diffs/utils/merge_request_spec.js
+++ b/spec/frontend/diffs/utils/merge_request_spec.js
@@ -1,6 +1,7 @@
import {
updateChangesTabCount,
getDerivedMergeRequestInformation,
+ extractFileHash,
} from '~/diffs/utils/merge_request';
import { ZERO_CHANGES_ALT_DISPLAY } from '~/diffs/constants';
import { diffMetadata } from '../mock_data/diff_metadata';
@@ -128,4 +129,19 @@ describe('Merge Request utilities', () => {
});
});
});
+
+ describe('extractFileHash', () => {
+ const sha1Like = 'abcdef1234567890abcdef1234567890abcdef12';
+ const sha1LikeToo = 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+
+ it('returns undefined when a SHA1-like string cannot be found in the input', () => {
+ expect(extractFileHash({ input: 'something' })).toBe(undefined);
+ });
+
+ it('returns the first matching string of SHA1-like characters in the input', () => {
+ const fullString = `#${sha1Like}_34_42--${sha1LikeToo}`;
+
+ expect(extractFileHash({ input: fullString })).toBe(sha1Like);
+ });
+ });
});
diff --git a/spec/frontend/diffs/utils/sort_errors_by_file_spec.js b/spec/frontend/diffs/utils/sort_errors_by_file_spec.js
new file mode 100644
index 00000000000..ca8a8ec3516
--- /dev/null
+++ b/spec/frontend/diffs/utils/sort_errors_by_file_spec.js
@@ -0,0 +1,52 @@
+import { sortFindingsByFile } from '~/diffs/utils/sort_findings_by_file';
+
+describe('sort_findings_by_file utilities', () => {
+ const mockDescription = 'mockDescription';
+ const mockSeverity = 'mockseverity';
+ const mockLine = '00';
+ const mockFile1 = 'file1.js';
+ const mockFile2 = 'file2.rb';
+ const emptyResponse = {
+ files: {},
+ };
+
+ const unsortedFindings = [
+ {
+ severity: mockSeverity,
+ filePath: mockFile1,
+ line: mockLine,
+ description: mockDescription,
+ },
+ {
+ severity: mockSeverity,
+ filePath: mockFile2,
+ line: mockLine,
+ description: mockDescription,
+ },
+ ];
+ const sortedFindings = {
+ files: {
+ [mockFile1]: [
+ {
+ line: mockLine,
+ description: mockDescription,
+ severity: mockSeverity,
+ },
+ ],
+ [mockFile2]: [
+ {
+ line: mockLine,
+ description: mockDescription,
+ severity: mockSeverity,
+ },
+ ],
+ },
+ };
+
+ it('sorts Findings correctly', () => {
+ expect(sortFindingsByFile(unsortedFindings)).toEqual(sortedFindings);
+ });
+ it('does not throw error when given no input', () => {
+ expect(sortFindingsByFile()).toEqual(emptyResponse);
+ });
+});
diff --git a/spec/frontend/editor/schema/ci/ci_schema_spec.js b/spec/frontend/editor/schema/ci/ci_schema_spec.js
index 77c7f0d49a8..0f380f13679 100644
--- a/spec/frontend/editor/schema/ci/ci_schema_spec.js
+++ b/spec/frontend/editor/schema/ci/ci_schema_spec.js
@@ -36,6 +36,7 @@ import HooksYaml from './yaml_tests/positive_tests/hooks.yml';
import SecretsYaml from './yaml_tests/positive_tests/secrets.yml';
import ServicesYaml from './yaml_tests/positive_tests/services.yml';
import NeedsParallelMatrixYaml from './yaml_tests/positive_tests/needs_parallel_matrix.yml';
+import ScriptYaml from './yaml_tests/positive_tests/script.yml';
// YAML NEGATIVE TEST
import ArtifactsNegativeYaml from './yaml_tests/negative_tests/artifacts.yml';
@@ -60,6 +61,7 @@ import ServicesNegativeYaml from './yaml_tests/negative_tests/services.yml';
import NeedsParallelMatrixNumericYaml from './yaml_tests/negative_tests/needs/parallel_matrix/numeric.yml';
import NeedsParallelMatrixWrongParallelValueYaml from './yaml_tests/negative_tests/needs/parallel_matrix/wrong_parallel_value.yml';
import NeedsParallelMatrixWrongMatrixValueYaml from './yaml_tests/negative_tests/needs/parallel_matrix/wrong_matrix_value.yml';
+import ScriptNegativeYaml from './yaml_tests/negative_tests/script.yml';
const ajv = new Ajv({
strictTypes: false,
@@ -101,6 +103,7 @@ describe('positive tests', () => {
ServicesYaml,
SecretsYaml,
NeedsParallelMatrixYaml,
+ ScriptYaml,
}),
)('schema validates %s', (_, input) => {
// We construct a new "JSON" from each main key that is inside a
@@ -144,6 +147,7 @@ describe('negative tests', () => {
NeedsParallelMatrixNumericYaml,
NeedsParallelMatrixWrongParallelValueYaml,
NeedsParallelMatrixWrongMatrixValueYaml,
+ ScriptNegativeYaml,
}),
)('schema validates %s', (_, input) => {
// We construct a new "JSON" from each main key that is inside a
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/script.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/script.yml
new file mode 100644
index 00000000000..f5bf3f54f6f
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/script.yml
@@ -0,0 +1,14 @@
+script: echo "invalid global script"
+
+default:
+ before_script: 0.1
+ after_script: 1
+
+invalid_script_type:
+ script: true
+
+empty_array_script:
+ script: []
+
+empty_string_script:
+ script: ""
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/script.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/script.yml
new file mode 100644
index 00000000000..0ffb1f3e89e
--- /dev/null
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/script.yml
@@ -0,0 +1,52 @@
+default:
+ before_script:
+ - echo "default before_script"
+ after_script: |
+ echo "default after_script"
+
+valid_job_with_empty_string_script:
+ before_script: ""
+ after_script: ""
+ script:
+ - echo "overwrite default before_script and after_script"
+
+valid_job_with_empty_array_script:
+ before_script: []
+ after_script: []
+ script:
+ - echo "overwrite default before_script and after_script"
+
+valid_job_with_string_scripts:
+ before_script: echo before_script
+ script: echo script
+ after_script: echo after_script
+
+valid_job_with_multi_line_scripts:
+ before_script: |
+ echo multiline
+ echo before_script
+ script: |
+ echo multiline
+ echo script
+ after_script: |
+ echo multiline
+ echo after_script
+
+valid_job_with_array_scripts:
+ before_script:
+ - echo array
+ - echo before_script
+ script:
+ - echo array
+ - echo script
+ after_script:
+ - echo array
+ - echo after_script
+
+valid_job_with_nested_array_scripts:
+ before_script:
+ - [echo nested_array, echo before_script]
+ script:
+ - [echo nested_array, echo script]
+ after_script:
+ - [echo nested_array, echo after_script]
diff --git a/spec/frontend/editor/source_editor_spec.js b/spec/frontend/editor/source_editor_spec.js
index 6a8e7b296aa..f66de61da1e 100644
--- a/spec/frontend/editor/source_editor_spec.js
+++ b/spec/frontend/editor/source_editor_spec.js
@@ -9,21 +9,6 @@ import SourceEditor from '~/editor/source_editor';
import { DEFAULT_THEME, themes } from '~/ide/lib/themes';
import { joinPaths } from '~/lib/utils/url_utility';
-jest.mock('~/helpers/startup_css_helper', () => {
- return {
- waitForCSSLoaded: jest.fn().mockImplementation((cb) => {
- // We have to artificially put the callback's execution
- // to the end of the current call stack to be able to
- // test that the callback is called after waitForCSSLoaded.
- // setTimeout with 0 delay does exactly that.
- // Otherwise we might end up with false positive results
- setTimeout(() => {
- cb.apply();
- }, 0);
- }),
- };
-});
-
describe('Base editor', () => {
let editorEl;
let editor;
@@ -161,7 +146,7 @@ describe('Base editor', () => {
expect(instance.getModel()).toBeNull();
});
- it('resets the layout in waitForCSSLoaded callback', async () => {
+ it('resets the layout in createInstance', () => {
const layoutSpy = jest.fn();
jest.spyOn(monacoEditor, 'create').mockReturnValue({
layout: layoutSpy,
@@ -170,10 +155,6 @@ describe('Base editor', () => {
dispose: jest.fn(),
});
editor.createInstance(defaultArguments);
- expect(layoutSpy).not.toHaveBeenCalled();
-
- // We're waiting for the waitForCSSLoaded mock to kick in
- await jest.runOnlyPendingTimers();
expect(layoutSpy).toHaveBeenCalled();
});
diff --git a/spec/frontend/environments/canary_ingress_spec.js b/spec/frontend/environments/canary_ingress_spec.js
index e0247731b63..1d0d9385bfe 100644
--- a/spec/frontend/environments/canary_ingress_spec.js
+++ b/spec/frontend/environments/canary_ingress_spec.js
@@ -1,21 +1,21 @@
-import { GlDropdownItem } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { createMockDirective } from 'helpers/vue_mock_directive';
import CanaryIngress from '~/environments/components/canary_ingress.vue';
-import { CANARY_UPDATE_MODAL } from '~/environments/constants';
import { rolloutStatus } from './graphql/mock_data';
+jest.mock('lodash/uniqueId', () => {
+ return jest.fn((input) => input);
+});
+
describe('/environments/components/canary_ingress.vue', () => {
let wrapper;
- const setWeightTo = (weightWrapper, x) =>
- weightWrapper
- .findAllComponents(GlDropdownItem)
- .at(x / 5)
- .vm.$emit('click');
+ const setWeightTo = (weightWrapper, x) => {
+ weightWrapper.vm.$emit('select', x);
+ };
const createComponent = (props = {}, options = {}) => {
- wrapper = mount(CanaryIngress, {
+ wrapper = mountExtended(CanaryIngress, {
propsData: {
canaryIngress: {
canary_weight: 60,
@@ -37,11 +37,11 @@ describe('/environments/components/canary_ingress.vue', () => {
let stableWeightDropdown;
beforeEach(() => {
- stableWeightDropdown = wrapper.find('[data-testid="stable-weight"]');
+ stableWeightDropdown = extendedWrapper(wrapper.find('#stable-weight-'));
});
it('displays the current stable weight', () => {
- expect(stableWeightDropdown.props('text')).toBe('40');
+ expect(stableWeightDropdown.props('selected')).toBe(40);
});
it('emits a change with the new canary weight', () => {
@@ -51,17 +51,9 @@ describe('/environments/components/canary_ingress.vue', () => {
});
it('lists options from 0 to 100 in increments of 5', () => {
- const options = stableWeightDropdown.findAllComponents(GlDropdownItem);
+ const options = stableWeightDropdown.props('items');
expect(options).toHaveLength(21);
- options.wrappers.forEach((w, i) => expect(w.text()).toBe((i * 5).toString()));
- });
-
- it('is set to open the change modal', () => {
- stableWeightDropdown
- .findAllComponents(GlDropdownItem)
- .wrappers.forEach((w) =>
- expect(getBinding(w.element, 'gl-modal')).toMatchObject({ value: CANARY_UPDATE_MODAL }),
- );
+ options.forEach((option, i) => expect(option.text).toBe((i * 5).toString()));
});
});
@@ -69,11 +61,11 @@ describe('/environments/components/canary_ingress.vue', () => {
let canaryWeightDropdown;
beforeEach(() => {
- canaryWeightDropdown = wrapper.find('[data-testid="canary-weight"]');
+ canaryWeightDropdown = wrapper.find('#canary-weight-');
});
it('displays the current canary weight', () => {
- expect(canaryWeightDropdown.props('text')).toBe('60');
+ expect(canaryWeightDropdown.props('selected')).toBe(60);
});
it('emits a change with the new canary weight', () => {
@@ -83,17 +75,9 @@ describe('/environments/components/canary_ingress.vue', () => {
});
it('lists options from 0 to 100 in increments of 5', () => {
- canaryWeightDropdown
- .findAllComponents(GlDropdownItem)
- .wrappers.forEach((w, i) => expect(w.text()).toBe((i * 5).toString()));
- });
-
- it('is set to open the change modal', () => {
- canaryWeightDropdown
- .findAllComponents(GlDropdownItem)
- .wrappers.forEach((w) =>
- expect(getBinding(w.element, 'gl-modal')).toMatchObject({ value: CANARY_UPDATE_MODAL }),
- );
+ const options = canaryWeightDropdown.props('items');
+ expect(options).toHaveLength(21);
+ options.forEach((option, i) => expect(option.text).toBe((i * 5).toString()));
});
});
@@ -106,8 +90,8 @@ describe('/environments/components/canary_ingress.vue', () => {
});
it('shows the correct weight', () => {
- const canaryWeightDropdown = wrapper.find('[data-testid="canary-weight"]');
- expect(canaryWeightDropdown.props('text')).toBe('50');
+ const canaryWeightDropdown = wrapper.find('#canary-weight-');
+ expect(canaryWeightDropdown.props('selected')).toBe(50);
});
});
});
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index 22dd7437d82..5888b22aece 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -28,12 +28,11 @@ const userAccessAuthorizedAgents = [
const configuration = {
basePath: mockKasTunnelUrl.replace(/\/$/, ''),
- baseOptions: {
- headers: {
- 'GitLab-Agent-Id': 2,
- },
- withCredentials: true,
+ headers: {
+ 'GitLab-Agent-Id': 2,
+ 'Content-Type': 'application/json',
},
+ credentials: 'include',
};
describe('~/environments/components/form.vue', () => {
diff --git a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
index 1d41fb11b14..ed15c66f4c6 100644
--- a/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
+++ b/spec/frontend/environments/graphql/resolvers/kubernetes_spec.js
@@ -29,9 +29,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
describe('k8sPods', () => {
const mockPodsListFn = jest.fn().mockImplementation(() => {
return Promise.resolve({
- data: {
- items: k8sPodsMock,
- },
+ items: k8sPodsMock,
});
});
@@ -50,7 +48,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
it('should request namespaced pods from the cluster_client library if namespace is specified', async () => {
const pods = await mockResolvers.Query.k8sPods(null, { configuration, namespace });
- expect(mockNamespacedPodsListFn).toHaveBeenCalledWith(namespace);
+ expect(mockNamespacedPodsListFn).toHaveBeenCalledWith({ namespace });
expect(mockAllPodsListFn).not.toHaveBeenCalled();
expect(pods).toEqual(k8sPodsMock);
@@ -76,22 +74,42 @@ describe('~/frontend/environments/graphql/resolvers', () => {
describe('k8sServices', () => {
const mockServicesListFn = jest.fn().mockImplementation(() => {
return Promise.resolve({
- data: {
- items: k8sServicesMock,
- },
+ items: k8sServicesMock,
});
});
+ const mockNamespacedServicesListFn = jest.fn().mockImplementation(mockServicesListFn);
+ const mockAllServicesListFn = jest.fn().mockImplementation(mockServicesListFn);
+
beforeEach(() => {
jest
.spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces')
.mockImplementation(mockServicesListFn);
+
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1NamespacedService')
+ .mockImplementation(mockNamespacedServicesListFn);
+ jest
+ .spyOn(CoreV1Api.prototype, 'listCoreV1ServiceForAllNamespaces')
+ .mockImplementation(mockAllServicesListFn);
});
- it('should request services from the cluster_client library', async () => {
- const services = await mockResolvers.Query.k8sServices(null, { configuration });
+ it('should request namespaced services from the cluster_client library if namespace is specified', async () => {
+ const services = await mockResolvers.Query.k8sServices(null, { configuration, namespace });
+
+ expect(mockNamespacedServicesListFn).toHaveBeenCalledWith({ namespace });
+ expect(mockAllServicesListFn).not.toHaveBeenCalled();
+
+ expect(services).toEqual(k8sServicesMock);
+ });
+ it('should request all services from the cluster_client library if namespace is not specified', async () => {
+ const services = await mockResolvers.Query.k8sServices(null, {
+ configuration,
+ namespace: '',
+ });
expect(mockServicesListFn).toHaveBeenCalled();
+ expect(mockNamespacedServicesListFn).not.toHaveBeenCalled();
expect(services).toEqual(k8sServicesMock);
});
@@ -159,7 +177,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
await mockResolvers.Query.k8sWorkloads(null, { configuration, namespace });
namespacedMocks.forEach((workloadMock) => {
- expect(workloadMock.spy).toHaveBeenCalledWith(namespace);
+ expect(workloadMock.spy).toHaveBeenCalledWith({ namespace });
});
});
@@ -194,9 +212,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
describe('k8sNamespaces', () => {
const mockNamespacesListFn = jest.fn().mockImplementation(() => {
return Promise.resolve({
- data: {
- items: k8sNamespacesMock,
- },
+ items: k8sNamespacesMock,
});
});
@@ -221,13 +237,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
])(
'should throw an error if the API call fails with the reason "%s"',
async (reason, message) => {
- jest.spyOn(CoreV1Api.prototype, 'listCoreV1Namespace').mockRejectedValue({
- response: {
- data: {
- reason,
- },
- },
- });
+ jest.spyOn(CoreV1Api.prototype, 'listCoreV1Namespace').mockRejectedValue({ reason });
await expect(mockResolvers.Query.k8sNamespaces(null, { configuration })).rejects.toThrow(
message,
diff --git a/spec/frontend/environments/kubernetes_overview_spec.js b/spec/frontend/environments/kubernetes_overview_spec.js
index aa7e2e9a3b7..2b810aac653 100644
--- a/spec/frontend/environments/kubernetes_overview_spec.js
+++ b/spec/frontend/environments/kubernetes_overview_spec.js
@@ -27,10 +27,11 @@ const provide = {
const configuration = {
basePath: provide.kasTunnelUrl.replace(/\/$/, ''),
- baseOptions: {
- headers: { 'GitLab-Agent-Id': '1' },
- withCredentials: true,
+ headers: {
+ 'GitLab-Agent-Id': '1',
+ 'Content-Type': 'application/json',
},
+ credentials: 'include',
};
describe('~/environments/components/kubernetes_overview.vue', () => {
diff --git a/spec/frontend/environments/kubernetes_pods_spec.js b/spec/frontend/environments/kubernetes_pods_spec.js
index 0420d8df1a9..a51c85468b4 100644
--- a/spec/frontend/environments/kubernetes_pods_spec.js
+++ b/spec/frontend/environments/kubernetes_pods_spec.js
@@ -123,7 +123,7 @@ describe('~/environments/components/kubernetes_pods.vue', () => {
});
it('emits an error message', () => {
- expect(wrapper.emitted('cluster-error')).toMatchObject([[error]]);
+ expect(wrapper.emitted('cluster-error')).toMatchObject([[error.message]]);
});
});
});
diff --git a/spec/frontend/environments/kubernetes_summary_spec.js b/spec/frontend/environments/kubernetes_summary_spec.js
index 22c81f29f64..fdcf32e7d01 100644
--- a/spec/frontend/environments/kubernetes_summary_spec.js
+++ b/spec/frontend/environments/kubernetes_summary_spec.js
@@ -16,9 +16,7 @@ describe('~/environments/components/kubernetes_summary.vue', () => {
const namespace = 'my-kubernetes-namespace';
const configuration = {
basePath: mockKasTunnelUrl,
- baseOptions: {
- headers: { 'GitLab-Agent-Id': '1' },
- },
+ headers: { 'GitLab-Agent-Id': '1', 'Content-Type': 'application/json' },
};
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
@@ -121,7 +119,7 @@ describe('~/environments/components/kubernetes_summary.vue', () => {
createWrapper(createErroredApolloProvider());
await waitForPromises();
- expect(wrapper.emitted('cluster-error')).toEqual([[error]]);
+ expect(wrapper.emitted('cluster-error')).toEqual([[error.message]]);
});
});
});
diff --git a/spec/frontend/environments/kubernetes_tabs_spec.js b/spec/frontend/environments/kubernetes_tabs_spec.js
index 81b0bb86e0e..fecd6d2a8ee 100644
--- a/spec/frontend/environments/kubernetes_tabs_spec.js
+++ b/spec/frontend/environments/kubernetes_tabs_spec.js
@@ -162,7 +162,7 @@ describe('~/environments/components/kubernetes_tabs.vue', () => {
createWrapper(createErroredApolloProvider());
await waitForPromises();
- expect(wrapper.emitted('cluster-error')).toEqual([[error]]);
+ expect(wrapper.emitted('cluster-error')).toEqual([[error.message]]);
});
});
diff --git a/spec/frontend/fixtures/autocomplete.rb b/spec/frontend/fixtures/autocomplete.rb
index 6215fa44e27..0ceacc41cdb 100644
--- a/spec/frontend/fixtures/autocomplete.rb
+++ b/spec/frontend/fixtures/autocomplete.rb
@@ -22,15 +22,17 @@ RSpec.describe ::AutocompleteController, '(JavaScript fixtures)', type: :control
project.add_developer(user)
end
- get :users,
- format: :json,
- params: {
- project_id: project.id,
- active: true,
- current_user: true,
- author: merge_request.author.id,
- merge_request_iid: merge_request.iid
- }
+ get(
+ :users,
+ format: :json,
+ params: {
+ project_id: project.id,
+ active: true,
+ current_user: true,
+ author: merge_request.author.id,
+ merge_request_iid: merge_request.iid
+ }
+ )
expect(response).to be_successful
end
diff --git a/spec/frontend/fixtures/autocomplete_sources.rb b/spec/frontend/fixtures/autocomplete_sources.rb
index 74bf58cc106..2c28440ab0c 100644
--- a/spec/frontend/fixtures/autocomplete_sources.rb
+++ b/spec/frontend/fixtures/autocomplete_sources.rb
@@ -26,14 +26,16 @@ RSpec.describe Projects::AutocompleteSourcesController, '(JavaScript fixtures)',
create(:label, project: project, title: 'P3')
create(:label, project: project, title: 'P4')
- get :labels,
- format: :json,
- params: {
- namespace_id: group.path,
- project_id: project.path,
- type: issue.class.name,
- type_id: issue.id
- }
+ get(
+ :labels,
+ format: :json,
+ params: {
+ namespace_id: group.path,
+ project_id: project.path,
+ type: issue.class.name,
+ type_id: issue.id
+ }
+ )
expect(response).to be_successful
end
diff --git a/spec/frontend/fixtures/environments.rb b/spec/frontend/fixtures/environments.rb
index 81f1eb11e3e..8cf0977c5ed 100644
--- a/spec/frontend/fixtures/environments.rb
+++ b/spec/frontend/fixtures/environments.rb
@@ -27,13 +27,16 @@ RSpec.describe 'Environments (JavaScript fixtures)', feature_category: :environm
query = get_graphql_query_as_string(environment_details_query_path)
puts project.full_path
puts environment.name
- post_graphql(query, current_user: admin,
- variables:
- {
- projectFullPath: project.full_path,
- environmentName: environment.name,
- pageSize: 10
- })
+ post_graphql(
+ query,
+ current_user: admin,
+ variables:
+ {
+ projectFullPath: project.full_path,
+ environmentName: environment.name,
+ pageSize: 10
+ }
+ )
expect_graphql_errors_to_be_empty
end
end
@@ -58,13 +61,16 @@ RSpec.describe 'Environments (JavaScript fixtures)', feature_category: :environm
it "graphql/#{environment_details_query_path}.json" do
query = get_graphql_query_as_string(environment_details_query_path)
- post_graphql(query, current_user: admin,
- variables:
- {
- projectFullPath: project.full_path,
- environmentName: environment.name,
- pageSize: 10
- })
+ post_graphql(
+ query,
+ current_user: admin,
+ variables:
+ {
+ projectFullPath: project.full_path,
+ environmentName: environment.name,
+ pageSize: 10
+ }
+ )
expect_graphql_errors_to_be_empty
end
end
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index 9e6fcea2d17..90aa0544526 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -70,25 +70,29 @@ RSpec.describe API::Issues, '(JavaScript fixtures)', type: :request do
issue_title = 'foo'
issue_description = 'closed'
milestone = create(:milestone, title: '1.0.0', project: project)
- issue = create :issue,
- author: user,
- assignees: [user],
- project: project,
- milestone: milestone,
- created_at: generate(:past_time),
- updated_at: 1.hour.ago,
- title: issue_title,
- description: issue_description
+ issue = create(
+ :issue,
+ author: user,
+ assignees: [user],
+ project: project,
+ milestone: milestone,
+ created_at: generate(:past_time),
+ updated_at: 1.hour.ago,
+ title: issue_title,
+ description: issue_description
+ )
project.add_reporter(user)
create_referencing_mr(user, project, issue)
- create(:merge_request,
- :simple,
- author: user,
- source_project: project,
- target_project: project,
- description: "Some description")
+ create(
+ :merge_request,
+ :simple,
+ author: user,
+ source_project: project,
+ target_project: project,
+ description: "Some description"
+ )
project2 = create(:project, :public, creator_id: user.id, namespace: user.namespace)
create_referencing_mr(user, project2, issue).update!(head_pipeline: create(:ci_pipeline))
diff --git a/spec/frontend/fixtures/releases.rb b/spec/frontend/fixtures/releases.rb
index c7e3d8fe804..32ebe174800 100644
--- a/spec/frontend/fixtures/releases.rb
+++ b/spec/frontend/fixtures/releases.rb
@@ -11,23 +11,27 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
let_it_be(:user) { create(:user, email: 'user@example.gitlab.com', username: 'user1') }
let_it_be(:milestone_12_3) do
- create(:milestone,
- id: 123,
- project: project,
- title: '12.3',
- description: 'The 12.3 milestone',
- start_date: Time.zone.parse('2018-12-10'),
- due_date: Time.zone.parse('2019-01-10'))
+ create(
+ :milestone,
+ id: 123,
+ project: project,
+ title: '12.3',
+ description: 'The 12.3 milestone',
+ start_date: Time.zone.parse('2018-12-10'),
+ due_date: Time.zone.parse('2019-01-10')
+ )
end
let_it_be(:milestone_12_4) do
- create(:milestone,
- id: 124,
- project: project,
- title: '12.4',
- description: 'The 12.4 milestone',
- start_date: Time.zone.parse('2019-01-10'),
- due_date: Time.zone.parse('2019-02-10'))
+ create(
+ :milestone,
+ id: 124,
+ project: project,
+ title: '12.4',
+ description: 'The 12.4 milestone',
+ start_date: Time.zone.parse('2019-01-10'),
+ due_date: Time.zone.parse('2019-02-10')
+ )
end
let_it_be(:open_issues_12_3) do
@@ -47,68 +51,78 @@ RSpec.describe 'Releases (JavaScript fixtures)' do
end
let_it_be(:release) do
- create(:release,
- milestones: [milestone_12_3, milestone_12_4],
- project: project,
- tag: 'v1.1',
- name: 'The first release',
- author: user,
- description: 'Best. Release. **Ever.** :rocket:',
- created_at: Time.zone.parse('2018-12-3'),
- released_at: Time.zone.parse('2018-12-10'))
+ create(
+ :release,
+ milestones: [milestone_12_3, milestone_12_4],
+ project: project,
+ tag: 'v1.1',
+ name: 'The first release',
+ author: user,
+ description: 'Best. Release. **Ever.** :rocket:',
+ created_at: Time.zone.parse('2018-12-3'),
+ released_at: Time.zone.parse('2018-12-10')
+ )
end
let_it_be(:evidence) do
- create(:evidence,
- release: release,
- collected_at: Time.zone.parse('2018-12-03'))
+ create(:evidence, release: release, collected_at: Time.zone.parse('2018-12-03'))
end
let_it_be(:other_link) do
- create(:release_link,
- id: 10,
- release: release,
- name: 'linux-amd64 binaries',
- filepath: '/binaries/linux-amd64',
- url: 'https://downloads.example.com/bin/gitlab-linux-amd64')
+ create(
+ :release_link,
+ id: 10,
+ release: release,
+ name: 'linux-amd64 binaries',
+ filepath: '/binaries/linux-amd64',
+ url: 'https://downloads.example.com/bin/gitlab-linux-amd64'
+ )
end
let_it_be(:runbook_link) do
- create(:release_link,
- id: 11,
- release: release,
- name: 'Runbook',
- url: "#{release.project.web_url}/runbook",
- link_type: :runbook)
+ create(
+ :release_link,
+ id: 11,
+ release: release,
+ name: 'Runbook',
+ url: "#{release.project.web_url}/runbook",
+ link_type: :runbook
+ )
end
let_it_be(:package_link) do
- create(:release_link,
- id: 12,
- release: release,
- name: 'Package',
- url: 'https://example.com/package',
- link_type: :package)
+ create(
+ :release_link,
+ id: 12,
+ release: release,
+ name: 'Package',
+ url: 'https://example.com/package',
+ link_type: :package
+ )
end
let_it_be(:image_link) do
- create(:release_link,
- id: 13,
- release: release,
- name: 'Image',
- url: 'https://example.com/image',
- link_type: :image)
+ create(
+ :release_link,
+ id: 13,
+ release: release,
+ name: 'Image',
+ url: 'https://example.com/image',
+ link_type: :image
+ )
end
let_it_be(:another_release) do
- create(:release,
- project: project,
- tag: 'v1.2',
- name: 'The second release',
- author: user,
- description: 'An okay release :shrug:',
- created_at: Time.zone.parse('2019-01-03'),
- released_at: Time.zone.parse('2019-01-10'))
+ create(
+ :release,
+ project: project,
+ tag: 'v1.2',
+ name: 'The second release',
+ author: user,
+ description: 'An okay release :shrug:',
+ created_at: Time.zone.parse('2019-01-03'),
+ released_at: Time.zone.parse('2019-01-10')
+ )
end
before do
diff --git a/spec/frontend/fixtures/search.rb b/spec/frontend/fixtures/search.rb
index b2da383d657..0036fb353a5 100644
--- a/spec/frontend/fixtures/search.rb
+++ b/spec/frontend/fixtures/search.rb
@@ -58,9 +58,10 @@ RSpec.describe SearchController, '(JavaScript fixtures)', type: :controller do
project_id: project.id,
startline: 2)
],
- total_count: 4,
- limit: 4,
- offset: 0)
+ total_count: 4,
+ limit: 4,
+ offset: 0
+ )
end
before do
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index 2d19c9871b6..da465552db3 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -55,14 +55,14 @@ describe('GfmAutoComplete', () => {
describe('assets loading', () => {
beforeEach(() => {
- atwhoInstance = { setting: {}, $inputor: 'inputor', at: '[vulnerability:' };
+ atwhoInstance = { setting: {}, $inputor: 'inputor', at: '~' };
items = ['loading'];
filterValue = gfmAutoCompleteCallbacks.filter.call(atwhoInstance, '', items);
});
it('should call the fetchData function without query', () => {
- expect(fetchDataMock.fetchData).toHaveBeenCalledWith('inputor', '[vulnerability:');
+ expect(fetchDataMock.fetchData).toHaveBeenCalledWith('inputor', '~');
});
it('should not call the default atwho filter', () => {
@@ -80,6 +80,29 @@ describe('GfmAutoComplete', () => {
items = [];
});
+ describe('when loading', () => {
+ beforeEach(() => {
+ items = ['loading'];
+ filterValue = gfmAutoCompleteCallbacks.filter.call(atwhoInstance, 'oldquery', items);
+ });
+
+ it('should call the fetchData function with query', () => {
+ expect(fetchDataMock.fetchData).toHaveBeenCalledWith(
+ 'inputor',
+ '[vulnerability:',
+ 'oldquery',
+ );
+ });
+
+ it('should not call the default atwho filter', () => {
+ expect($.fn.atwho.default.callbacks.filter).not.toHaveBeenCalled();
+ });
+
+ it('should return the passed unfiltered items', () => {
+ expect(filterValue).toEqual(items);
+ });
+ });
+
describe('when previous query is different from current one', () => {
beforeEach(() => {
gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call({
@@ -173,7 +196,7 @@ describe('GfmAutoComplete', () => {
context = {
isLoadingData: { '[vulnerability:': false },
dataSources: { vulnerabilities: 'vulnerabilities_autocomplete_url' },
- cachedData: {},
+ cachedData: { '[vulnerability:': { other_query: [] } },
};
});
@@ -206,15 +229,14 @@ describe('GfmAutoComplete', () => {
const context = {
isLoadingData: { '[vulnerability:': false },
dataSources: { vulnerabilities: 'vulnerabilities_autocomplete_url' },
- cachedData: { '[vulnerability:': [{}] },
+ cachedData: { '[vulnerability:': { query: [] } },
+ loadData: () => {},
};
fetchData.call(context, {}, '[vulnerability:', 'query');
});
- it('should anyway call axios with query ignoring cache', () => {
- expect(axios.get).toHaveBeenCalledWith('vulnerabilities_autocomplete_url', {
- params: { search: 'query' },
- });
+ it('should not call axios', () => {
+ expect(axios.get).not.toHaveBeenCalled();
});
});
});
diff --git a/spec/frontend/google_tag_manager/index_spec.js b/spec/frontend/google_tag_manager/index_spec.js
index dd8e886e6bc..c32c86d5f5a 100644
--- a/spec/frontend/google_tag_manager/index_spec.js
+++ b/spec/frontend/google_tag_manager/index_spec.js
@@ -1,537 +1,9 @@
-import { merge } from 'lodash';
-import { v4 as uuidv4 } from 'uuid';
-import {
- trackCombinedGroupProjectForm,
- trackFreeTrialAccountSubmissions,
- trackProjectImport,
- trackNewRegistrations,
- trackSaasTrialSubmit,
- trackSaasTrialGroup,
- trackSaasTrialGetStarted,
- trackTrialAcceptTerms,
- trackCheckout,
- trackTransaction,
- trackAddToCartUsageTab,
- getNamespaceId,
- trackCompanyForm,
-} from '~/google_tag_manager';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { logError } from '~/lib/logger';
-
-jest.mock('~/lib/logger');
-jest.mock('uuid');
+import { trackTrialAcceptTerms } from 'ee_else_ce/google_tag_manager';
describe('~/google_tag_manager/index', () => {
- let spy;
-
- beforeEach(() => {
- spy = jest.fn();
-
- window.dataLayer = {
- push: spy,
- };
- window.gon.features = {
- gitlabGtmDatalayer: true,
- };
- });
-
- const createHTML = ({ links = [], forms = [] } = {}) => {
- // .foo elements are used to test elements which shouldn't do anything
- const allLinks = links.concat({ cls: 'foo' });
- const allForms = forms.concat({ cls: 'foo' });
-
- const el = document.createElement('div');
-
- allLinks.forEach(({ cls = '', id = '', href = '#', text = 'Hello', attributes = {} }) => {
- const a = document.createElement('a');
- a.id = id;
- a.href = href || '#';
- a.className = cls;
- a.textContent = text;
-
- Object.entries(attributes).forEach(([key, value]) => {
- a.setAttribute(key, value);
- });
-
- el.append(a);
- });
-
- allForms.forEach(({ cls = '', id = '' }) => {
- const form = document.createElement('form');
- form.id = id;
- form.className = cls;
-
- el.append(form);
- });
-
- return el.innerHTML;
- };
-
- const triggerEvent = (selector, eventType) => {
- const el = document.querySelector(selector);
-
- el.dispatchEvent(new Event(eventType));
- };
-
- const getSelector = ({ id, cls }) => (id ? `#${id}` : `.${cls}`);
-
- const createTestCase = (subject, { forms = [], links = [] }) => {
- const expectedFormEvents = forms.map(({ expectation, ...form }) => ({
- selector: getSelector(form),
- trigger: 'submit',
- expectation,
- }));
-
- const expectedLinkEvents = links.map(({ expectation, ...link }) => ({
- selector: getSelector(link),
- trigger: 'click',
- expectation,
- }));
-
- return [
- subject,
- {
- forms,
- links,
- expectedEvents: [...expectedFormEvents, ...expectedLinkEvents],
- },
- ];
- };
-
- const createOmniAuthTestCase = (subject, accountType) =>
- createTestCase(subject, {
- forms: [
- {
- id: 'new_new_user',
- expectation: {
- event: 'accountSubmit',
- accountMethod: 'form',
- accountType,
- },
- },
- ],
- links: [
- {
- // id is needed so that the test selects the right element to trigger
- id: 'test-0',
- cls: 'js-oauth-login',
- attributes: {
- 'data-provider': 'myspace',
- },
- expectation: {
- event: 'accountSubmit',
- accountMethod: 'myspace',
- accountType,
- },
- },
- {
- id: 'test-1',
- cls: 'js-oauth-login',
- attributes: {
- 'data-provider': 'gitlab',
- },
- expectation: {
- event: 'accountSubmit',
- accountMethod: 'gitlab',
- accountType,
- },
- },
- ],
- });
-
- describe.each([
- createOmniAuthTestCase(trackFreeTrialAccountSubmissions, 'freeThirtyDayTrial'),
- createOmniAuthTestCase(trackNewRegistrations, 'standardSignUp'),
- createTestCase(trackSaasTrialGroup, {
- forms: [{ cls: 'js-saas-trial-group', expectation: { event: 'saasTrialGroup' } }],
- }),
- createTestCase(trackProjectImport, {
- links: [
- {
- id: 'js-test-btn-0',
- cls: 'js-import-project-btn',
- attributes: { 'data-platform': 'bitbucket' },
- expectation: { event: 'projectImport', platform: 'bitbucket' },
- },
- {
- // id is neeeded so we trigger the right element in the test
- id: 'js-test-btn-1',
- cls: 'js-import-project-btn',
- attributes: { 'data-platform': 'github' },
- expectation: { event: 'projectImport', platform: 'github' },
- },
- ],
- }),
- createTestCase(trackSaasTrialGetStarted, {
- links: [
- {
- cls: 'js-get-started-btn',
- expectation: { event: 'saasTrialGetStarted' },
- },
- ],
- }),
- createTestCase(trackAddToCartUsageTab, {
- links: [
- {
- cls: 'js-buy-additional-minutes',
- expectation: {
- event: 'EECproductAddToCart',
- ecommerce: {
- currencyCode: 'USD',
- add: {
- products: [
- {
- name: 'CI/CD Minutes',
- id: '0003',
- price: '10',
- brand: 'GitLab',
- category: 'DevOps',
- variant: 'add-on',
- quantity: 1,
- },
- ],
- },
- },
- },
- },
- ],
- }),
- createTestCase(trackCombinedGroupProjectForm, {
- forms: [
- {
- cls: 'js-groups-projects-form',
- expectation: { event: 'combinedGroupProjectFormSubmit' },
- },
- ],
- }),
- ])('%p', (subject, { links = [], forms = [], expectedEvents }) => {
- beforeEach(() => {
- setHTMLFixture(createHTML({ links, forms }));
-
- subject();
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it.each(expectedEvents)('when %p', ({ selector, trigger, expectation }) => {
- expect(spy).not.toHaveBeenCalled();
-
- triggerEvent(selector, trigger);
-
- expect(spy).toHaveBeenCalledTimes(1);
- expect(spy).toHaveBeenCalledWith(expectation);
- expect(logError).not.toHaveBeenCalled();
- });
-
- it('when random link is clicked, does nothing', () => {
- triggerEvent('a.foo', 'click');
-
- expect(spy).not.toHaveBeenCalled();
- });
-
- it('when random form is submitted, does nothing', () => {
- triggerEvent('form.foo', 'submit');
-
- expect(spy).not.toHaveBeenCalled();
- });
- });
-
describe('No listener events', () => {
- it('when trackSaasTrialSubmit is invoked', () => {
- expect(spy).not.toHaveBeenCalled();
-
- trackSaasTrialSubmit();
-
- expect(spy).toHaveBeenCalledTimes(1);
- expect(spy).toHaveBeenCalledWith({ event: 'saasTrialSubmit' });
- expect(logError).not.toHaveBeenCalled();
- });
-
it('when trackTrialAcceptTerms is invoked', () => {
- expect(spy).not.toHaveBeenCalled();
-
- trackTrialAcceptTerms();
-
- expect(spy).toHaveBeenCalledTimes(1);
- expect(spy).toHaveBeenCalledWith({ event: 'saasTrialAcceptTerms' });
- expect(logError).not.toHaveBeenCalled();
- });
-
- describe('when trackCheckout is invoked', () => {
- it('with selectedPlan: 2c92a00d76f0d5060176f2fb0a5029ff', () => {
- expect(spy).not.toHaveBeenCalled();
-
- trackCheckout('2c92a00d76f0d5060176f2fb0a5029ff', 1);
-
- expect(spy.mock.calls.flatMap((x) => x)).toEqual([
- { ecommerce: null },
- {
- event: 'EECCheckout',
- ecommerce: {
- currencyCode: 'USD',
- checkout: {
- actionField: { step: 1 },
- products: [
- {
- brand: 'GitLab',
- category: 'DevOps',
- id: '0002',
- name: 'Premium',
- price: '228',
- quantity: 1,
- variant: 'SaaS',
- },
- ],
- },
- },
- },
- ]);
- });
-
- it('with selectedPlan: 2c92a0ff76f0d5250176f2f8c86f305a', () => {
- expect(spy).not.toHaveBeenCalled();
-
- trackCheckout('2c92a0ff76f0d5250176f2f8c86f305a', 1);
-
- expect(spy).toHaveBeenCalledTimes(2);
- expect(spy).toHaveBeenCalledWith({ ecommerce: null });
- expect(spy).toHaveBeenCalledWith({
- event: 'EECCheckout',
- ecommerce: {
- currencyCode: 'USD',
- checkout: {
- actionField: { step: 1 },
- products: [
- {
- brand: 'GitLab',
- category: 'DevOps',
- id: '0001',
- name: 'Ultimate',
- price: '1188',
- quantity: 1,
- variant: 'SaaS',
- },
- ],
- },
- },
- });
- });
-
- it('with selectedPlan: Something else', () => {
- expect(spy).not.toHaveBeenCalled();
-
- trackCheckout('Something else', 1);
-
- expect(spy).not.toHaveBeenCalled();
- });
-
- it('with a different number of users', () => {
- expect(spy).not.toHaveBeenCalled();
-
- trackCheckout('2c92a0ff76f0d5250176f2f8c86f305a', 5);
-
- expect(spy).toHaveBeenCalledTimes(2);
- expect(spy).toHaveBeenCalledWith({ ecommerce: null });
- expect(spy).toHaveBeenCalledWith({
- event: 'EECCheckout',
- ecommerce: {
- currencyCode: 'USD',
- checkout: {
- actionField: { step: 1 },
- products: [
- {
- brand: 'GitLab',
- category: 'DevOps',
- id: '0001',
- name: 'Ultimate',
- price: '1188',
- quantity: 5,
- variant: 'SaaS',
- },
- ],
- },
- },
- });
- });
- });
-
- describe('when trackTransactions is invoked', () => {
- describe.each([
- {
- selectedPlan: '2c92a00d76f0d5060176f2fb0a5029ff',
- revenue: 228,
- name: 'Premium',
- id: '0002',
- },
- {
- selectedPlan: '2c92a0ff76f0d5250176f2f8c86f305a',
- revenue: 1188,
- name: 'Ultimate',
- id: '0001',
- },
- ])('with %o', (planObject) => {
- it('invokes pushes a new event that references the selected plan', () => {
- const { selectedPlan, revenue, name, id } = planObject;
-
- expect(spy).not.toHaveBeenCalled();
- uuidv4.mockImplementationOnce(() => '123');
-
- const transactionDetails = {
- paymentOption: 'visa',
- revenue,
- tax: 10,
- selectedPlan,
- quantity: 1,
- };
-
- trackTransaction(transactionDetails);
-
- expect(spy.mock.calls.flatMap((x) => x)).toEqual([
- { ecommerce: null },
- {
- event: 'EECtransactionSuccess',
- ecommerce: {
- currencyCode: 'USD',
- purchase: {
- actionField: {
- id: '123',
- affiliation: 'GitLab',
- option: 'visa',
- revenue: revenue.toString(),
- tax: '10',
- },
- products: [
- {
- brand: 'GitLab',
- category: 'DevOps',
- dimension36: 'not available',
- id,
- name,
- price: revenue.toString(),
- quantity: 1,
- variant: 'SaaS',
- },
- ],
- },
- },
- },
- ]);
- });
- });
- });
-
- describe('when trackTransaction is invoked', () => {
- describe('with an invalid plan object', () => {
- it('does not get called', () => {
- expect(spy).not.toHaveBeenCalled();
-
- trackTransaction({ selectedPlan: 'notAplan' });
-
- expect(spy).not.toHaveBeenCalled();
- });
- });
- });
-
- describe('when trackCompanyForm is invoked', () => {
- it('with an ultimate trial', () => {
- expect(spy).not.toHaveBeenCalled();
-
- trackCompanyForm('ultimate_trial');
-
- expect(spy).toHaveBeenCalledTimes(1);
- expect(spy).toHaveBeenCalledWith({
- event: 'aboutYourCompanyFormSubmit',
- aboutYourCompanyType: 'ultimate_trial',
- });
- expect(logError).not.toHaveBeenCalled();
- });
-
- it('with a free account', () => {
- expect(spy).not.toHaveBeenCalled();
-
- trackCompanyForm('free_account');
-
- expect(spy).toHaveBeenCalledTimes(1);
- expect(spy).toHaveBeenCalledWith({
- event: 'aboutYourCompanyFormSubmit',
- aboutYourCompanyType: 'free_account',
- });
- expect(logError).not.toHaveBeenCalled();
- });
- });
- });
-
- describe.each([
- { dataLayer: null },
- { gon: { features: null } },
- { gon: { features: { gitlabGtmDatalayer: false } } },
- ])('when window %o', (windowAttrs) => {
- beforeEach(() => {
- merge(window, windowAttrs);
- });
-
- it('no ops', () => {
- setHTMLFixture(createHTML({ forms: [{ cls: 'js-saas-trial-group' }] }));
-
- trackSaasTrialGroup();
-
- triggerEvent('.js-saas-trial-group', 'submit');
-
- expect(spy).not.toHaveBeenCalled();
- expect(logError).not.toHaveBeenCalled();
-
- resetHTMLFixture();
- });
- });
-
- describe('when window.dataLayer throws error', () => {
- const pushError = new Error('test');
-
- beforeEach(() => {
- window.dataLayer = {
- push() {
- throw pushError;
- },
- };
- });
-
- it('logs error', () => {
- setHTMLFixture(createHTML({ forms: [{ cls: 'js-saas-trial-group' }] }));
-
- trackSaasTrialGroup();
-
- triggerEvent('.js-saas-trial-group', 'submit');
-
- expect(logError).toHaveBeenCalledWith(
- 'Unexpected error while pushing to dataLayer',
- pushError,
- );
-
- resetHTMLFixture();
- });
- });
-
- describe('when getting the namespace_id from Snowplow standard context', () => {
- describe('when window.gl.snowplowStandardContext.data.namespace_id has a value', () => {
- beforeEach(() => {
- window.gl = { snowplowStandardContext: { data: { namespace_id: '321' } } };
- });
-
- it('returns the value', () => {
- expect(getNamespaceId()).toBe('321');
- });
- });
-
- describe('when window.gl.snowplowStandardContext.data.namespace_id is undefined', () => {
- beforeEach(() => {
- window.gl = {};
- });
-
- it('returns a placeholder value', () => {
- expect(getNamespaceId()).toBe('not available');
- });
+ expect(trackTrialAcceptTerms()).toBeUndefined();
});
});
});
diff --git a/spec/frontend/helpers/startup_css_helper_spec.js b/spec/frontend/helpers/startup_css_helper_spec.js
deleted file mode 100644
index 28c742386cc..00000000000
--- a/spec/frontend/helpers/startup_css_helper_spec.js
+++ /dev/null
@@ -1,67 +0,0 @@
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { waitForCSSLoaded } from '~/helpers/startup_css_helper';
-
-describe('waitForCSSLoaded', () => {
- let mockedCallback;
-
- beforeEach(() => {
- mockedCallback = jest.fn();
- });
-
- describe('Promise-like api', () => {
- it('can be used with a callback', async () => {
- await waitForCSSLoaded(mockedCallback);
- expect(mockedCallback).toHaveBeenCalledTimes(1);
- });
-
- it('can be used as a promise', async () => {
- await waitForCSSLoaded().then(mockedCallback);
- expect(mockedCallback).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('when gon features is not provided', () => {
- beforeEach(() => {
- window.gon = null;
- });
-
- it('should invoke the action right away', async () => {
- const events = waitForCSSLoaded(mockedCallback);
- await events;
-
- expect(mockedCallback).toHaveBeenCalledTimes(1);
- });
- });
-
- describe('with startup css enabled', () => {
- it('should dispatch CSSLoaded when the assets are cached or already loaded', async () => {
- setHTMLFixture(`
- <link href="one.css" data-startupcss="loaded">
- <link href="two.css" data-startupcss="loaded">
- `);
- await waitForCSSLoaded(mockedCallback);
-
- expect(mockedCallback).toHaveBeenCalledTimes(1);
-
- resetHTMLFixture();
- });
-
- it('should wait to call CssLoaded until the assets are loaded', async () => {
- setHTMLFixture(`
- <link href="one.css" data-startupcss="loading">
- <link href="two.css" data-startupcss="loading">
- `);
- const events = waitForCSSLoaded(mockedCallback);
- document.querySelectorAll('[data-startupcss="loading"]').forEach((elem) => {
- // eslint-disable-next-line no-param-reassign
- elem.dataset.startupcss = 'loaded';
- });
- document.dispatchEvent(new CustomEvent('CSSStartupLinkLoaded'));
- await events;
-
- expect(mockedCallback).toHaveBeenCalledTimes(1);
-
- resetHTMLFixture();
- });
- });
-});
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index efbbd6c7514..6a5bedb0bbb 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -4,6 +4,7 @@ import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form';
import { handleTracking } from '~/ide/lib/gitlab_web_ide/handle_tracking_event';
+import Tracking from '~/tracking';
import { TEST_HOST } from 'helpers/test_constants';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -15,6 +16,7 @@ jest.mock('~/lib/utils/csrf', () => ({
token: 'mock-csrf-token',
headerKey: 'mock-csrf-header',
}));
+jest.mock('~/tracking');
const ROOT_ELEMENT_ID = 'ide';
const TEST_NONCE = 'test123nonce';
@@ -34,9 +36,9 @@ const TEST_START_REMOTE_PARAMS = {
remotePath: '/test/projects/f oo',
connectionToken: '123abc',
};
-const TEST_EDITOR_FONT_SRC_URL = 'http://gitlab.test/assets/jetbrains-mono/JetBrainsMono.woff2';
+const TEST_EDITOR_FONT_SRC_URL = 'http://gitlab.test/assets/gitlab-mono/GitLabMono.woff2';
const TEST_EDITOR_FONT_FORMAT = 'woff2';
-const TEST_EDITOR_FONT_FAMILY = 'JebBrains Mono';
+const TEST_EDITOR_FONT_FAMILY = 'GitLab Mono';
describe('ide/init_gitlab_web_ide', () => {
let resolveConfirm;
@@ -54,9 +56,20 @@ describe('ide/init_gitlab_web_ide', () => {
el.dataset.userPreferencesPath = TEST_USER_PREFERENCES_PATH;
el.dataset.mergeRequest = TEST_MR_ID;
el.dataset.filePath = TEST_FILE_PATH;
- el.dataset.editorFontSrcUrl = TEST_EDITOR_FONT_SRC_URL;
- el.dataset.editorFontFormat = TEST_EDITOR_FONT_FORMAT;
- el.dataset.editorFontFamily = TEST_EDITOR_FONT_FAMILY;
+ el.dataset.editorFont = JSON.stringify({
+ fallback_font_family: 'monospace',
+ font_faces: [
+ {
+ family: TEST_EDITOR_FONT_FAMILY,
+ src: [
+ {
+ url: TEST_EDITOR_FONT_SRC_URL,
+ format: TEST_EDITOR_FONT_FORMAT,
+ },
+ ],
+ },
+ ],
+ });
el.dataset.signInPath = TEST_SIGN_IN_PATH;
document.body.append(el);
@@ -88,7 +101,11 @@ describe('ide/init_gitlab_web_ide', () => {
});
describe('default', () => {
+ const telemetryEnabled = true;
+
beforeEach(() => {
+ Tracking.enabled.mockReturnValueOnce(telemetryEnabled);
+
createSubject();
});
@@ -115,12 +132,22 @@ describe('ide/init_gitlab_web_ide', () => {
signIn: TEST_SIGN_IN_PATH,
},
editorFont: {
- srcUrl: TEST_EDITOR_FONT_SRC_URL,
- fontFamily: TEST_EDITOR_FONT_FAMILY,
- format: TEST_EDITOR_FONT_FORMAT,
+ fallbackFontFamily: 'monospace',
+ fontFaces: [
+ {
+ family: TEST_EDITOR_FONT_FAMILY,
+ src: [
+ {
+ url: TEST_EDITOR_FONT_SRC_URL,
+ format: TEST_EDITOR_FONT_FORMAT,
+ },
+ ],
+ },
+ ],
},
handleStartRemote: expect.any(Function),
handleTracking,
+ telemetryEnabled,
});
});
diff --git a/spec/frontend/import/details/mock_data.js b/spec/frontend/import/details/mock_data.js
index 67148173404..b61a7f36f85 100644
--- a/spec/frontend/import/details/mock_data.js
+++ b/spec/frontend/import/details/mock_data.js
@@ -7,7 +7,7 @@ export const mockImportFailures = [
exception_class: 'ActiveRecord::RecordInvalid',
exception_message: 'Record invalid',
source: 'Gitlab::GithubImport::Importer::PullRequestImporter',
- github_identifiers: {
+ external_identifiers: {
iid: 2,
issuable_type: 'MergeRequest',
object_type: 'pull_request',
@@ -22,7 +22,7 @@ export const mockImportFailures = [
exception_class: 'ActiveRecord::RecordInvalid',
exception_message: 'Record invalid',
source: 'Gitlab::GithubImport::Importer::PullRequestImporter',
- github_identifiers: {
+ external_identifiers: {
iid: 3,
issuable_type: 'MergeRequest',
object_type: 'pull_request',
@@ -37,7 +37,7 @@ export const mockImportFailures = [
exception_class: 'NameError',
exception_message: 'some message',
source: 'Gitlab::GithubImport::Importer::LfsObjectImporter',
- github_identifiers: {
+ external_identifiers: {
oid: '3a9257fae9e86faee27d7208cb55e086f18e6f29f48c430bfbc26d42eb',
size: 2473979,
},
diff --git a/spec/frontend/import_entities/components/group_dropdown_spec.js b/spec/frontend/import_entities/components/group_dropdown_spec.js
deleted file mode 100644
index 14f39a35387..00000000000
--- a/spec/frontend/import_entities/components/group_dropdown_spec.js
+++ /dev/null
@@ -1,94 +0,0 @@
-import { GlSearchBoxByType, GlDropdown } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import GroupDropdown from '~/import_entities/components/group_dropdown.vue';
-import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import searchNamespacesWhereUserCanImportProjectsQuery from '~/import_entities/import_projects/graphql/queries/search_namespaces_where_user_can_import_projects.query.graphql';
-
-Vue.use(VueApollo);
-
-const makeGroupMock = (fullPath) => ({
- id: `gid://gitlab/Group/${fullPath}`,
- fullPath,
- name: fullPath,
- visibility: 'public',
- webUrl: `http://gdk.test:3000/groups/${fullPath}`,
- __typename: 'Group',
-});
-
-const AVAILABLE_NAMESPACES = [
- makeGroupMock('match1'),
- makeGroupMock('unrelated'),
- makeGroupMock('match2'),
-];
-
-const SEARCH_NAMESPACES_MOCK = Promise.resolve({
- data: {
- currentUser: {
- id: 'gid://gitlab/User/1',
- groups: {
- nodes: AVAILABLE_NAMESPACES,
- __typename: 'GroupConnection',
- },
- namespace: {
- id: 'gid://gitlab/Namespaces::UserNamespace/1',
- fullPath: 'root',
- __typename: 'Namespace',
- },
- __typename: 'UserCore',
- },
- },
-});
-
-describe('Import entities group dropdown component', () => {
- let wrapper;
- let namespacesTracker;
-
- const createComponent = (propsData) => {
- const apolloProvider = createMockApollo([
- [searchNamespacesWhereUserCanImportProjectsQuery, () => SEARCH_NAMESPACES_MOCK],
- ]);
-
- namespacesTracker = jest.fn();
-
- wrapper = shallowMount(GroupDropdown, {
- apolloProvider,
- scopedSlots: {
- default: namespacesTracker,
- },
- stubs: { GlDropdown },
- propsData,
- });
- };
-
- it('passes namespaces from graphql query to default slot', async () => {
- createComponent();
- jest.advanceTimersByTime(DEBOUNCE_DELAY);
- await nextTick();
- await waitForPromises();
- await nextTick();
-
- expect(namespacesTracker).toHaveBeenCalledWith({ namespaces: AVAILABLE_NAMESPACES });
- });
-
- it('filters namespaces based on user input', async () => {
- createComponent();
-
- namespacesTracker.mockReset();
- wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'match');
- jest.advanceTimersByTime(DEBOUNCE_DELAY);
- await nextTick();
- await waitForPromises();
- await nextTick();
-
- expect(namespacesTracker).toHaveBeenCalledWith({
- namespaces: [
- expect.objectContaining({ fullPath: 'match1' }),
- expect.objectContaining({ fullPath: 'match2' }),
- ],
- });
- });
-});
diff --git a/spec/frontend/import_entities/components/import_target_dropdown_spec.js b/spec/frontend/import_entities/components/import_target_dropdown_spec.js
index c12baed2374..ba0bb0b0f74 100644
--- a/spec/frontend/import_entities/components/import_target_dropdown_spec.js
+++ b/spec/frontend/import_entities/components/import_target_dropdown_spec.js
@@ -18,7 +18,6 @@ describe('ImportTargetDropdown', () => {
const defaultProps = {
selected: mockUserNamespace,
- userNamespace: mockUserNamespace,
};
const createComponent = ({ props = {} } = {}) => {
@@ -39,7 +38,7 @@ describe('ImportTargetDropdown', () => {
};
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
- const findListboxUsersItems = () => findListbox().props('items')[0].options;
+ const findListboxFirstGroupItems = () => findListbox().props('items')[0].options;
const findListboxGroupsItems = () => findListbox().props('items')[1].options;
const waitForQuery = async () => {
@@ -63,12 +62,54 @@ describe('ImportTargetDropdown', () => {
expect(findListbox().props('toggleText')).toBe('a-group-path-that-is-lo…');
});
- it('passes userNamespace as "Users" group item', () => {
- createComponent();
+ describe('when used on group import', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- expect(findListboxUsersItems()).toEqual([
- { text: mockUserNamespace, value: mockUserNamespace },
- ]);
+ it('adds "No parent" in "Parent" group', () => {
+ expect(findListboxFirstGroupItems()).toEqual([{ text: 'No parent', value: '' }]);
+ });
+
+ it('emits "select" event with { fullPath: "", id: null } when "No parent" is selected', () => {
+ findListbox().vm.$emit('select', '');
+
+ expect(wrapper.emitted('select')[0]).toEqual([{ fullPath: '', id: null }]);
+ });
+
+ it('emits "select" event with { fullPath, id } when a group is selected', async () => {
+ await waitForQuery();
+
+ const mockGroupPath = 'match1';
+
+ findListbox().vm.$emit('select', mockGroupPath);
+
+ expect(wrapper.emitted('select')[0]).toEqual([
+ { fullPath: mockGroupPath, id: `gid://gitlab/Group/${mockGroupPath}` },
+ ]);
+ });
+ });
+
+ describe('when used on project import', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { userNamespace: mockUserNamespace },
+ });
+ });
+
+ it('passes userNamespace as "Users" group item', () => {
+ expect(findListboxFirstGroupItems()).toEqual([
+ { text: mockUserNamespace, value: mockUserNamespace },
+ ]);
+ });
+
+ it('emits "select" event with path as value', () => {
+ const mockProjectPath = 'mock-project';
+
+ findListbox().vm.$emit('select', mockProjectPath);
+
+ expect(wrapper.emitted('select')[0]).toEqual([mockProjectPath]);
+ });
});
it('passes namespaces from GraphQL as "Groups" group item', async () => {
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 03d0920994c..4fab22e316a 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,8 +1,8 @@
import { GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import MockAdapter from 'axios-mock-adapter';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
@@ -55,14 +55,15 @@ describe('import table', () => {
wrapper.findAll('tbody td button').wrappers.filter((w) => w.text() === 'Import with projects')[
idx
];
- const findPaginationDropdown = () => wrapper.find('[data-testid="page-size"]');
+ const findPaginationDropdown = () => wrapper.findByTestId('page-size');
const findTargetNamespaceDropdown = (rowWrapper) =>
- rowWrapper.find('[data-testid="target-namespace-selector"]');
+ extendedWrapper(rowWrapper).findByTestId('target-namespace-dropdown');
+ const findTargetNamespaceInput = (rowWrapper) =>
+ extendedWrapper(rowWrapper).findByTestId('target-namespace-input');
const findPaginationDropdownText = () => findPaginationDropdown().find('button').text();
const findSelectionCount = () => wrapper.find('[data-test-id="selection-count"]');
const findNewPathCol = () => wrapper.find('[data-test-id="new-path-col"]');
- const findUnavailableFeaturesWarning = () =>
- wrapper.find('[data-testid="unavailable-features-alert"]');
+ const findUnavailableFeaturesWarning = () => wrapper.findByTestId('unavailable-features-alert');
const triggerSelectAllCheckbox = (checked = true) =>
wrapper.find('thead input[type=checkbox]').setChecked(checked);
@@ -88,7 +89,7 @@ describe('import table', () => {
},
);
- wrapper = mount(ImportTable, {
+ wrapper = mountExtended(ImportTable, {
propsData: {
groupPathRegex: /.*/,
jobsPath: '/fake_job_path',
@@ -220,32 +221,42 @@ describe('import table', () => {
expect(wrapper.text()).not.toContain('Showing 1-0');
});
- it('invokes importGroups mutation when row button is clicked', async () => {
- createComponent({
- bulkImportSourceGroups: () => ({
- nodes: [FAKE_GROUP],
- pageInfo: FAKE_PAGE_INFO,
- versionValidation: FAKE_VERSION_VALIDATION,
- }),
- });
+ describe('when import button is clicked', () => {
+ beforeEach(async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [FAKE_GROUP],
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ });
- jest.spyOn(apolloProvider.defaultClient, 'mutate');
+ jest.spyOn(apolloProvider.defaultClient, 'mutate');
- await waitForPromises();
+ await waitForPromises();
- await findRowImportDropdownAtIndex(0).trigger('click');
- expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
- mutation: importGroupsMutation,
- variables: {
- importRequests: [
- {
- migrateProjects: true,
- newName: FAKE_GROUP.lastImportTarget.newName,
- sourceGroupId: FAKE_GROUP.id,
- targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
- },
- ],
- },
+ await findRowImportDropdownAtIndex(0).trigger('click');
+ });
+
+ it('invokes importGroups mutation', () => {
+ expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
+ mutation: importGroupsMutation,
+ variables: {
+ importRequests: [
+ {
+ migrateProjects: true,
+ newName: FAKE_GROUP.lastImportTarget.newName,
+ sourceGroupId: FAKE_GROUP.id,
+ targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
+ },
+ ],
+ },
+ });
+ });
+
+ it('disables the import target input', () => {
+ const firstRow = wrapper.find('tbody tr');
+ expect(findTargetNamespaceInput(firstRow).attributes('disabled')).toBe('disabled');
});
});
@@ -294,6 +305,42 @@ describe('import table', () => {
expect(wrapper.find('tbody tr').text()).toContain(i18n.ERROR_TOO_MANY_REQUESTS);
});
+ it('displays inline error if backend returns validation error', async () => {
+ const mockValidationError =
+ 'Import failed. Destination URL must not start or end with a special character and must not contain consecutive special characters.';
+ const mockMutationWithProgressError = jest.fn().mockResolvedValue({
+ __typename: 'ClientBulkImportSourceGroup',
+ id: 1,
+ lastImportTarget: { id: 1, targetNamespace: 'root', newName: 'group1' },
+ progress: {
+ __typename: 'ClientBulkImportProgress',
+ id: null,
+ status: 'failed',
+ message: mockValidationError,
+ },
+ });
+
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [FAKE_GROUP],
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ importGroups: mockMutationWithProgressError,
+ });
+
+ await waitForPromises();
+ await findRowImportDropdownAtIndex(0).trigger('click');
+ await waitForPromises();
+
+ expect(mockMutationWithProgressError).toHaveBeenCalled();
+ expect(createAlert).not.toHaveBeenCalled();
+
+ const firstRow = wrapper.find('tbody tr');
+ expect(findTargetNamespaceInput(firstRow).attributes('disabled')).toBeUndefined();
+ expect(firstRow.text()).toContain(mockValidationError);
+ });
+
describe('pagination', () => {
const bulkImportSourceGroupsQueryMock = jest.fn().mockResolvedValue({
nodes: [FAKE_GROUP],
@@ -345,6 +392,28 @@ describe('import table', () => {
);
});
+ it('resets page to 1 when page size is changed', async () => {
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page', 2);
+ await waitForPromises();
+
+ expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ page: 2, perPage: 50 }),
+ expect.anything(),
+ expect.anything(),
+ );
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', 200);
+ await waitForPromises();
+
+ expect(bulkImportSourceGroupsQueryMock).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ page: 1, perPage: 200 }),
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+
it('updates status text when page is changed', async () => {
const REQUESTED_PAGE = 2;
bulkImportSourceGroupsQueryMock.mockResolvedValue({
@@ -601,7 +670,7 @@ describe('import table', () => {
});
describe('re-import', () => {
- it('renders finished row as disabled by default', async () => {
+ beforeEach(async () => {
createComponent({
bulkImportSourceGroups: () => ({
nodes: [generateFakeEntry({ id: 5, status: STATUSES.FINISHED })],
@@ -609,21 +678,15 @@ describe('import table', () => {
versionValidation: FAKE_VERSION_VALIDATION,
}),
});
+
await waitForPromises();
+ });
+ it('renders finished row as disabled by default', () => {
expect(findRowCheckbox(0).attributes('disabled')).toBeDefined();
});
it('enables row after clicking re-import', async () => {
- createComponent({
- bulkImportSourceGroups: () => ({
- nodes: [generateFakeEntry({ id: 5, status: STATUSES.FINISHED })],
- pageInfo: FAKE_PAGE_INFO,
- versionValidation: FAKE_VERSION_VALIDATION,
- }),
- });
- await waitForPromises();
-
const reimportButton = wrapper
.findAll('tbody td button')
.wrappers.find((w) => w.text().includes('Re-import'));
diff --git a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
index 46884a42707..ac95026a9a4 100644
--- a/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_target_cell_spec.js
@@ -1,10 +1,9 @@
-import { GlDropdownItem, GlFormInput } from '@gitlab/ui';
+import { GlFormInput } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import ImportGroupDropdown from '~/import_entities/components/group_dropdown.vue';
+import ImportTargetDropdown from '~/import_entities/components/import_target_dropdown.vue';
import { STATUSES } from '~/import_entities/constants';
import ImportTargetCell from '~/import_entities/import_groups/components/import_target_cell.vue';
import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
@@ -37,7 +36,7 @@ describe('import target cell', () => {
let group;
const findNameInput = () => wrapper.findComponent(GlFormInput);
- const findNamespaceDropdown = () => wrapper.findComponent(ImportGroupDropdown);
+ const findNamespaceDropdown = () => wrapper.findComponent(ImportTargetDropdown);
const createComponent = (props) => {
apolloProvider = createMockApollo([
@@ -49,7 +48,7 @@ describe('import target cell', () => {
wrapper = shallowMount(ImportTargetCell, {
apolloProvider,
- stubs: { ImportGroupDropdown },
+ stubs: { ImportTargetDropdown },
propsData: {
groupPathRegex: /.*/,
...props,
@@ -73,14 +72,14 @@ describe('import target cell', () => {
});
it('emits update-target-namespace when dropdown option is clicked', () => {
- const dropdownItem = findNamespaceDropdown().findAllComponents(GlDropdownItem).at(2);
+ const targetNamespace = {
+ fullPath: AVAILABLE_NAMESPACES[1].fullPath,
+ id: AVAILABLE_NAMESPACES[1].id,
+ };
- dropdownItem.vm.$emit('click');
+ findNamespaceDropdown().vm.$emit('select', targetNamespace);
- expect(wrapper.emitted('update-target-namespace')).toBeDefined();
- expect(wrapper.emitted('update-target-namespace')[0][0]).toStrictEqual(
- AVAILABLE_NAMESPACES[1],
- );
+ expect(wrapper.emitted('update-target-namespace')[0]).toStrictEqual([targetNamespace]);
});
});
@@ -101,36 +100,6 @@ describe('import target cell', () => {
});
});
- it('renders only no parent option if available namespaces list is empty', () => {
- createComponent({
- group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }),
- availableNamespaces: [],
- });
-
- const items = findNamespaceDropdown()
- .findAllComponents(GlDropdownItem)
- .wrappers.map((w) => w.text());
-
- expect(items[0]).toBe('No parent');
- expect(items).toHaveLength(1);
- });
-
- it('renders both no parent option and available namespaces list when available namespaces list is not empty', async () => {
- createComponent({
- group: generateFakeTableEntry({ id: 1, status: STATUSES.NONE }),
- });
- jest.advanceTimersByTime(DEBOUNCE_DELAY);
- await waitForPromises();
- await nextTick();
-
- const [firstItem, ...rest] = findNamespaceDropdown()
- .findAllComponents(GlDropdownItem)
- .wrappers.map((w) => w.text());
-
- expect(firstItem).toBe('No parent');
- expect(rest).toHaveLength(AVAILABLE_NAMESPACES.length);
- });
-
describe('when entity is not available for import', () => {
beforeEach(() => {
group = generateFakeTableEntry({
@@ -147,6 +116,7 @@ describe('import target cell', () => {
describe('when entity is available for import', () => {
const FAKE_PROGRESS_MESSAGE = 'progress message';
+
beforeEach(() => {
group = generateFakeTableEntry({
id: 1,
diff --git a/spec/frontend/integrations/gitlab_slack_application/components/projects_dropdown_spec.js b/spec/frontend/integrations/gitlab_slack_application/components/projects_dropdown_spec.js
new file mode 100644
index 00000000000..8879a86a578
--- /dev/null
+++ b/spec/frontend/integrations/gitlab_slack_application/components/projects_dropdown_spec.js
@@ -0,0 +1,54 @@
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import ProjectsDropdown from '~/integrations/gitlab_slack_application/components/projects_dropdown.vue';
+
+describe('Slack application projects dropdown', () => {
+ let wrapper;
+
+ const projectsMockData = [
+ {
+ avatar_url: null,
+ id: 1,
+ name: 'Gitlab Smoke Tests',
+ name_with_namespace: 'Toolbox / Gitlab Smoke Tests',
+ },
+ {
+ avatar_url: null,
+ id: 2,
+ name: 'Gitlab Test',
+ name_with_namespace: 'Gitlab Org / Gitlab Test',
+ },
+ {
+ avatar_url: 'foo/bar',
+ id: 3,
+ name: 'Gitlab Shell',
+ name_with_namespace: 'Gitlab Org / Gitlab Shell',
+ },
+ ];
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(ProjectsDropdown, {
+ propsData: {
+ projects: projectsMockData,
+ ...props,
+ },
+ });
+ };
+
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the listbox with 3 items', () => {
+ expect(findListbox().exists()).toBe(true);
+ expect(findListbox().props('items')).toHaveLength(3);
+ });
+
+ it('should emit project-selected if a project is clicked', () => {
+ findListbox().vm.$emit('select', 1);
+
+ expect(wrapper.emitted('project-selected')).toMatchObject([[projectsMockData[0]]]);
+ });
+});
diff --git a/spec/frontend/invite_members/components/invite_members_modal_spec.js b/spec/frontend/invite_members/components/invite_members_modal_spec.js
index 526487f6460..cfc2fd65cc1 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -34,6 +34,7 @@ import {
displaySuccessfulInvitationAlert,
reloadOnInvitationSuccess,
} from '~/invite_members/utils/trigger_successful_invite_alert';
+import { captureException } from '~/ci/runner/sentry_utils';
import { GROUPS_INVITATIONS_PATH, invitationsApiResponse } from '../mock_data/api_responses';
import {
propsData,
@@ -52,6 +53,7 @@ import {
jest.mock('~/invite_members/utils/trigger_successful_invite_alert');
jest.mock('~/experimentation/experiment_tracking');
+jest.mock('~/ci/runner/sentry_utils');
describe('InviteMembersModal', () => {
let wrapper;
@@ -130,10 +132,10 @@ describe('InviteMembersModal', () => {
const findUserLimitAlert = () => wrapper.findComponent(UserLimitNotification);
const findAccordion = () => wrapper.findComponent(GlCollapse);
const findErrorsIcon = () => wrapper.findComponent(GlIcon);
- const findMemberErrorMessage = (element) =>
- `${Object.keys(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]}: ${
- Object.values(invitationsApiResponse.EXPANDED_RESTRICTED.message)[element]
- }`;
+ const expectedErrorMessage = (index, errorType) => {
+ const [username, message] = Object.entries(errorType.parsedMessage)[index];
+ return `${username}: ${message}`;
+ };
const findActionButton = () => wrapper.findByTestId('invite-modal-submit');
const findCancelButton = () => wrapper.findByTestId('invite-modal-cancel');
@@ -315,8 +317,6 @@ describe('InviteMembersModal', () => {
mock.onPost(GROUPS_INVITATIONS_PATH).reply(code, data);
};
- const expectedEmailRestrictedError =
- "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.";
const expectedSyntaxError = 'email contains an invalid email address';
describe('when no invites have been entered in the form and then some are entered', () => {
@@ -447,10 +447,8 @@ describe('InviteMembersModal', () => {
});
it('displays the generic error for http server error', async () => {
- mockInvitationsApi(
- HTTP_STATUS_INTERNAL_SERVER_ERROR,
- 'Request failed with status code 500',
- );
+ const SERVER_ERROR_MESSAGE = 'Request failed with status code 500';
+ mockInvitationsApi(HTTP_STATUS_INTERNAL_SERVER_ERROR, SERVER_ERROR_MESSAGE);
clickInviteButton();
@@ -458,17 +456,25 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupInvalidFeedback()).toBe('Something went wrong');
expect(findMembersSelect().props('exceptionState')).toBe(false);
+ expect(captureException).toHaveBeenCalledWith({
+ error: new Error(SERVER_ERROR_MESSAGE),
+ component: wrapper.vm.$options.name,
+ });
});
it('displays the restricted user api message for response with bad request', async () => {
mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.EMAIL_RESTRICTED);
+ await triggerMembersTokenSelect([user3]);
+
clickInviteButton();
await waitForPromises();
expect(findMemberErrorAlert().exists()).toBe(true);
- expect(findMemberErrorAlert().text()).toContain(expectedEmailRestrictedError);
+ expect(findMemberErrorAlert().text()).toContain(
+ expectedErrorMessage(0, invitationsApiResponse.EMAIL_RESTRICTED),
+ );
expect(membersFormGroupInvalidFeedback()).toBe('');
expect(findMembersSelect().props('exceptionState')).not.toBe(false);
});
@@ -476,19 +482,21 @@ describe('InviteMembersModal', () => {
it('displays all errors when there are multiple existing users that are restricted by email', async () => {
mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
+ await triggerMembersTokenSelect([user3, user4, user5]);
+
clickInviteButton();
await waitForPromises();
expect(findMemberErrorAlert().exists()).toBe(true);
expect(findMemberErrorAlert().text()).toContain(
- Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[0],
+ expectedErrorMessage(0, invitationsApiResponse.MULTIPLE_RESTRICTED),
);
expect(findMemberErrorAlert().text()).toContain(
- Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[1],
+ expectedErrorMessage(1, invitationsApiResponse.MULTIPLE_RESTRICTED),
);
expect(findMemberErrorAlert().text()).toContain(
- Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[2],
+ expectedErrorMessage(2, invitationsApiResponse.MULTIPLE_RESTRICTED),
);
expect(membersFormGroupInvalidFeedback()).toBe('');
expect(findMembersSelect().props('exceptionState')).not.toBe(false);
@@ -608,7 +616,9 @@ describe('InviteMembersModal', () => {
await waitForPromises();
expect(findMemberErrorAlert().exists()).toBe(true);
- expect(findMemberErrorAlert().text()).toContain(expectedEmailRestrictedError);
+ expect(findMemberErrorAlert().text()).toContain(
+ expectedErrorMessage(0, invitationsApiResponse.EMAIL_RESTRICTED),
+ );
expect(membersFormGroupInvalidFeedback()).toBe('');
expect(findMembersSelect().props('exceptionState')).not.toBe(false);
expect(findActionButton().props('loading')).toBe(false);
@@ -617,19 +627,21 @@ describe('InviteMembersModal', () => {
it('displays all errors when there are multiple emails that return a restricted error message', async () => {
mockInvitationsApi(HTTP_STATUS_CREATED, invitationsApiResponse.MULTIPLE_RESTRICTED);
+ await triggerMembersTokenSelect([user3, user4, user5]);
+
clickInviteButton();
await waitForPromises();
expect(findMemberErrorAlert().exists()).toBe(true);
expect(findMemberErrorAlert().text()).toContain(
- Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[0],
+ expectedErrorMessage(0, invitationsApiResponse.MULTIPLE_RESTRICTED),
);
expect(findMemberErrorAlert().text()).toContain(
- Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[1],
+ expectedErrorMessage(1, invitationsApiResponse.MULTIPLE_RESTRICTED),
);
expect(findMemberErrorAlert().text()).toContain(
- Object.values(invitationsApiResponse.MULTIPLE_RESTRICTED.message)[2],
+ expectedErrorMessage(2, invitationsApiResponse.MULTIPLE_RESTRICTED),
);
expect(membersFormGroupInvalidFeedback()).toBe('');
expect(findMembersSelect().props('exceptionState')).not.toBe(false);
@@ -685,10 +697,18 @@ describe('InviteMembersModal', () => {
expect(findMemberErrorAlert().props('title')).toContain(
"The following 4 members couldn't be invited",
);
- expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(0));
- expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(1));
- expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(2));
- expect(findMemberErrorAlert().text()).toContain(findMemberErrorMessage(3));
+ expect(findMemberErrorAlert().text()).toContain(
+ expectedErrorMessage(0, invitationsApiResponse.EXPANDED_RESTRICTED),
+ );
+ expect(findMemberErrorAlert().text()).toContain(
+ expectedErrorMessage(1, invitationsApiResponse.EXPANDED_RESTRICTED),
+ );
+ expect(findMemberErrorAlert().text()).toContain(
+ expectedErrorMessage(2, invitationsApiResponse.EXPANDED_RESTRICTED),
+ );
+ expect(findMemberErrorAlert().text()).toContain(
+ expectedErrorMessage(3, invitationsApiResponse.EXPANDED_RESTRICTED),
+ );
expect(findAccordion().exists()).toBe(true);
expect(findMoreInviteErrorsButton().text()).toContain('Show more (2)');
expect(findErrorsIcon().attributes('class')).not.toContain('gl-rotate-180');
@@ -711,7 +731,9 @@ describe('InviteMembersModal', () => {
expect(findMemberErrorAlert().props('title')).toContain(
"The following 3 members couldn't be invited",
);
- expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(0));
+ expect(findMemberErrorAlert().text()).not.toContain(
+ expectedErrorMessage(0, invitationsApiResponse.EXPANDED_RESTRICTED),
+ );
await removeMembersToken(user6);
@@ -719,14 +741,18 @@ describe('InviteMembersModal', () => {
expect(findMemberErrorAlert().props('title')).toContain(
"The following 2 members couldn't be invited",
);
- expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(2));
+ expect(findMemberErrorAlert().text()).not.toContain(
+ expectedErrorMessage(2, invitationsApiResponse.EXPANDED_RESTRICTED),
+ );
await removeMembersToken(user4);
expect(findMemberErrorAlert().props('title')).toContain(
"The following member couldn't be invited",
);
- expect(findMemberErrorAlert().text()).not.toContain(findMemberErrorMessage(1));
+ expect(findMemberErrorAlert().text()).not.toContain(
+ expectedErrorMessage(1, invitationsApiResponse.EXPANDED_RESTRICTED),
+ );
await removeMembersToken(user5);
diff --git a/spec/frontend/invite_members/mock_data/api_responses.js b/spec/frontend/invite_members/mock_data/api_responses.js
index 4f773009f37..9190f85d7a0 100644
--- a/spec/frontend/invite_members/mock_data/api_responses.js
+++ b/spec/frontend/invite_members/mock_data/api_responses.js
@@ -6,36 +6,56 @@ const ERROR_EMAIL_INVALID = {
error: 'email contains an invalid email address',
};
+const BASE_ERROR_MEMBER_NOT_ALLOWED = `The member's email address is not allowed for this project. \
+Go to the &#39;Admin area &gt; Sign-up restrictions&#39;, and check`;
+
+const ALLOWED_DOMAIN_ERROR = `${BASE_ERROR_MEMBER_NOT_ALLOWED} &#39;Allowed domains for sign-ups&#39;.`;
+const DOMAIN_DENYLIST_ERROR = `${BASE_ERROR_MEMBER_NOT_ALLOWED} the &#39;Domain denylist&#39;.`;
+
+function htmlDecode(input) {
+ const doc = new DOMParser().parseFromString(input, 'text/html');
+ return doc.documentElement.textContent;
+}
+
+const DECODED_ALLOWED_DOMAIN_ERROR = htmlDecode(ALLOWED_DOMAIN_ERROR);
+const DECODED_DOMAIN_DENYLIST_ERROR = htmlDecode(DOMAIN_DENYLIST_ERROR);
+
const EMAIL_RESTRICTED = {
message: {
- 'email@example.com':
- "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
+ 'email@example.com': ALLOWED_DOMAIN_ERROR,
+ },
+ parsedMessage: {
+ 'email@example.com': DECODED_ALLOWED_DOMAIN_ERROR,
},
status: 'error',
};
const MULTIPLE_RESTRICTED = {
message: {
- 'email@example.com':
- "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
- 'email4@example.com':
- "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.",
- root:
- "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
+ 'email@example.com': ALLOWED_DOMAIN_ERROR,
+ 'email4@example.com': DOMAIN_DENYLIST_ERROR,
+ root: ALLOWED_DOMAIN_ERROR,
+ },
+ parsedMessage: {
+ 'email@example.com': DECODED_ALLOWED_DOMAIN_ERROR,
+ 'email4@example.com': DECODED_DOMAIN_DENYLIST_ERROR,
+ root: DECODED_ALLOWED_DOMAIN_ERROR,
},
status: 'error',
};
const EXPANDED_RESTRICTED = {
message: {
- 'email@example.com':
- "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
- 'email4@example.com':
- "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.",
- 'email5@example.com':
- "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check the Domain denylist.",
- root:
- "The member's email address is not allowed for this project. Go to the Admin area > Sign-up restrictions, and check Allowed domains for sign-ups.",
+ 'email@example.com': ALLOWED_DOMAIN_ERROR,
+ 'email4@example.com': DOMAIN_DENYLIST_ERROR,
+ 'email5@example.com': DOMAIN_DENYLIST_ERROR,
+ root: ALLOWED_DOMAIN_ERROR,
+ },
+ parsedMessage: {
+ 'email@example.com': DECODED_ALLOWED_DOMAIN_ERROR,
+ 'email4@example.com': DECODED_DOMAIN_DENYLIST_ERROR,
+ 'email5@example.com': DECODED_DOMAIN_DENYLIST_ERROR,
+ root: DECODED_ALLOWED_DOMAIN_ERROR,
},
status: 'error',
};
diff --git a/spec/frontend/issuable/components/hidden_badge_spec.js b/spec/frontend/issuable/components/hidden_badge_spec.js
new file mode 100644
index 00000000000..db2248bb2d2
--- /dev/null
+++ b/spec/frontend/issuable/components/hidden_badge_spec.js
@@ -0,0 +1,45 @@
+import { GlBadge, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import HiddenBadge from '~/issuable/components/hidden_badge.vue';
+
+describe('HiddenBadge component', () => {
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = shallowMount(HiddenBadge, {
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ propsData: {
+ issuableType: 'issue',
+ },
+ });
+ };
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findIcon = () => wrapper.findComponent(GlIcon);
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders warning badge', () => {
+ expect(findBadge().text()).toBe('Hidden');
+ expect(findBadge().props('variant')).toEqual('warning');
+ });
+
+ it('renders spam icon', () => {
+ expect(findIcon().props('name')).toBe('spam');
+ });
+
+ it('has tooltip', () => {
+ expect(getBinding(wrapper.element, 'gl-tooltip')).not.toBeUndefined();
+ });
+
+ it('has title', () => {
+ expect(findBadge().attributes('title')).toBe(
+ 'This issue is hidden because its author has been banned.',
+ );
+ });
+});
diff --git a/spec/frontend/issuable/components/locked_badge_spec.js b/spec/frontend/issuable/components/locked_badge_spec.js
new file mode 100644
index 00000000000..73ab6e36ba1
--- /dev/null
+++ b/spec/frontend/issuable/components/locked_badge_spec.js
@@ -0,0 +1,45 @@
+import { GlBadge, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import LockedBadge from '~/issuable/components/locked_badge.vue';
+
+describe('LockedBadge component', () => {
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = shallowMount(LockedBadge, {
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
+ propsData: {
+ issuableType: 'issue',
+ },
+ });
+ };
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findIcon = () => wrapper.findComponent(GlIcon);
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders warning badge', () => {
+ expect(findBadge().text()).toBe('Locked');
+ expect(findBadge().props('variant')).toEqual('warning');
+ });
+
+ it('renders lock icon', () => {
+ expect(findIcon().props('name')).toBe('lock');
+ });
+
+ it('has tooltip', () => {
+ expect(getBinding(wrapper.element, 'gl-tooltip')).not.toBeUndefined();
+ });
+
+ it('has title', () => {
+ expect(findBadge().attributes('title')).toBe(
+ 'This issue is locked. Only project members can comment.',
+ );
+ });
+});
diff --git a/spec/frontend/issues/dashboard/mock_data.js b/spec/frontend/issues/dashboard/mock_data.js
index 1e3abd5a018..adcd4268449 100644
--- a/spec/frontend/issues/dashboard/mock_data.js
+++ b/spec/frontend/issues/dashboard/mock_data.js
@@ -19,7 +19,6 @@ export const issuesQueryResponse = {
reference: 'group/project#123456',
state: 'opened',
title: 'Issue title',
- titleHtml: 'Issue title',
type: 'issue',
updatedAt: '2021-05-22T04:08:01Z',
upvotes: 3,
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 73fda11f38c..b9a8bc171db 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -49,7 +49,6 @@ export const getIssuesQueryResponse = {
moved: false,
state: 'opened',
title: 'Issue title',
- titleHtml: 'Issue title',
updatedAt: '2021-05-22T04:08:01Z',
closedAt: null,
upvotes: 3,
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 93860aaa925..25e89db7957 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -69,8 +69,8 @@ describe('Description component', () => {
wrapper = shallowMountExtended(Description, {
apolloProvider: mockApollo,
propsData: {
- issueId: 1,
- issueIid: 1,
+ issueId: '1',
+ issueIid: '1',
...initialProps,
...props,
},
diff --git a/spec/frontend/issues/show/components/fields/description_spec.js b/spec/frontend/issues/show/components/fields/description_spec.js
index 83b927d3699..e1d2809be9d 100644
--- a/spec/frontend/issues/show/components/fields/description_spec.js
+++ b/spec/frontend/issues/show/components/fields/description_spec.js
@@ -10,7 +10,7 @@ describe('Description field component', () => {
let trackingSpy;
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
- const mountComponent = ({ description = 'test', contentEditorOnIssues = false } = {}) => {
+ const mountComponent = ({ description = 'test' } = {}) => {
wrapper = shallowMount(DescriptionField, {
attachTo: document.body,
propsData: {
@@ -18,11 +18,6 @@ describe('Description field component', () => {
markdownDocsPath: '/',
value: description,
},
- provide: {
- glFeatures: {
- contentEditorOnIssues,
- },
- },
stubs: {
MarkdownField,
},
@@ -33,15 +28,7 @@ describe('Description field component', () => {
trackingSpy = mockTracking(undefined, null, jest.spyOn);
jest.spyOn(eventHub, '$emit');
- mountComponent({ contentEditorOnIssues: true });
- });
-
- it('passes feature flag to the MarkdownEditorComponent', () => {
- expect(findMarkdownEditor().props('enableContentEditor')).toBe(true);
-
- mountComponent({ contentEditorOnIssues: false });
-
- expect(findMarkdownEditor().props('enableContentEditor')).toBe(false);
+ mountComponent();
});
it('uses the MarkdownEditor component to edit markdown', () => {
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index ce2161f4670..e508045eff3 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -123,7 +123,7 @@ describe('HeaderActions component', () => {
const findMobileDropdownItems = () => findMobileDropdown().findAllComponents(GlDropdownItem);
const findDesktopDropdownItems = () => findDesktopDropdown().findAllComponents(GlDropdownItem);
const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
- const findReportAbuseSelectorItem = () => wrapper.find(`[data-testid="report-abuse-item"]`);
+ const findReportAbuseButton = () => wrapper.find(`[data-testid="report-abuse-item"]`);
const findNotificationWidget = () => wrapper.find(`[data-testid="notification-toggle"]`);
const findLockIssueWidget = () => wrapper.find(`[data-testid="lock-issue-toggle"]`);
const findCopyRefenceDropdownItem = () => wrapper.find(`[data-testid="copy-reference"]`);
@@ -239,24 +239,24 @@ describe('HeaderActions component', () => {
});
describe.each`
- description | isCloseIssueItemVisible | findDropdownItems | findDropdown
- ${'mobile dropdown'} | ${true} | ${findMobileDropdownItems} | ${findMobileDropdown}
- ${'desktop dropdown'} | ${false} | ${findDesktopDropdownItems} | ${findDesktopDropdown}
- `('$description', ({ isCloseIssueItemVisible, findDropdownItems, findDropdown }) => {
+ description | findDropdownItems
+ ${'mobile dropdown'} | ${findMobileDropdownItems}
+ ${'desktop dropdown'} | ${findDesktopDropdownItems}
+ `('$description', ({ findDropdownItems }) => {
describe.each`
- description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic | canDestroyIssue
- ${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${isCloseIssueItemVisible} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${`when user can create ${issueType}`} | ${`New related ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${`when user cannot create ${issueType}`} | ${`New related ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true} | ${true}
- ${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false} | ${true}
- ${'when user can report abuse'} | ${'Report abuse to administrator'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true} | ${true}
- ${'when user cannot report abuse'} | ${'Report abuse to administrator'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
- ${`when user can delete ${issueType}`} | ${`Delete ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
- ${`when user cannot delete ${issueType}`} | ${`Delete ${issueType}`} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${false}
+ description | itemText | isItemVisible | canUpdateIssue | canCreateIssue | isIssueAuthor | canReportSpam | canPromoteToEpic | canDestroyIssue
+ ${`when user can update ${issueType}`} | ${`Close ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user cannot update ${issueType}`} | ${`Close ${issueType}`} | ${false} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user can create ${issueType}`} | ${`New related ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user cannot create ${issueType}`} | ${`New related ${issueType}`} | ${false} | ${true} | ${false} | ${true} | ${true} | ${true} | ${true}
+ ${'when user can promote to epic'} | ${'Promote to epic'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${'when user cannot promote to epic'} | ${'Promote to epic'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${false} | ${true}
+ ${'when user can report abuse'} | ${'Report abuse to administrator'} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true} | ${true}
+ ${'when user cannot report abuse'} | ${'Report abuse to administrator'} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${'when user can submit as spam'} | ${'Submit as spam'} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${'when user cannot submit as spam'} | ${'Submit as spam'} | ${false} | ${true} | ${true} | ${true} | ${false} | ${true} | ${true}
+ ${`when user can delete ${issueType}`} | ${`Delete ${issueType}`} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true} | ${true}
+ ${`when user cannot delete ${issueType}`} | ${`Delete ${issueType}`} | ${false} | ${true} | ${true} | ${true} | ${true} | ${true} | ${false}
`(
'$description',
({
@@ -292,24 +292,6 @@ describe('HeaderActions component', () => {
});
},
);
-
- describe(`when user can update but not create ${issueType}`, () => {
- beforeEach(() => {
- wrapper = mountComponent({
- props: {
- canUpdateIssue: true,
- canCreateIssue: false,
- isIssueAuthor: true,
- issueType,
- canReportSpam: false,
- canPromoteToEpic: false,
- },
- });
- });
- it(`${isCloseIssueItemVisible ? 'shows' : 'hides'} the dropdown button`, () => {
- expect(findDropdown().exists()).toBe(isCloseIssueItemVisible);
- });
- });
});
describe(`show edit button ${issueType}`, () => {
@@ -346,7 +328,7 @@ describe('HeaderActions component', () => {
});
it('tracks clicking on button', () => {
- findDesktopDropdownItems().at(3).vm.$emit('click');
+ findDesktopDropdownItems().at(4).vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_dropdown', {
label: 'delete_issue',
@@ -490,29 +472,41 @@ describe('HeaderActions component', () => {
});
});
- describe('abuse category selector', () => {
+ describe('report abuse to admin button', () => {
beforeEach(() => {
wrapper = mountComponent({ props: { isIssueAuthor: false } });
});
- it("doesn't render", () => {
+ it('renders the button but not the abuse category drawer', () => {
+ expect(findReportAbuseButton().exists()).toBe(true);
expect(findAbuseCategorySelector().exists()).toEqual(false);
});
- it('opens the drawer', async () => {
- findReportAbuseSelectorItem().vm.$emit('click');
+ it('opens the abuse category drawer', async () => {
+ findReportAbuseButton().vm.$emit('click');
await nextTick();
expect(findAbuseCategorySelector().props('showDrawer')).toEqual(true);
});
- it('closes the drawer', async () => {
- await findReportAbuseSelectorItem().vm.$emit('click');
- await findAbuseCategorySelector().vm.$emit('close-drawer');
+ it('closes the abuse category drawer', async () => {
+ await findReportAbuseButton().vm.$emit('click');
+ expect(findAbuseCategorySelector().exists()).toEqual(true);
+ await findAbuseCategorySelector().vm.$emit('close-drawer');
expect(findAbuseCategorySelector().exists()).toEqual(false);
});
+
+ describe('when the logged in user is the issue author', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({ props: { isIssueAuthor: true } });
+ });
+
+ it('does not render the button', () => {
+ expect(findReportAbuseButton().exists()).toBe(false);
+ });
+ });
});
describe('notification toggle', () => {
@@ -694,7 +688,7 @@ describe('HeaderActions component', () => {
expect(findDesktopDropdown().exists()).toBe(headerActionsVisible);
expect(findCopyRefenceDropdownItem().exists()).toBe(headerActionsVisible);
expect(findNotificationWidget().exists()).toBe(false);
- expect(findReportAbuseSelectorItem().exists()).toBe(false);
+ expect(findReportAbuseButton().exists()).toBe(false);
expect(findLockIssueWidget().exists()).toBe(false);
});
},
@@ -720,7 +714,7 @@ describe('HeaderActions component', () => {
`${capitalizeFirstCharacter(expectedText)} actions`,
);
expect(findDropdownBy('copy-email').text()).toBe(`Copy ${expectedText} email address`);
- expect(findDesktopDropdownItems().at(0).text()).toBe(`New related ${expectedText}`);
+ expect(findDesktopDropdownItems().at(1).text()).toBe(`New related ${expectedText}`);
});
});
});
diff --git a/spec/frontend/issues/show/components/new_header_actions_popover_spec.js b/spec/frontend/issues/show/components/new_header_actions_popover_spec.js
deleted file mode 100644
index bf3e81c7d3a..00000000000
--- a/spec/frontend/issues/show/components/new_header_actions_popover_spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import { GlPopover } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import NewHeaderActionsPopover from '~/issues/show/components/new_header_actions_popover.vue';
-import { NEW_ACTIONS_POPOVER_KEY } from '~/issues/show/constants';
-import { TYPE_ISSUE } from '~/issues/constants';
-import * as utils from '~/lib/utils/common_utils';
-
-describe('NewHeaderActionsPopover', () => {
- let wrapper;
-
- const createComponent = ({ issueType = TYPE_ISSUE, movedMrSidebarEnabled = true }) => {
- wrapper = shallowMountExtended(NewHeaderActionsPopover, {
- propsData: {
- issueType,
- },
- stubs: {
- GlPopover,
- },
- provide: {
- glFeatures: {
- movedMrSidebar: movedMrSidebarEnabled,
- },
- },
- });
- };
-
- const findPopover = () => wrapper.findComponent(GlPopover);
- const findConfirmButton = () => wrapper.findByTestId('confirm-button');
-
- it('should not be visible when the feature flag :moved_mr_sidebar is disabled', () => {
- createComponent({ movedMrSidebarEnabled: false });
- expect(findPopover().exists()).toBe(false);
- });
-
- describe('without the popover cookie', () => {
- beforeEach(() => {
- utils.setCookie = jest.fn();
-
- createComponent({});
- });
-
- it('renders the popover with correct text', () => {
- expect(findPopover().exists()).toBe(true);
- expect(findPopover().text()).toContain('issue actions');
- });
-
- it('does not call setCookie', () => {
- expect(utils.setCookie).not.toHaveBeenCalled();
- });
-
- describe('when the confirm button is clicked', () => {
- beforeEach(() => {
- findConfirmButton().vm.$emit('click');
- });
-
- it('sets the popover cookie', () => {
- expect(utils.setCookie).toHaveBeenCalledWith(NEW_ACTIONS_POPOVER_KEY, true);
- });
-
- it('hides the popover', () => {
- expect(findPopover().exists()).toBe(false);
- });
- });
- });
-
- describe('with the popover cookie', () => {
- beforeEach(() => {
- jest.spyOn(utils, 'getCookie').mockReturnValue('true');
-
- createComponent({});
- });
-
- it('does not render the popover', () => {
- expect(findPopover().exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/issues/show/components/sticky_header_spec.js b/spec/frontend/issues/show/components/sticky_header_spec.js
index 0c54ae45e70..a909084956f 100644
--- a/spec/frontend/issues/show/components/sticky_header_spec.js
+++ b/spec/frontend/issues/show/components/sticky_header_spec.js
@@ -1,6 +1,7 @@
-import { GlIcon } from '@gitlab/ui';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import HiddenBadge from '~/issuable/components/hidden_badge.vue';
+import LockedBadge from '~/issuable/components/locked_badge.vue';
import {
issuableStatusText,
STATUS_CLOSED,
@@ -17,20 +18,17 @@ describe('StickyHeader component', () => {
let wrapper;
const findConfidentialBadge = () => wrapper.findComponent(ConfidentialityBadge);
- const findHiddenBadge = () => wrapper.findByTestId('hidden');
- const findLockedBadge = () => wrapper.findByTestId('locked');
+ const findHiddenBadge = () => wrapper.findComponent(HiddenBadge);
+ const findLockedBadge = () => wrapper.findComponent(LockedBadge);
+ const findTitle = () => wrapper.findComponent(GlLink);
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(StickyHeader, {
- directives: {
- GlTooltip: createMockDirective('gl-tooltip'),
- },
propsData: {
issuableStatus: STATUS_OPEN,
issuableType: TYPE_ISSUE,
show: true,
title: 'A sticky issue',
- titleHtml: '',
...props,
},
});
@@ -91,13 +89,6 @@ describe('StickyHeader component', () => {
const lockedBadge = findLockedBadge();
expect(lockedBadge.exists()).toBe(isLocked);
-
- if (isLocked) {
- expect(lockedBadge.attributes('title')).toBe(
- 'This issue is locked. Only project members can comment.',
- );
- expect(getBinding(lockedBadge.element, 'gl-tooltip')).not.toBeUndefined();
- }
});
it.each`
@@ -109,27 +100,13 @@ describe('StickyHeader component', () => {
const hiddenBadge = findHiddenBadge();
expect(hiddenBadge.exists()).toBe(isHidden);
-
- if (isHidden) {
- expect(hiddenBadge.attributes('title')).toBe(
- 'This issue is hidden because its author has been banned',
- );
- expect(getBinding(hiddenBadge.element, 'gl-tooltip')).not.toBeUndefined();
- }
});
it('shows with title', () => {
createComponent();
- const title = wrapper.find('a');
+ const title = findTitle();
expect(title.text()).toContain('A sticky issue');
expect(title.attributes('href')).toBe('#top');
});
-
- it('shows title containing markup', () => {
- const titleHtml = '<b>A sticky issue</b>';
- createComponent({ titleHtml });
-
- expect(wrapper.find('a').html()).toContain(titleHtml);
- });
});
diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js
index 37aa18ced8d..ed969a08ac5 100644
--- a/spec/frontend/issues/show/mock_data/mock_data.js
+++ b/spec/frontend/issues/show/mock_data/mock_data.js
@@ -1,9 +1,8 @@
import { TEST_HOST } from 'helpers/test_constants';
export const initialRequest = {
- title: '<gl-emoji title="party-parrot"></gl-emoji>this is a title',
+ title: '<p>this is a title</p>',
title_text: 'this is a title',
- title_html: '<gl-emoji title="party-parrot"></gl-emoji>this is a title',
description: '<p>this is a description!</p>',
description_text: 'this is a description',
task_completion_status: { completed_count: 2, count: 4 },
diff --git a/spec/frontend/lib/utils/global_alerts_spec.js b/spec/frontend/lib/utils/global_alerts_spec.js
new file mode 100644
index 00000000000..97fe427c281
--- /dev/null
+++ b/spec/frontend/lib/utils/global_alerts_spec.js
@@ -0,0 +1,80 @@
+import {
+ getGlobalAlerts,
+ setGlobalAlerts,
+ removeGlobalAlertById,
+ GLOBAL_ALERTS_SESSION_STORAGE_KEY,
+} from '~/lib/utils/global_alerts';
+
+describe('global alerts utils', () => {
+ describe('getGlobalAlerts', () => {
+ describe('when there are alerts', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(Storage.prototype, 'getItem')
+ .mockImplementation(() => '[{"id":"foo","variant":"danger","message":"Foo"}]');
+ });
+
+ it('returns alerts from session storage', () => {
+ expect(getGlobalAlerts()).toEqual([{ id: 'foo', variant: 'danger', message: 'Foo' }]);
+ });
+ });
+
+ describe('when there are no alerts', () => {
+ beforeEach(() => {
+ jest.spyOn(Storage.prototype, 'getItem').mockImplementation(() => null);
+ });
+
+ it('returns empty array', () => {
+ expect(getGlobalAlerts()).toEqual([]);
+ });
+ });
+ });
+});
+
+describe('setGlobalAlerts', () => {
+ it('sets alerts in session storage', () => {
+ const setItemSpy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {});
+
+ setGlobalAlerts([
+ {
+ id: 'foo',
+ variant: 'danger',
+ message: 'Foo',
+ },
+ {
+ id: 'bar',
+ variant: 'success',
+ message: 'Bar',
+ persistOnPages: ['dashboard:groups:index'],
+ dismissible: false,
+ },
+ ]);
+
+ expect(setItemSpy).toHaveBeenCalledWith(
+ GLOBAL_ALERTS_SESSION_STORAGE_KEY,
+ '[{"dismissible":true,"persistOnPages":[],"id":"foo","variant":"danger","message":"Foo"},{"dismissible":false,"persistOnPages":["dashboard:groups:index"],"id":"bar","variant":"success","message":"Bar"}]',
+ );
+ });
+});
+
+describe('removeGlobalAlertById', () => {
+ beforeEach(() => {
+ jest
+ .spyOn(Storage.prototype, 'getItem')
+ .mockImplementation(
+ () =>
+ '[{"id":"foo","variant":"success","message":"Foo"},{"id":"bar","variant":"danger","message":"Bar"}]',
+ );
+ });
+
+ it('removes alert', () => {
+ const setItemSpy = jest.spyOn(Storage.prototype, 'setItem').mockImplementation(() => {});
+
+ removeGlobalAlertById('bar');
+
+ expect(setItemSpy).toHaveBeenCalledWith(
+ GLOBAL_ALERTS_SESSION_STORAGE_KEY,
+ '[{"id":"foo","variant":"success","message":"Foo"}]',
+ );
+ });
+});
diff --git a/spec/frontend/lib/utils/url_utility_spec.js b/spec/frontend/lib/utils/url_utility_spec.js
index ecd2d7f888d..3a846bbda06 100644
--- a/spec/frontend/lib/utils/url_utility_spec.js
+++ b/spec/frontend/lib/utils/url_utility_spec.js
@@ -1,8 +1,20 @@
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import * as urlUtils from '~/lib/utils/url_utility';
+import { setGlobalAlerts } from '~/lib/utils/global_alerts';
import { safeUrls, unsafeUrls } from './mock_data';
+jest.mock('~/lib/utils/global_alerts', () => ({
+ getGlobalAlerts: jest.fn().mockImplementation(() => [
+ {
+ id: 'foo',
+ message: 'Foo',
+ variant: 'success',
+ },
+ ]),
+ setGlobalAlerts: jest.fn(),
+}));
+
const shas = {
valid: [
'ad9be38573f9ee4c4daec22673478c2dd1d81cd8',
@@ -327,6 +339,26 @@ describe('URL utility', () => {
});
});
+ describe('getLocationHash', () => {
+ it('gets a default empty value', () => {
+ setWindowLocation(TEST_HOST);
+
+ expect(urlUtils.getLocationHash()).toBeUndefined();
+ });
+
+ it('gets a value', () => {
+ setWindowLocation('#hash-value');
+
+ expect(urlUtils.getLocationHash()).toBe('hash-value');
+ });
+
+ it('gets an empty value when only hash is set', () => {
+ setWindowLocation('#');
+
+ expect(urlUtils.getLocationHash()).toBeUndefined();
+ });
+ });
+
describe('doesHashExistInUrl', () => {
beforeEach(() => {
setWindowLocation('#note_1');
@@ -462,6 +494,48 @@ describe('URL utility', () => {
});
});
+ describe('visitUrlWithAlerts', () => {
+ let originalLocation;
+
+ beforeAll(() => {
+ originalLocation = window.location;
+
+ Object.defineProperty(window, 'location', {
+ writable: true,
+ value: {
+ assign: jest.fn(),
+ protocol: 'http:',
+ host: TEST_HOST,
+ },
+ });
+ });
+
+ afterAll(() => {
+ window.location = originalLocation;
+ });
+
+ it('sets alerts and then visits url', () => {
+ const url = '/foo/bar';
+ const alert = {
+ id: 'bar',
+ message: 'Bar',
+ variant: 'danger',
+ };
+
+ urlUtils.visitUrlWithAlerts(url, [alert]);
+
+ expect(setGlobalAlerts).toHaveBeenCalledWith([
+ {
+ id: 'foo',
+ message: 'Foo',
+ variant: 'success',
+ },
+ alert,
+ ]);
+ expect(window.location.assign).toHaveBeenCalledWith(url);
+ });
+ });
+
describe('updateHistory', () => {
const state = { key: 'prop' };
const title = 'TITLE';
diff --git a/spec/frontend/merge_requests/components/header_metadata_spec.js b/spec/frontend/merge_requests/components/header_metadata_spec.js
deleted file mode 100644
index 2823b4b9d97..00000000000
--- a/spec/frontend/merge_requests/components/header_metadata_spec.js
+++ /dev/null
@@ -1,93 +0,0 @@
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import HeaderMetadata from '~/merge_requests/components/header_metadata.vue';
-import mrStore from '~/mr_notes/stores';
-import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
-
-jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores'));
-
-describe('HeaderMetadata component', () => {
- let wrapper;
-
- const findConfidentialIcon = () => wrapper.findComponent(ConfidentialityBadge);
- const findLockedIcon = () => wrapper.findByTestId('locked');
- const findHiddenIcon = () => wrapper.findByTestId('hidden');
-
- const renderTestMessage = (renders) => (renders ? 'renders' : 'does not render');
-
- const createComponent = ({ store, provide }) => {
- wrapper = shallowMountExtended(HeaderMetadata, {
- mocks: {
- $store: store,
- },
- provide,
- directives: {
- GlTooltip: createMockDirective('gl-tooltip'),
- },
- });
- };
-
- describe.each`
- lockStatus | confidentialStatus | hiddenStatus
- ${true} | ${true} | ${false}
- ${true} | ${false} | ${false}
- ${false} | ${true} | ${false}
- ${false} | ${false} | ${false}
- ${true} | ${true} | ${true}
- ${true} | ${false} | ${true}
- ${false} | ${true} | ${true}
- ${false} | ${false} | ${true}
- `(
- `when locked=$lockStatus, confidential=$confidentialStatus, and hidden=$hiddenStatus`,
- ({ lockStatus, confidentialStatus, hiddenStatus }) => {
- const store = mrStore;
-
- beforeEach(() => {
- store.getters.getNoteableData = {};
- store.getters.getNoteableData.confidential = confidentialStatus;
- store.getters.getNoteableData.discussion_locked = lockStatus;
- store.getters.getNoteableData.targetType = 'merge_request';
-
- createComponent({ store, provide: { hidden: hiddenStatus } });
- });
-
- it(`${renderTestMessage(lockStatus)} the locked icon`, () => {
- const lockedIcon = findLockedIcon();
-
- expect(lockedIcon.exists()).toBe(lockStatus);
-
- if (lockStatus) {
- expect(lockedIcon.attributes('title')).toBe(
- `This merge request is locked. Only project members can comment.`,
- );
- expect(getBinding(lockedIcon.element, 'gl-tooltip')).not.toBeUndefined();
- }
- });
-
- it(`${renderTestMessage(confidentialStatus)} the confidential icon`, () => {
- const confidentialIcon = findConfidentialIcon();
- expect(confidentialIcon.exists()).toBe(confidentialStatus);
-
- if (confidentialStatus && !hiddenStatus) {
- expect(confidentialIcon.props()).toMatchObject({
- workspaceType: 'project',
- issuableType: 'issue',
- });
- }
- });
-
- it(`${renderTestMessage(confidentialStatus)} the hidden icon`, () => {
- const hiddenIcon = findHiddenIcon();
-
- expect(hiddenIcon.exists()).toBe(hiddenStatus);
-
- if (hiddenStatus) {
- expect(hiddenIcon.attributes('title')).toBe(
- `This merge request is hidden because its author has been banned`,
- );
- expect(getBinding(hiddenIcon.element, 'gl-tooltip')).not.toBeUndefined();
- }
- });
- },
- );
-});
diff --git a/spec/frontend/merge_requests/components/merge_request_header_spec.js b/spec/frontend/merge_requests/components/merge_request_header_spec.js
new file mode 100644
index 00000000000..3f774098379
--- /dev/null
+++ b/spec/frontend/merge_requests/components/merge_request_header_spec.js
@@ -0,0 +1,88 @@
+import { shallowMount } from '@vue/test-utils';
+import HiddenBadge from '~/issuable/components/hidden_badge.vue';
+import LockedBadge from '~/issuable/components/locked_badge.vue';
+import StatusBadge from '~/issuable/components/status_badge.vue';
+import MergeRequestHeader from '~/merge_requests/components/merge_request_header.vue';
+import mrStore from '~/mr_notes/stores';
+import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
+
+jest.mock('~/mr_notes/stores', () => jest.requireActual('helpers/mocks/mr_notes/stores'));
+
+describe('MergeRequestHeader component', () => {
+ let wrapper;
+
+ const findConfidentialBadge = () => wrapper.findComponent(ConfidentialityBadge);
+ const findLockedBadge = () => wrapper.findComponent(LockedBadge);
+ const findHiddenBadge = () => wrapper.findComponent(HiddenBadge);
+ const findStatusBadge = () => wrapper.findComponent(StatusBadge);
+
+ const renderTestMessage = (renders) => (renders ? 'renders' : 'does not render');
+
+ const createComponent = ({ confidential, hidden, locked }) => {
+ const store = mrStore;
+ store.getters.getNoteableData = {};
+ store.getters.getNoteableData.confidential = confidential;
+ store.getters.getNoteableData.discussion_locked = locked;
+ store.getters.getNoteableData.targetType = 'merge_request';
+
+ wrapper = shallowMount(MergeRequestHeader, {
+ mocks: {
+ $store: store,
+ },
+ provide: {
+ hidden,
+ },
+ propsData: {
+ initialState: 'opened',
+ },
+ });
+ };
+
+ it('renders status badge', () => {
+ createComponent({ propsData: { initialState: 'opened' } });
+
+ expect(findStatusBadge().props()).toEqual({
+ issuableType: 'merge_request',
+ state: 'opened',
+ });
+ });
+
+ describe.each`
+ locked | confidential | hidden
+ ${true} | ${true} | ${false}
+ ${true} | ${false} | ${false}
+ ${false} | ${true} | ${false}
+ ${false} | ${false} | ${false}
+ ${true} | ${true} | ${true}
+ ${true} | ${false} | ${true}
+ ${false} | ${true} | ${true}
+ ${false} | ${false} | ${true}
+ `(
+ `when locked=$locked, confidential=$confidential, and hidden=$hidden`,
+ ({ locked, confidential, hidden }) => {
+ beforeEach(() => {
+ createComponent({ confidential, hidden, locked });
+ });
+
+ it(`${renderTestMessage(confidential)} the confidential badge`, () => {
+ const confidentialBadge = findConfidentialBadge();
+ expect(confidentialBadge.exists()).toBe(confidential);
+
+ if (confidential && !hidden) {
+ expect(confidentialBadge.props()).toMatchObject({
+ workspaceType: 'project',
+ issuableType: 'issue',
+ });
+ }
+ });
+
+ it(`${renderTestMessage(locked)} the locked badge`, () => {
+ expect(findLockedBadge().exists()).toBe(locked);
+ });
+
+ it(`${renderTestMessage(hidden)} the hidden badge`, () => {
+ expect(findHiddenBadge().exists()).toBe(hidden);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js
index 53dbd796d85..cd252560590 100644
--- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row_spec.js
@@ -2,15 +2,14 @@ import { shallowMount } from '@vue/test-utils';
import DetailRow from '~/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue';
describe('CandidateDetailRow', () => {
- const SECTION_LABEL_CELL = 0;
- const ROW_LABEL_CELL = 1;
- const ROW_VALUE_CELL = 2;
+ const ROW_LABEL_CELL = 0;
+ const ROW_VALUE_CELL = 1;
let wrapper;
const createWrapper = ({ slots = {} } = {}) => {
wrapper = shallowMount(DetailRow, {
- propsData: { sectionLabel: 'Section', label: 'Item' },
+ propsData: { label: 'Item' },
slots,
});
};
@@ -19,10 +18,6 @@ describe('CandidateDetailRow', () => {
beforeEach(() => createWrapper());
- it('renders section label', () => {
- expect(findCellAt(SECTION_LABEL_CELL).text()).toBe('Section');
- });
-
it('renders row label', () => {
expect(findCellAt(ROW_LABEL_CELL).text()).toBe('Item');
});
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
index 0b3b780cb3f..296728af46a 100644
--- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/ml_candidates_show_spec.js
@@ -1,32 +1,51 @@
-import { shallowMount } from '@vue/test-utils';
-import { GlAvatarLabeled, GlLink } from '@gitlab/ui';
+import { GlAvatarLabeled, GlLink, GlTableLite } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import MlCandidatesShow from '~/ml/experiment_tracking/routes/candidates/show';
import DetailRow from '~/ml/experiment_tracking/routes/candidates/show/components/candidate_detail_row.vue';
-import { TITLE_LABEL } from '~/ml/experiment_tracking/routes/candidates/show/translations';
+import {
+ TITLE_LABEL,
+ NO_PARAMETERS_MESSAGE,
+ NO_METRICS_MESSAGE,
+ NO_METADATA_MESSAGE,
+ NO_CI_MESSAGE,
+} from '~/ml/experiment_tracking/routes/candidates/show/translations';
import DeleteButton from '~/ml/experiment_tracking/components/delete_button.vue';
import ModelExperimentsHeader from '~/ml/experiment_tracking/components/model_experiments_header.vue';
+import { stubComponent } from 'helpers/stub_component';
import { newCandidate } from './mock_data';
describe('MlCandidatesShow', () => {
let wrapper;
const CANDIDATE = newCandidate();
- const USER_ROW = 6;
+ const USER_ROW = 1;
+
+ const INFO_SECTION = 0;
+ const CI_SECTION = 1;
+ const PARAMETER_SECTION = 2;
+ const METADATA_SECTION = 3;
const createWrapper = (createCandidate = () => CANDIDATE) => {
- wrapper = shallowMount(MlCandidatesShow, {
+ wrapper = shallowMountExtended(MlCandidatesShow, {
propsData: { candidate: createCandidate() },
+ stubs: {
+ GlTableLite: { ...stubComponent(GlTableLite), props: ['items', 'fields'] },
+ },
});
};
const findDeleteButton = () => wrapper.findComponent(DeleteButton);
const findHeader = () => wrapper.findComponent(ModelExperimentsHeader);
- const findNthDetailRow = (index) => wrapper.findAllComponents(DetailRow).at(index);
- const findLinkInNthDetailRow = (index) => findNthDetailRow(index).findComponent(GlLink);
- const findSectionLabel = (label) => wrapper.find(`[sectionLabel='${label}']`);
+ const findSection = (section) => wrapper.findAll('section').at(section);
+ const findRowInSection = (section, row) =>
+ findSection(section).findAllComponents(DetailRow).at(row);
+ const findLinkAtRow = (section, rowIndex) =>
+ findRowInSection(section, rowIndex).findComponent(GlLink);
+ const findNoDataMessage = (label) => wrapper.findByText(label);
const findLabel = (label) => wrapper.find(`[label='${label}']`);
- const findCiUserDetailRow = () => findNthDetailRow(USER_ROW);
+ const findCiUserDetailRow = () => findRowInSection(CI_SECTION, USER_ROW);
const findCiUserAvatar = () => findCiUserDetailRow().findComponent(GlAvatarLabeled);
const findCiUserAvatarNameLink = () => findCiUserAvatar().findComponent(GlLink);
+ const findMetricsTable = () => wrapper.findComponent(GlTableLite);
describe('Header', () => {
beforeEach(() => createWrapper());
@@ -50,42 +69,57 @@ describe('MlCandidatesShow', () => {
const mrText = `!${CANDIDATE.info.ci_job.merge_request.iid} ${CANDIDATE.info.ci_job.merge_request.title}`;
const expectedTable = [
- ['Info', 'ID', CANDIDATE.info.iid],
- ['', 'MLflow run ID', CANDIDATE.info.eid],
- ['', 'Status', CANDIDATE.info.status],
- ['', 'Experiment', CANDIDATE.info.experiment_name],
- ['', 'Artifacts', 'Artifacts'],
- ['CI', 'Job', CANDIDATE.info.ci_job.name],
- ['', 'Triggered by', 'CI User'],
- ['', 'Merge request', mrText],
- ['Parameters', CANDIDATE.params[0].name, CANDIDATE.params[0].value],
- ['', CANDIDATE.params[1].name, CANDIDATE.params[1].value],
- ['Metrics', CANDIDATE.metrics[0].name, CANDIDATE.metrics[0].value],
- ['', CANDIDATE.metrics[1].name, CANDIDATE.metrics[1].value],
- ['Metadata', CANDIDATE.metadata[0].name, CANDIDATE.metadata[0].value],
- ['', CANDIDATE.metadata[1].name, CANDIDATE.metadata[1].value],
- ].map((row, index) => [index, ...row]);
-
- it.each(expectedTable)(
- 'row %s is created correctly',
- (rowIndex, sectionLabel, label, text) => {
- const row = findNthDetailRow(rowIndex);
-
- expect(row.props()).toMatchObject({ sectionLabel, label });
- expect(row.text()).toBe(text);
- },
- );
+ [INFO_SECTION, 0, 'ID', CANDIDATE.info.iid],
+ [INFO_SECTION, 1, 'MLflow run ID', CANDIDATE.info.eid],
+ [INFO_SECTION, 2, 'Status', CANDIDATE.info.status],
+ [INFO_SECTION, 3, 'Experiment', CANDIDATE.info.experiment_name],
+ [INFO_SECTION, 4, 'Artifacts', 'Artifacts'],
+ [CI_SECTION, 0, 'Job', CANDIDATE.info.ci_job.name],
+ [CI_SECTION, 1, 'Triggered by', 'CI User'],
+ [CI_SECTION, 2, 'Merge request', mrText],
+ [PARAMETER_SECTION, 0, CANDIDATE.params[0].name, CANDIDATE.params[0].value],
+ [PARAMETER_SECTION, 1, CANDIDATE.params[1].name, CANDIDATE.params[1].value],
+ [METADATA_SECTION, 0, CANDIDATE.metadata[0].name, CANDIDATE.metadata[0].value],
+ [METADATA_SECTION, 1, CANDIDATE.metadata[1].name, CANDIDATE.metadata[1].value],
+ ];
+
+ it.each(expectedTable)('row %s is created correctly', (section, rowIndex, label, text) => {
+ const row = findRowInSection(section, rowIndex);
+
+ expect(row.props()).toMatchObject({ label });
+ expect(row.text()).toBe(text);
+ });
describe('Table links', () => {
const linkRows = [
- [3, CANDIDATE.info.path_to_experiment],
- [4, CANDIDATE.info.path_to_artifact],
- [5, CANDIDATE.info.ci_job.path],
- [7, CANDIDATE.info.ci_job.merge_request.path],
+ [INFO_SECTION, 3, CANDIDATE.info.path_to_experiment],
+ [INFO_SECTION, 4, CANDIDATE.info.path_to_artifact],
+ [CI_SECTION, 0, CANDIDATE.info.ci_job.path],
+ [CI_SECTION, 2, CANDIDATE.info.ci_job.merge_request.path],
];
- it.each(linkRows)('row %s is created correctly', (rowIndex, href) => {
- expect(findLinkInNthDetailRow(rowIndex).attributes().href).toBe(href);
+ it.each(linkRows)('row %s is created correctly', (section, rowIndex, href) => {
+ expect(findLinkAtRow(section, rowIndex).attributes().href).toBe(href);
+ });
+ });
+
+ describe('Metrics table', () => {
+ it('computes metrics table items correctly', () => {
+ expect(findMetricsTable().props('items')).toEqual([
+ { name: 'AUC', 0: '.55' },
+ { name: 'Accuracy', 1: '.99', 2: '.98', 3: '.97' },
+ { name: 'F1', 3: '.1' },
+ ]);
+ });
+
+ it('computes metrics table fields correctly', () => {
+ expect(findMetricsTable().props('fields')).toEqual([
+ expect.objectContaining({ key: 'name', label: 'Metric' }),
+ expect.objectContaining({ key: '0', label: 'Step 0' }),
+ expect.objectContaining({ key: '1', label: 'Step 1' }),
+ expect.objectContaining({ key: '2', label: 'Step 2' }),
+ expect.objectContaining({ key: '3', label: 'Step 3' }),
+ ]);
});
});
@@ -105,22 +139,6 @@ describe('MlCandidatesShow', () => {
expect(nameLink.text()).toEqual('CI User');
});
});
-
- it('does not render params', () => {
- expect(findSectionLabel('Parameters').exists()).toBe(true);
- });
-
- it('renders all conditional rows', () => {
- // This is a bit of a duplicated test from the above table test, but having this makes sure that the
- // tests that test the negatives are implemented correctly
- expect(findLabel('Artifacts').exists()).toBe(true);
- expect(findSectionLabel('Parameters').exists()).toBe(true);
- expect(findSectionLabel('Metadata').exists()).toBe(true);
- expect(findSectionLabel('Metrics').exists()).toBe(true);
- expect(findSectionLabel('CI').exists()).toBe(true);
- expect(findLabel('Merge request').exists()).toBe(true);
- expect(findLabel('Triggered by').exists()).toBe(true);
- });
});
describe('No artifact path', () => {
@@ -150,19 +168,19 @@ describe('MlCandidatesShow', () => {
);
it('does not render params', () => {
- expect(findSectionLabel('Parameters').exists()).toBe(false);
+ expect(findNoDataMessage(NO_PARAMETERS_MESSAGE).exists()).toBe(true);
});
it('does not render metadata', () => {
- expect(findSectionLabel('Metadata').exists()).toBe(false);
+ expect(findNoDataMessage(NO_METADATA_MESSAGE).exists()).toBe(true);
});
it('does not render metrics', () => {
- expect(findSectionLabel('Metrics').exists()).toBe(false);
+ expect(findNoDataMessage(NO_METRICS_MESSAGE).exists()).toBe(true);
});
it('does not render CI info', () => {
- expect(findSectionLabel('CI').exists()).toBe(false);
+ expect(findNoDataMessage(NO_CI_MESSAGE).exists()).toBe(true);
});
});
diff --git a/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js b/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js
index 3fbcf122997..4ea23ed2513 100644
--- a/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js
+++ b/spec/frontend/ml/experiment_tracking/routes/candidates/show/mock_data.js
@@ -4,8 +4,11 @@ export const newCandidate = () => ({
{ name: 'MaxDepth', value: '3' },
],
metrics: [
- { name: 'AUC', value: '.55' },
- { name: 'Accuracy', value: '.99' },
+ { name: 'AUC', value: '.55', step: 0 },
+ { name: 'Accuracy', value: '.99', step: 1 },
+ { name: 'Accuracy', value: '.98', step: 2 },
+ { name: 'Accuracy', value: '.97', step: 3 },
+ { name: 'F1', value: '.1', step: 3 },
],
metadata: [
{ name: 'FileName', value: 'test.py' },
diff --git a/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js b/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js
new file mode 100644
index 00000000000..57a5a5f003f
--- /dev/null
+++ b/spec/frontend/ml/model_registry/apps/show_ml_model_spec.js
@@ -0,0 +1,15 @@
+import { shallowMount } from '@vue/test-utils';
+import { ShowMlModel } from '~/ml/model_registry/apps';
+import { MODEL } from '../mock_data';
+
+let wrapper;
+const createWrapper = () => {
+ wrapper = shallowMount(ShowMlModel, { propsData: { model: MODEL } });
+};
+
+describe('ShowMlModel', () => {
+ beforeEach(() => createWrapper());
+ it('renders the app', () => {
+ expect(wrapper.text()).toContain(MODEL.name);
+ });
+});
diff --git a/spec/frontend/ml/model_registry/mock_data.js b/spec/frontend/ml/model_registry/mock_data.js
new file mode 100644
index 00000000000..18b2b32e069
--- /dev/null
+++ b/spec/frontend/ml/model_registry/mock_data.js
@@ -0,0 +1 @@
+export const MODEL = { name: 'blah' };
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js b/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
index d1715ccd8f1..c1b9aef9634 100644
--- a/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
+++ b/spec/frontend/ml/model_registry/routes/models/index/components/ml_models_index_spec.js
@@ -1,39 +1,63 @@
-import { GlLink } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import MlModelsIndexApp from '~/ml/model_registry/routes/models/index';
-import { TITLE_LABEL } from '~/ml/model_registry/routes/models/index/translations';
-import { mockModels } from './mock_data';
+import ModelRow from '~/ml/model_registry/routes/models/index/components/model_row.vue';
+import { TITLE_LABEL, NO_MODELS_LABEL } from '~/ml/model_registry/routes/models/index/translations';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+import { mockModels, startCursor, defaultPageInfo } from './mock_data';
let wrapper;
-const createWrapper = (models = mockModels) => {
- wrapper = shallowMountExtended(MlModelsIndexApp, {
- propsData: { models },
- });
+const createWrapper = (propsData = { models: mockModels, pageInfo: defaultPageInfo }) => {
+ wrapper = shallowMountExtended(MlModelsIndexApp, { propsData });
};
-const findModelLink = (index) => wrapper.findAllComponents(GlLink).at(index);
-const modelLinkText = (index) => findModelLink(index).text();
-const modelLinkHref = (index) => findModelLink(index).attributes('href');
+const findModelRow = (index) => wrapper.findAllComponents(ModelRow).at(index);
+const findPagination = () => wrapper.findComponent(Pagination);
const findTitle = () => wrapper.findByText(TITLE_LABEL);
+const findEmptyLabel = () => wrapper.findByText(NO_MODELS_LABEL);
describe('MlModelsIndex', () => {
- beforeEach(() => {
- createWrapper();
- });
+ describe('empty state', () => {
+ beforeEach(() => createWrapper({ models: [], pageInfo: defaultPageInfo }));
+
+ it('displays empty state when no experiment', () => {
+ expect(findEmptyLabel().exists()).toBe(true);
+ });
- describe('header', () => {
- it('displays the title', () => {
- expect(findTitle().exists()).toBe(true);
+ it('does not show pagination', () => {
+ expect(findPagination().exists()).toBe(false);
});
});
- describe('model list', () => {
- it('displays the models', () => {
- expect(modelLinkHref(0)).toBe(mockModels[0].path);
- expect(modelLinkText(0)).toBe(`${mockModels[0].name} / ${mockModels[0].version}`);
+ describe('with data', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('does not show empty state', () => {
+ expect(findEmptyLabel().exists()).toBe(false);
+ });
+
+ describe('header', () => {
+ it('displays the title', () => {
+ expect(findTitle().exists()).toBe(true);
+ });
+ });
+
+ describe('model list', () => {
+ it('displays the models', () => {
+ expect(findModelRow(0).props('model')).toMatchObject(mockModels[0]);
+ expect(findModelRow(1).props('model')).toMatchObject(mockModels[1]);
+ });
+ });
+
+ describe('pagination', () => {
+ it('should show', () => {
+ expect(findPagination().exists()).toBe(true);
+ });
- expect(modelLinkHref(1)).toBe(mockModels[1].path);
- expect(modelLinkText(1)).toBe(`${mockModels[1].name} / ${mockModels[1].version}`);
+ it('passes pagination to pagination component', () => {
+ expect(findPagination().props('startCursor')).toBe(startCursor);
+ });
});
});
});
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js b/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js
index b8a999abbbd..841a543606f 100644
--- a/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js
+++ b/spec/frontend/ml/model_registry/routes/models/index/components/mock_data.js
@@ -3,10 +3,27 @@ export const mockModels = [
name: 'model_1',
version: '1.0',
path: 'path/to/model_1',
+ versionCount: 3,
},
{
name: 'model_2',
- version: '1.0',
+ version: '1.1',
path: 'path/to/model_2',
+ versionCount: 1,
},
];
+
+export const modelWithoutVersion = {
+ name: 'model_without_version',
+ path: 'path/to/model_without_version',
+ versionCount: 0,
+};
+
+export const startCursor = 'eyJpZCI6IjE2In0';
+
+export const defaultPageInfo = Object.freeze({
+ startCursor,
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+});
diff --git a/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js b/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js
new file mode 100644
index 00000000000..7600288f560
--- /dev/null
+++ b/spec/frontend/ml/model_registry/routes/models/index/components/model_row_spec.js
@@ -0,0 +1,42 @@
+import { GlLink } from '@gitlab/ui';
+import {
+ mockModels,
+ modelWithoutVersion,
+} from 'jest/ml/model_registry/routes/models/index/components/mock_data';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ModelRow from '~/ml/model_registry/routes/models/index/components/model_row.vue';
+
+let wrapper;
+const createWrapper = (model = mockModels[0]) => {
+ wrapper = shallowMountExtended(ModelRow, { propsData: { model } });
+};
+
+const findLink = () => wrapper.findComponent(GlLink);
+const findMessage = (message) => wrapper.findByText(message);
+
+describe('ModelRow', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('Has a link to the model', () => {
+ expect(findLink().text()).toBe(mockModels[0].name);
+ expect(findLink().attributes('href')).toBe(mockModels[0].path);
+ });
+
+ it('Shows the latest version and the version count', () => {
+ expect(findMessage('1.0 · 3 versions').exists()).toBe(true);
+ });
+
+ it('Shows the latest version and no version count if it has only 1 version', () => {
+ createWrapper(mockModels[1]);
+
+ expect(findMessage('1.1 · No other versions').exists()).toBe(true);
+ });
+
+ it('Shows no version message if model has no versions', () => {
+ createWrapper(modelWithoutVersion);
+
+ expect(findMessage('No registered versions').exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 9b1678c0a8a..1309fd79c14 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -5,6 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
@@ -34,7 +35,6 @@ describe('issue_comment_form component', () => {
useLocalStorageSpy();
let trackingSpy;
- let store;
let wrapper;
let axiosMock;
@@ -48,21 +48,7 @@ describe('issue_comment_form component', () => {
const findCommentButton = () => findCommentTypeDropdown().find('button');
const findErrorAlerts = () => wrapper.findAllComponents(GlAlert).wrappers;
- async function clickCommentButton({ waitForComponent = true, waitForNetwork = true } = {}) {
- findCommentButton().trigger('click');
-
- if (waitForComponent || waitForNetwork) {
- // Wait for the click to bubble out and trigger the handler
- await nextTick();
-
- if (waitForNetwork) {
- // Wait for the network request promise to resolve
- await nextTick();
- }
- }
- }
-
- function createStore({ actions = {} } = {}) {
+ const createStore = ({ actions = {}, state = {} } = {}) => {
const baseModule = notesModule();
return new Vuex.Store({
@@ -71,8 +57,12 @@ describe('issue_comment_form component', () => {
...baseModule.actions,
...actions,
},
+ state: {
+ ...baseModule.state,
+ ...state,
+ },
});
- }
+ };
const createNotableDataMock = (data = {}) => {
return {
@@ -105,6 +95,7 @@ describe('issue_comment_form component', () => {
userData = userDataMock,
features = {},
mountFunction = shallowMount,
+ store = createStore(),
} = {}) => {
store.dispatch('setNoteableData', noteableData);
store.dispatch('setNotesData', notesData);
@@ -139,7 +130,6 @@ describe('issue_comment_form component', () => {
beforeEach(() => {
axiosMock = new MockAdapter(axios);
- store = createStore();
trackingSpy = mockTracking(undefined, null, jest.spyOn);
});
@@ -149,25 +139,32 @@ describe('issue_comment_form component', () => {
describe('user is logged in', () => {
describe('handleSave', () => {
- it('should request to save note when note is entered', () => {
- mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
-
- jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
-
- findCloseReopenButton().trigger('click');
+ const note = 'hello world';
- expect(wrapper.vm.isSubmitting).toBe(true);
- expect(wrapper.vm.note).toBe('');
- expect(wrapper.vm.saveNote).toHaveBeenCalled();
+ it('should request to save note when note is entered', async () => {
+ const saveNoteSpy = jest.fn();
+ const store = createStore({
+ actions: {
+ saveNote: saveNoteSpy,
+ },
+ });
+ mountComponent({ mountFunction: mount, initialData: { note }, store });
+ expect(findCloseReopenButton().props('disabled')).toBe(false);
+ expect(findMarkdownEditor().props('value')).toBe(note);
+ await findCloseReopenButton().trigger('click');
+ expect(findCloseReopenButton().props('disabled')).toBe(true);
+ expect(findMarkdownEditor().props('value')).toBe('');
+ expect(saveNoteSpy).toHaveBeenCalled();
});
- it('tracks event', () => {
- mountComponent({ mountFunction: mount, initialData: { note: 'hello world' } });
-
- jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
-
- findCloseReopenButton().trigger('click');
-
+ it('tracks event', async () => {
+ const store = createStore({
+ actions: {
+ saveNote: jest.fn().mockResolvedValue(),
+ },
+ });
+ mountComponent({ mountFunction: mount, initialData: { note }, store });
+ await findCloseReopenButton().trigger('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'save_markdown', {
label: 'markdown_editor',
property: 'Issue_comment',
@@ -175,12 +172,13 @@ describe('issue_comment_form component', () => {
});
it('does not report errors in the UI when the save succeeds', async () => {
- mountComponent({ mountFunction: mount, initialData: { note: '/label ~sdfghj' } });
-
- jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
-
- await clickCommentButton();
-
+ const store = createStore({
+ actions: {
+ saveNote: jest.fn().mockResolvedValue(),
+ },
+ });
+ mountComponent({ mountFunction: mount, initialData: { note: '/label ~sdfghj' }, store });
+ await findCommentButton().trigger('click');
// findErrorAlerts().exists returns false if *any* wrapper is empty,
// not necessarily that there aren't any at all.
// We want to check here that there are none found, so we use the
@@ -197,20 +195,17 @@ describe('issue_comment_form component', () => {
`(
'displays the correct errors ($errors) for a $httpStatus network response',
async ({ errors, httpStatus }) => {
- store = createStore({
+ const store = createStore({
actions: {
saveNote: jest.fn().mockRejectedValue({
response: { status: httpStatus, data: { errors: { commands_only: errors } } },
}),
},
});
-
- mountComponent({ mountFunction: mount, initialData: { note: '/label ~sdfghj' } });
-
- await clickCommentButton();
-
+ mountComponent({ mountFunction: mount, initialData: { note: '/label ~sdfghj' }, store });
+ await findCommentButton().trigger('click');
+ await waitForPromises();
const errorAlerts = findErrorAlerts();
-
expect(errorAlerts.length).toBe(errors.length);
errors.forEach((msg, index) => {
const alert = errorAlerts[index];
@@ -222,7 +217,7 @@ describe('issue_comment_form component', () => {
describe('if response contains validation errors', () => {
beforeEach(() => {
- store = createStore({
+ const store = createStore({
actions: {
saveNote: jest.fn().mockRejectedValue({
response: {
@@ -233,9 +228,9 @@ describe('issue_comment_form component', () => {
},
});
- mountComponent({ mountFunction: mount, initialData: { note: 'invalid note' } });
+ mountComponent({ mountFunction: mount, initialData: { note: 'invalid note' }, store });
- clickCommentButton();
+ findCommentButton().trigger('click');
});
it('renders an error message', () => {
@@ -251,7 +246,7 @@ describe('issue_comment_form component', () => {
it('should remove the correct error from the list when it is dismissed', async () => {
const commandErrors = ['1', '2', '3'];
- store = createStore({
+ const store = createStore({
actions: {
saveNote: jest.fn().mockRejectedValue({
response: {
@@ -261,10 +256,9 @@ describe('issue_comment_form component', () => {
}),
},
});
-
- mountComponent({ mountFunction: mount, initialData: { note: '/label ~sdfghj' } });
-
- await clickCommentButton();
+ mountComponent({ mountFunction: mount, initialData: { note: '/label ~sdfghj' }, store });
+ await findCommentButton().trigger('click');
+ await waitForPromises();
let errorAlerts = findErrorAlerts();
@@ -314,15 +308,8 @@ describe('issue_comment_form component', () => {
});
});
- it('hides content editor switcher if feature flag content_editor_on_issues is off', () => {
- mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: false } });
-
- expect(wrapper.text()).not.toContain('Switch to rich text editing');
- });
-
- it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
- mountComponent({ mountFunction: mount, features: { contentEditorOnIssues: true } });
-
+ it('shows content editor switcher', () => {
+ mountComponent({ mountFunction: mount });
expect(wrapper.text()).toContain('Switch to rich text editing');
});
@@ -335,11 +322,8 @@ describe('issue_comment_form component', () => {
`(
'should render textarea with placeholder for $noteType',
async ({ noteIsInternal, placeholder }) => {
- mountComponent();
-
- wrapper.vm.noteIsInternal = noteIsInternal;
- await nextTick();
-
+ await mountComponent();
+ await findConfidentialNoteCheckbox().vm.$emit('input', noteIsInternal);
expect(findMarkdownEditor().props('formFieldProps').placeholder).toBe(placeholder);
},
);
@@ -371,25 +355,20 @@ describe('issue_comment_form component', () => {
expect(wrapper.find(`[href="${markdownDocsPath}"]`).exists()).toBe(true);
});
- it('should resize textarea after note discarded', async () => {
- mountComponent({ mountFunction: mount, initialData: { note: 'foo' } });
-
- jest.spyOn(wrapper.vm, 'discard');
-
- wrapper.vm.discard();
-
- await nextTick();
-
+ it('should resize textarea after note is saved', async () => {
+ const store = createStore();
+ store.registerModule('batchComments', batchComments());
+ store.state.batchComments.drafts = [{ note: 'A' }];
+ await mountComponent({ mountFunction: mount, initialData: { note: 'foo' }, store });
+ await findAddCommentNowButton().trigger('click');
+ await waitForPromises();
expect(Autosize.update).toHaveBeenCalled();
});
});
describe('edit mode', () => {
- beforeEach(() => {
- mountComponent({ mountFunction: mount });
- });
-
it('should enter edit mode when arrow up is pressed', () => {
+ mountComponent({ mountFunction: mount });
jest.spyOn(wrapper.vm, 'editCurrentUserLastNote');
findMarkdownEditorTextarea().trigger('keydown.up');
@@ -400,6 +379,7 @@ describe('issue_comment_form component', () => {
describe('event enter', () => {
describe('when no draft exists', () => {
it('should save note when cmd+enter is pressed', () => {
+ mountComponent({ mountFunction: mount });
jest.spyOn(wrapper.vm, 'handleSave');
findMarkdownEditorTextarea().trigger('keydown.enter', { metaKey: true });
@@ -408,6 +388,7 @@ describe('issue_comment_form component', () => {
});
it('should save note when ctrl+enter is pressed', () => {
+ mountComponent({ mountFunction: mount });
jest.spyOn(wrapper.vm, 'handleSave');
findMarkdownEditorTextarea().trigger('keydown.enter', { ctrlKey: true });
@@ -417,24 +398,25 @@ describe('issue_comment_form component', () => {
});
describe('when a draft exists', () => {
+ let store;
+
beforeEach(() => {
+ store = createStore();
store.registerModule('batchComments', batchComments());
store.state.batchComments.drafts = [{ note: 'A' }];
});
- it('should save note draft when cmd+enter is pressed', () => {
+ it('should save note draft when cmd+enter is pressed', async () => {
+ mountComponent({ mountFunction: mount, store });
jest.spyOn(wrapper.vm, 'handleSaveDraft');
-
- findMarkdownEditorTextarea().trigger('keydown.enter', { metaKey: true });
-
+ await findMarkdownEditorTextarea().trigger('keydown.enter', { metaKey: true });
expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
});
- it('should save note draft when ctrl+enter is pressed', () => {
+ it('should save note draft when ctrl+enter is pressed', async () => {
+ mountComponent({ mountFunction: mount, store });
jest.spyOn(wrapper.vm, 'handleSaveDraft');
-
- findMarkdownEditorTextarea().trigger('keydown.enter', { ctrlKey: true });
-
+ await findMarkdownEditorTextarea().trigger('keydown.enter', { ctrlKey: true });
expect(wrapper.vm.handleSaveDraft).toHaveBeenCalledWith();
});
});
@@ -706,7 +688,7 @@ describe('issue_comment_form component', () => {
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
- clickCommentButton();
+ findCommentButton().trigger('click');
expect(wrapper.vm.saveNote).not.toHaveBeenCalled();
});
@@ -719,7 +701,7 @@ describe('issue_comment_form component', () => {
jest.spyOn(wrapper.vm, 'saveNote').mockResolvedValue();
- clickCommentButton();
+ findCommentButton().trigger('click');
expect(wrapper.vm.saveNote).toHaveBeenCalled();
});
@@ -740,14 +722,16 @@ describe('issue_comment_form component', () => {
});
describe('with batchComments in store', () => {
- beforeEach(() => {
- store.registerModule('batchComments', batchComments());
- });
-
describe('add to review and comment now buttons', () => {
- it('when no drafts exist, should not render', () => {
- mountComponent();
+ let store;
+
+ beforeEach(() => {
+ store = createStore();
+ store.registerModule('batchComments', batchComments());
+ });
+ it('when no drafts exist, should not render', () => {
+ mountComponent({ store });
expect(findCommentTypeDropdown().exists()).toBe(true);
expect(findAddToReviewButton().exists()).toBe(false);
expect(findAddCommentNowButton().exists()).toBe(false);
@@ -758,20 +742,17 @@ describe('issue_comment_form component', () => {
store.state.batchComments.drafts = [{ note: 'A' }];
});
- it('should render', () => {
- mountComponent();
-
+ it('should render', async () => {
+ await mountComponent({ store });
expect(findCommentTypeDropdown().exists()).toBe(false);
expect(findAddToReviewButton().exists()).toBe(true);
expect(findAddCommentNowButton().exists()).toBe(true);
});
- it('clicking `add to review`, should call draft endpoint, set `isDraft` true', () => {
- mountComponent({ mountFunction: mount, initialData: { note: 'a draft note' } });
-
+ it('clicking `add to review`, should call draft endpoint, set `isDraft` true', async () => {
+ mountComponent({ mountFunction: mount, initialData: { note: 'a draft note' }, store });
jest.spyOn(store, 'dispatch').mockResolvedValue();
- findAddToReviewButton().trigger('click');
-
+ await findAddToReviewButton().trigger('click');
expect(store.dispatch).toHaveBeenCalledWith(
'saveNote',
expect.objectContaining({
@@ -781,12 +762,10 @@ describe('issue_comment_form component', () => {
);
});
- it('clicking `add comment now`, should call note endpoint, set `isDraft` false', () => {
- mountComponent({ mountFunction: mount, initialData: { note: 'a comment' } });
-
+ it('clicking `add comment now`, should call note endpoint, set `isDraft` false', async () => {
+ await mountComponent({ mountFunction: mount, initialData: { note: 'a comment' }, store });
jest.spyOn(store, 'dispatch').mockResolvedValue();
- findAddCommentNowButton().trigger('click');
-
+ await findAddCommentNowButton().trigger('click');
expect(store.dispatch).toHaveBeenCalledWith(
'saveNote',
expect.objectContaining({
diff --git a/spec/frontend/notes/components/email_participants_warning_spec.js b/spec/frontend/notes/components/email_participants_warning_spec.js
index 34b7524d8fb..620c753e3c5 100644
--- a/spec/frontend/notes/components/email_participants_warning_spec.js
+++ b/spec/frontend/notes/components/email_participants_warning_spec.js
@@ -1,10 +1,12 @@
import { mount } from '@vue/test-utils';
+import { GlButton } from '@gitlab/ui';
+
import EmailParticipantsWarning from '~/notes/components/email_participants_warning.vue';
describe('Email Participants Warning Component', () => {
let wrapper;
- const findMoreButton = () => wrapper.find('button');
+ const findMoreButton = () => wrapper.findComponent(GlButton);
const createWrapper = (emails) => {
wrapper = mount(EmailParticipantsWarning, {
@@ -48,7 +50,7 @@ describe('Email Participants Warning Component', () => {
describe('when more button clicked', () => {
beforeEach(() => {
- findMoreButton().trigger('click');
+ findMoreButton().vm.$emit('click');
});
it('more button no longer exists', () => {
diff --git a/spec/frontend/notes/components/note_form_spec.js b/spec/frontend/notes/components/note_form_spec.js
index 3c461f2b382..e2072ebd04d 100644
--- a/spec/frontend/notes/components/note_form_spec.js
+++ b/spec/frontend/notes/components/note_form_spec.js
@@ -4,6 +4,7 @@ import batchComments from '~/batch_comments/stores/modules/batch_comments';
import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
import { AT_WHO_ACTIVE_CLASS } from '~/gfm_auto_complete';
import eventHub from '~/environments/event_hub';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -75,14 +76,8 @@ describe('issue_note_form component', () => {
});
});
- it('hides content editor switcher if feature flag content_editor_on_issues is off', () => {
- createComponentWrapper({}, { contentEditorOnIssues: false });
-
- expect(wrapper.text()).not.toContain('Switch to rich text editing');
- });
-
- it('shows content editor switcher if feature flag content_editor_on_issues is on', () => {
- createComponentWrapper({}, { contentEditorOnIssues: true });
+ it('shows content editor switcher', () => {
+ createComponentWrapper();
expect(wrapper.text()).toContain('Switch to rich text editing');
});
@@ -239,6 +234,21 @@ describe('issue_note_form component', () => {
property: 'Issue_note',
});
});
+
+ describe('when discussion is confidential', () => {
+ beforeEach(() => {
+ createComponentWrapper({
+ discussion: {
+ ...discussionMock,
+ confidential: true,
+ },
+ });
+ });
+
+ it('passes correct confidentiality to CommentFieldLayout', () => {
+ expect(wrapper.findComponent(CommentFieldLayout).props('isInternalNote')).toBe(true);
+ });
+ });
});
});
diff --git a/spec/frontend/notes/mock_data.js b/spec/frontend/notes/mock_data.js
index b291eba61f5..67c0ba90d40 100644
--- a/spec/frontend/notes/mock_data.js
+++ b/spec/frontend/notes/mock_data.js
@@ -321,6 +321,7 @@ export const discussionMock = {
individual_note: false,
resolvable: true,
active: true,
+ confidential: false,
};
export const loggedOutnoteableData = {
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 104c297b44e..f07ba1e032f 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -1343,8 +1343,6 @@ describe('Actions Notes Store', () => {
});
it('dispatches `fetchDiscussionsBatch` action with notes_filter 0 for merge request', () => {
- window.gon = { features: { mrActivityFilters: true } };
-
return testAction(
actions.fetchDiscussions,
{ path: 'test-path', filter: 'test-filter', persistFilter: 'test-persist-filter' },
@@ -1397,7 +1395,7 @@ describe('Actions Notes Store', () => {
type: 'fetchDiscussionsBatch',
payload: {
config: {
- params: { notes_filter: 'test-filter', persist_filter: 'test-persist-filter' },
+ params: { notes_filter: 0, persist_filter: false },
},
path: 'test-path',
perPage: 20,
diff --git a/spec/frontend/observability/client_spec.js b/spec/frontend/observability/client_spec.js
index 056175eac07..68a53131539 100644
--- a/spec/frontend/observability/client_spec.js
+++ b/spec/frontend/observability/client_spec.js
@@ -12,7 +12,8 @@ describe('buildClient', () => {
const tracingUrl = 'https://example.com/tracing';
const provisioningUrl = 'https://example.com/provisioning';
-
+ const servicesUrl = 'https://example.com/services';
+ const operationsUrl = 'https://example.com/services/$SERVICE_NAME$/operations';
const FETCHING_TRACES_ERROR = 'traces are missing/invalid in the response';
beforeEach(() => {
@@ -22,6 +23,8 @@ describe('buildClient', () => {
client = buildClient({
tracingUrl,
provisioningUrl,
+ servicesUrl,
+ operationsUrl,
});
});
@@ -29,6 +32,27 @@ describe('buildClient', () => {
axiosMock.restore();
});
+ describe('buildClient', () => {
+ it('rejects if params are missing', () => {
+ const e = new Error(
+ 'missing required params. provisioningUrl, tracingUrl, servicesUrl, operationsUrl are required',
+ );
+ expect(() =>
+ buildClient({ tracingUrl: 'test', servicesUrl: 'test', operationsUrl: 'test' }),
+ ).toThrow(e);
+ expect(() =>
+ buildClient({ provisioningUrl: 'test', servicesUrl: 'test', operationsUrl: 'test' }),
+ ).toThrow(e);
+ expect(() =>
+ buildClient({ provisioningUrl: 'test', tracingUrl: 'test', operationsUrl: 'test' }),
+ ).toThrow(e);
+ expect(() =>
+ buildClient({ provisioningUrl: 'test', tracingUrl: 'test', servicesUrl: 'test' }),
+ ).toThrow(e);
+ expect(() => buildClient({})).toThrow(e);
+ });
+ });
+
describe('isTracingEnabled', () => {
it('returns true if requests succeedes', async () => {
axiosMock.onGet(provisioningUrl).reply(200, {
@@ -145,18 +169,18 @@ describe('buildClient', () => {
describe('fetchTraces', () => {
it('fetches traces from the tracing URL', async () => {
- const mockTraces = [
- {
- trace_id: 'trace-1',
- duration_nano: 3000,
- spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }],
- },
- { trace_id: 'trace-2', duration_nano: 3000, spans: [{ duration_nano: 2000 }] },
- ];
-
- axiosMock.onGet(tracingUrl).reply(200, {
- traces: mockTraces,
- });
+ const mockResponse = {
+ traces: [
+ {
+ trace_id: 'trace-1',
+ duration_nano: 3000,
+ spans: [{ duration_nano: 1000 }, { duration_nano: 2000 }],
+ },
+ { trace_id: 'trace-2', duration_nano: 3000, spans: [{ duration_nano: 2000 }] },
+ ],
+ };
+
+ axiosMock.onGet(tracingUrl).reply(200, mockResponse);
const result = await client.fetchTraces();
@@ -165,7 +189,7 @@ describe('buildClient', () => {
withCredentials: true,
params: new URLSearchParams(),
});
- expect(result).toEqual(mockTraces);
+ expect(result).toEqual(mockResponse);
});
it('rejects if traces are missing', async () => {
@@ -197,28 +221,42 @@ describe('buildClient', () => {
expect(getQueryParam()).toBe('');
});
+ it('appends page_token if specified', async () => {
+ await client.fetchTraces({ pageToken: 'page-token' });
+
+ expect(getQueryParam()).toBe('page_token=page-token');
+ });
+
+ it('appends page_size if specified', async () => {
+ await client.fetchTraces({ pageSize: 10 });
+
+ expect(getQueryParam()).toBe('page_size=10');
+ });
+
it('converts filter to proper query params', async () => {
await client.fetchTraces({
- durationMs: [
- { operator: '>', value: '100' },
- { operator: '<', value: '1000' },
- ],
- operation: [
- { operator: '=', value: 'op' },
- { operator: '!=', value: 'not-op' },
- ],
- serviceName: [
- { operator: '=', value: 'service' },
- { operator: '!=', value: 'not-service' },
- ],
- period: [{ operator: '=', value: '5m' }],
- traceId: [
- { operator: '=', value: 'trace-id' },
- { operator: '!=', value: 'not-trace-id' },
- ],
+ filters: {
+ durationMs: [
+ { operator: '>', value: '100' },
+ { operator: '<', value: '1000' },
+ ],
+ operation: [
+ { operator: '=', value: 'op' },
+ { operator: '!=', value: 'not-op' },
+ ],
+ serviceName: [
+ { operator: '=', value: 'service' },
+ { operator: '!=', value: 'not-service' },
+ ],
+ period: [{ operator: '=', value: '5m' }],
+ traceId: [
+ { operator: '=', value: 'trace-id' },
+ { operator: '!=', value: 'not-trace-id' },
+ ],
+ },
});
expect(getQueryParam()).toBe(
- 'gt[duration_nano]=100000&lt[duration_nano]=1000000' +
+ 'gt[duration_nano]=100000000&lt[duration_nano]=1000000000' +
'&operation=op&not[operation]=not-op' +
'&service_name=service&not[service_name]=not-service' +
'&period=5m' +
@@ -228,17 +266,21 @@ describe('buildClient', () => {
it('handles repeated params', async () => {
await client.fetchTraces({
- operation: [
- { operator: '=', value: 'op' },
- { operator: '=', value: 'op2' },
- ],
+ filters: {
+ operation: [
+ { operator: '=', value: 'op' },
+ { operator: '=', value: 'op2' },
+ ],
+ },
});
expect(getQueryParam()).toBe('operation=op&operation=op2');
});
it('ignores unsupported filters', async () => {
await client.fetchTraces({
- unsupportedFilter: [{ operator: '=', value: 'foo' }],
+ filters: {
+ unsupportedFilter: [{ operator: '=', value: 'foo' }],
+ },
});
expect(getQueryParam()).toBe('');
@@ -246,8 +288,10 @@ describe('buildClient', () => {
it('ignores empty filters', async () => {
await client.fetchTraces({
- durationMs: null,
- traceId: undefined,
+ filters: {
+ durationMs: null,
+ traceId: undefined,
+ },
});
expect(getQueryParam()).toBe('');
@@ -255,28 +299,103 @@ describe('buildClient', () => {
it('ignores unsupported operators', async () => {
await client.fetchTraces({
- durationMs: [
- { operator: '*', value: 'foo' },
- { operator: '=', value: 'foo' },
- { operator: '!=', value: 'foo' },
- ],
- operation: [
- { operator: '>', value: 'foo' },
- { operator: '<', value: 'foo' },
- ],
- serviceName: [
- { operator: '>', value: 'foo' },
- { operator: '<', value: 'foo' },
- ],
- period: [{ operator: '!=', value: 'foo' }],
- traceId: [
- { operator: '>', value: 'foo' },
- { operator: '<', value: 'foo' },
- ],
+ filters: {
+ durationMs: [
+ { operator: '*', value: 'foo' },
+ { operator: '=', value: 'foo' },
+ { operator: '!=', value: 'foo' },
+ ],
+ operation: [
+ { operator: '>', value: 'foo' },
+ { operator: '<', value: 'foo' },
+ ],
+ serviceName: [
+ { operator: '>', value: 'foo' },
+ { operator: '<', value: 'foo' },
+ ],
+ period: [{ operator: '!=', value: 'foo' }],
+ traceId: [
+ { operator: '>', value: 'foo' },
+ { operator: '<', value: 'foo' },
+ ],
+ },
});
expect(getQueryParam()).toBe('');
});
});
});
+
+ describe('fetchServices', () => {
+ it('fetches services from the services URL', async () => {
+ const mockResponse = {
+ services: [{ name: 'service-1' }, { name: 'service-2' }],
+ };
+
+ axiosMock.onGet(servicesUrl).reply(200, mockResponse);
+
+ const result = await client.fetchServices();
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ expect(axios.get).toHaveBeenCalledWith(servicesUrl, {
+ withCredentials: true,
+ });
+ expect(result).toEqual(mockResponse.services);
+ });
+
+ it('rejects if services are missing', async () => {
+ axiosMock.onGet(servicesUrl).reply(200, {});
+
+ const e = 'failed to fetch services. invalid response';
+ await expect(client.fetchServices()).rejects.toThrow(e);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ });
+ });
+
+ describe('fetchOperations', () => {
+ const serviceName = 'test-service';
+ const parsedOperationsUrl = `https://example.com/services/${serviceName}/operations`;
+
+ it('fetches operations from the operations URL', async () => {
+ const mockResponse = {
+ operations: [{ name: 'operation-1' }, { name: 'operation-2' }],
+ };
+
+ axiosMock.onGet(parsedOperationsUrl).reply(200, mockResponse);
+
+ const result = await client.fetchOperations(serviceName);
+
+ expect(axios.get).toHaveBeenCalledTimes(1);
+ expect(axios.get).toHaveBeenCalledWith(parsedOperationsUrl, {
+ withCredentials: true,
+ });
+ expect(result).toEqual(mockResponse.operations);
+ });
+
+ it('rejects if serviceName is missing', async () => {
+ const e = 'fetchOperations() - serviceName is required.';
+ await expect(client.fetchOperations()).rejects.toThrow(e);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ });
+
+ it('rejects if operationUrl does not contain $SERVICE_NAME$', async () => {
+ client = buildClient({
+ tracingUrl,
+ provisioningUrl,
+ servicesUrl,
+ operationsUrl: 'something',
+ });
+ const e = 'fetchOperations() - operationsUrl must contain $SERVICE_NAME$';
+ await expect(client.fetchOperations(serviceName)).rejects.toThrow(e);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ });
+
+ it('rejects if operations are missing', async () => {
+ axiosMock.onGet(parsedOperationsUrl).reply(200, {});
+
+ const e = 'failed to fetch operations. invalid response';
+ await expect(client.fetchOperations(serviceName)).rejects.toThrow(e);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error(e));
+ });
+ });
});
diff --git a/spec/frontend/observability/index_spec.js b/spec/frontend/observability/index_spec.js
deleted file mode 100644
index 25eb048c62b..00000000000
--- a/spec/frontend/observability/index_spec.js
+++ /dev/null
@@ -1,64 +0,0 @@
-import { createWrapper } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import renderObservability from '~/observability/index';
-import ObservabilityApp from '~/observability/components/observability_app.vue';
-import { SKELETON_VARIANTS_BY_ROUTE } from '~/observability/constants';
-
-describe('renderObservability', () => {
- let element;
- let vueInstance;
- let component;
-
- const OBSERVABILITY_ROUTES = Object.keys(SKELETON_VARIANTS_BY_ROUTE);
- const SKELETON_VARIANTS = Object.values(SKELETON_VARIANTS_BY_ROUTE);
-
- beforeEach(() => {
- element = document.createElement('div');
- element.setAttribute('id', 'js-observability-app');
- element.dataset.observabilityIframeSrc = 'https://observe.gitlab.com/';
- document.body.appendChild(element);
-
- vueInstance = renderObservability();
- component = createWrapper(vueInstance).findComponent(ObservabilityApp);
- });
-
- afterEach(() => {
- element.remove();
- });
-
- it('should return a Vue instance', () => {
- expect(vueInstance).toEqual(expect.any(Vue));
- });
-
- it('should render the ObservabilityApp component', () => {
- expect(component.props('observabilityIframeSrc')).toBe('https://observe.gitlab.com/');
- });
-
- describe('skeleton variant', () => {
- it.each`
- pathDescription | path | variant
- ${'dashboards'} | ${OBSERVABILITY_ROUTES[0]} | ${SKELETON_VARIANTS[0]}
- ${'explore'} | ${OBSERVABILITY_ROUTES[1]} | ${SKELETON_VARIANTS[1]}
- ${'manage dashboards'} | ${OBSERVABILITY_ROUTES[2]} | ${SKELETON_VARIANTS[2]}
- ${'any other'} | ${'unknown/route'} | ${SKELETON_VARIANTS[0]}
- `(
- 'renders the $variant skeleton variant for $pathDescription path',
- async ({ path, variant }) => {
- component.vm.$router.push(path);
- await nextTick();
-
- expect(component.props('skeletonVariant')).toBe(variant);
- },
- );
- });
-
- it('handle route-update events', () => {
- component.vm.$router.push('/something?foo=bar');
- component.vm.$emit('route-update', { url: '/some_path' });
- expect(component.vm.$router.currentRoute.path).toBe('/something');
- expect(component.vm.$router.currentRoute.query).toEqual({
- foo: 'bar',
- observability_path: '/some_path',
- });
- });
-});
diff --git a/spec/frontend/observability/observability_app_spec.js b/spec/frontend/observability/observability_app_spec.js
deleted file mode 100644
index 392992a5962..00000000000
--- a/spec/frontend/observability/observability_app_spec.js
+++ /dev/null
@@ -1,201 +0,0 @@
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import { stubComponent } from 'helpers/stub_component';
-import ObservabilityApp from '~/observability/components/observability_app.vue';
-import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
-import {
- MESSAGE_EVENT_TYPE,
- INLINE_EMBED_DIMENSIONS,
- FULL_APP_DIMENSIONS,
- SKELETON_VARIANT_EMBED,
-} from '~/observability/constants';
-
-import { darkModeEnabled } from '~/lib/utils/color_utils';
-
-jest.mock('~/lib/utils/color_utils');
-
-describe('ObservabilityApp', () => {
- let wrapper;
-
- const $route = {
- pathname: 'https://gitlab.com/gitlab-org/',
- path: 'https://gitlab.com/gitlab-org/-/observability/dashboards',
- query: { otherQuery: 100 },
- };
-
- const mockSkeletonOnContentLoaded = jest.fn();
-
- const findIframe = () => wrapper.findByTestId('observability-ui-iframe');
-
- const TEST_IFRAME_SRC = 'https://observe.gitlab.com/9970/?groupId=14485840';
-
- const TEST_USERNAME = 'test-user';
-
- const mountComponent = (props) => {
- wrapper = shallowMountExtended(ObservabilityApp, {
- propsData: {
- observabilityIframeSrc: TEST_IFRAME_SRC,
- ...props,
- },
- stubs: {
- ObservabilitySkeleton: stubComponent(ObservabilitySkeleton, {
- methods: { onContentLoaded: mockSkeletonOnContentLoaded },
- }),
- },
- mocks: {
- $route,
- },
- });
- };
-
- const dispatchMessageEvent = (message) =>
- window.dispatchEvent(new MessageEvent('message', message));
-
- beforeEach(() => {
- gon.current_username = TEST_USERNAME;
- });
-
- describe('iframe src', () => {
- it('should render an iframe with observabilityIframeSrc, decorated with light theme and username', () => {
- darkModeEnabled.mockReturnValueOnce(false);
- mountComponent();
- const iframe = findIframe();
-
- expect(iframe.exists()).toBe(true);
- expect(iframe.attributes('src')).toBe(
- `${TEST_IFRAME_SRC}&theme=light&username=${TEST_USERNAME}`,
- );
- });
-
- it('should render an iframe with observabilityIframeSrc decorated with dark theme and username', () => {
- darkModeEnabled.mockReturnValueOnce(true);
- mountComponent();
- const iframe = findIframe();
-
- expect(iframe.exists()).toBe(true);
- expect(iframe.attributes('src')).toBe(
- `${TEST_IFRAME_SRC}&theme=dark&username=${TEST_USERNAME}`,
- );
- });
- });
-
- describe('iframe sandbox', () => {
- it('should render an iframe with sandbox attributes', () => {
- mountComponent();
- const iframe = findIframe();
-
- expect(iframe.exists()).toBe(true);
- expect(iframe.attributes('sandbox')).toBe('allow-same-origin allow-forms allow-scripts');
- });
- });
-
- describe('iframe kiosk query param', () => {
- it('when inlineEmbed, it should set the proper kiosk query parameter', () => {
- mountComponent({
- inlineEmbed: true,
- });
-
- const iframe = findIframe();
-
- expect(iframe.attributes('src')).toBe(
- `${TEST_IFRAME_SRC}&theme=light&username=${TEST_USERNAME}&kiosk=inline-embed`,
- );
- });
- });
-
- describe('iframe size', () => {
- it('should set the specified size', () => {
- mountComponent({
- height: INLINE_EMBED_DIMENSIONS.HEIGHT,
- width: INLINE_EMBED_DIMENSIONS.WIDTH,
- });
-
- const iframe = findIframe();
-
- expect(iframe.attributes('width')).toBe(INLINE_EMBED_DIMENSIONS.WIDTH);
- expect(iframe.attributes('height')).toBe(INLINE_EMBED_DIMENSIONS.HEIGHT);
- });
-
- it('should fallback to default size', () => {
- mountComponent({});
-
- const iframe = findIframe();
-
- expect(iframe.attributes('width')).toBe(FULL_APP_DIMENSIONS.WIDTH);
- expect(iframe.attributes('height')).toBe(FULL_APP_DIMENSIONS.HEIGHT);
- });
- });
-
- describe('skeleton variant', () => {
- it('sets the specified skeleton variant', () => {
- mountComponent({ skeletonVariant: SKELETON_VARIANT_EMBED });
- const props = wrapper.findComponent(ObservabilitySkeleton).props();
-
- expect(props.variant).toBe(SKELETON_VARIANT_EMBED);
- });
-
- it('should have a default skeleton variant', () => {
- mountComponent();
- const props = wrapper.findComponent(ObservabilitySkeleton).props();
-
- expect(props.variant).toBe('dashboards');
- });
- });
-
- describe('on GOUI_ROUTE_UPDATE', () => {
- it('should emit a route-update event', () => {
- mountComponent();
-
- const payload = { url: '/explore' };
- dispatchMessageEvent({
- data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload },
- origin: 'https://observe.gitlab.com',
- });
-
- expect(wrapper.emitted('route-update')[0]).toEqual([payload]);
- });
- });
-
- describe('on GOUI_LOADED', () => {
- beforeEach(() => {
- mountComponent();
- });
-
- it('should call onContentLoaded method', () => {
- dispatchMessageEvent({
- data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
- origin: 'https://observe.gitlab.com',
- });
- expect(mockSkeletonOnContentLoaded).toHaveBeenCalled();
- });
-
- it('should not call onContentLoaded method if origin is different', () => {
- dispatchMessageEvent({
- data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
- origin: 'https://example.com',
- });
- expect(mockSkeletonOnContentLoaded).not.toHaveBeenCalled();
- });
-
- it('should not call onContentLoaded method if event type is different', () => {
- dispatchMessageEvent({
- data: { type: 'UNKNOWN_EVENT' },
- origin: 'https://observe.gitlab.com',
- });
- expect(mockSkeletonOnContentLoaded).not.toHaveBeenCalled();
- });
- });
-
- describe('on unmount', () => {
- it('should not emit any even on route update', () => {
- mountComponent();
- wrapper.destroy();
-
- dispatchMessageEvent({
- data: { type: MESSAGE_EVENT_TYPE.GOUI_ROUTE_UPDATE, payload: { url: '/explore' } },
- origin: 'https://observe.gitlab.com',
- });
-
- expect(wrapper.emitted('route-update')).toBeUndefined();
- });
- });
-});
diff --git a/spec/frontend/observability/observability_container_spec.js b/spec/frontend/observability/observability_container_spec.js
index 1152df072d4..5d838756308 100644
--- a/spec/frontend/observability/observability_container_spec.js
+++ b/spec/frontend/observability/observability_container_spec.js
@@ -16,6 +16,8 @@ describe('ObservabilityContainer', () => {
const OAUTH_URL = 'https://example.com/oauth';
const TRACING_URL = 'https://example.com/tracing';
const PROVISIONING_URL = 'https://example.com/provisioning';
+ const SERVICES_URL = 'https://example.com/services';
+ const OPERATIONS_URL = 'https://example.com/operations';
beforeEach(() => {
jest.spyOn(console, 'error').mockImplementation();
@@ -27,6 +29,8 @@ describe('ObservabilityContainer', () => {
oauthUrl: OAUTH_URL,
tracingUrl: TRACING_URL,
provisioningUrl: PROVISIONING_URL,
+ servicesUrl: SERVICES_URL,
+ operationsUrl: OPERATIONS_URL,
},
stubs: {
ObservabilitySkeleton: stubComponent(ObservabilitySkeleton, {
@@ -93,6 +97,8 @@ describe('ObservabilityContainer', () => {
expect(buildClient).toHaveBeenCalledWith({
provisioningUrl: PROVISIONING_URL,
tracingUrl: TRACING_URL,
+ servicesUrl: SERVICES_URL,
+ operationsUrl: OPERATIONS_URL,
});
expect(findIframe().exists()).toBe(false);
});
diff --git a/spec/frontend/observability/skeleton_spec.js b/spec/frontend/observability/skeleton_spec.js
index 979070cfb12..5501fa117e0 100644
--- a/spec/frontend/observability/skeleton_spec.js
+++ b/spec/frontend/observability/skeleton_spec.js
@@ -3,32 +3,16 @@ import { GlSkeletonLoader, GlAlert, GlLoadingIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Skeleton from '~/observability/components/skeleton/index.vue';
-import DashboardsSkeleton from '~/observability/components/skeleton/dashboards.vue';
-import ExploreSkeleton from '~/observability/components/skeleton/explore.vue';
-import ManageSkeleton from '~/observability/components/skeleton/manage.vue';
-import EmbedSkeleton from '~/observability/components/skeleton/embed.vue';
-import {
- SKELETON_VARIANTS_BY_ROUTE,
- DEFAULT_TIMERS,
- SKELETON_VARIANT_EMBED,
-} from '~/observability/constants';
+import { DEFAULT_TIMERS } from '~/observability/constants';
describe('Skeleton component', () => {
let wrapper;
- const SKELETON_VARIANTS = [...Object.values(SKELETON_VARIANTS_BY_ROUTE), 'spinner'];
+ const findSpinner = () => wrapper.findComponent(GlLoadingIcon);
const findContentWrapper = () => wrapper.findByTestId('content-wrapper');
- const findExploreSkeleton = () => wrapper.findComponent(ExploreSkeleton);
-
- const findDashboardsSkeleton = () => wrapper.findComponent(DashboardsSkeleton);
-
- const findManageSkeleton = () => wrapper.findComponent(ManageSkeleton);
-
- const findEmbedSkeleton = () => wrapper.findComponent(EmbedSkeleton);
-
const findAlert = () => wrapper.findComponent(GlAlert);
const mountComponent = ({ ...props } = {}) => {
@@ -39,39 +23,39 @@ describe('Skeleton component', () => {
describe('on mount', () => {
beforeEach(() => {
- mountComponent({ variant: 'explore' });
+ mountComponent({ variant: 'spinner' });
});
describe('showing content', () => {
it('shows the skeleton if content is not loaded within CONTENT_WAIT_MS', async () => {
- expect(findExploreSkeleton().exists()).toBe(false);
- expect(findContentWrapper().isVisible()).toBe(false);
+ expect(findSpinner().exists()).toBe(false);
+ expect(findContentWrapper().exists()).toBe(false);
jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
await nextTick();
- expect(findExploreSkeleton().exists()).toBe(true);
- expect(findContentWrapper().isVisible()).toBe(false);
+ expect(findSpinner().exists()).toBe(true);
+ expect(findContentWrapper().exists()).toBe(false);
});
it('does not show the skeleton if content loads within CONTENT_WAIT_MS', async () => {
- expect(findExploreSkeleton().exists()).toBe(false);
- expect(findContentWrapper().isVisible()).toBe(false);
+ expect(findSpinner().exists()).toBe(false);
+ expect(findContentWrapper().exists()).toBe(false);
wrapper.vm.onContentLoaded();
await nextTick();
- expect(findContentWrapper().isVisible()).toBe(true);
- expect(findExploreSkeleton().exists()).toBe(false);
+ expect(findContentWrapper().exists()).toBe(true);
+ expect(findSpinner().exists()).toBe(false);
jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
await nextTick();
- expect(findContentWrapper().isVisible()).toBe(true);
- expect(findExploreSkeleton().exists()).toBe(false);
+ expect(findContentWrapper().exists()).toBe(true);
+ expect(findSpinner().exists()).toBe(false);
});
it('hides the skeleton after content loads', async () => {
@@ -79,15 +63,15 @@ describe('Skeleton component', () => {
await nextTick();
- expect(findExploreSkeleton().exists()).toBe(true);
- expect(findContentWrapper().isVisible()).toBe(false);
+ expect(findSpinner().exists()).toBe(true);
+ expect(findContentWrapper().exists()).toBe(false);
wrapper.vm.onContentLoaded();
await nextTick();
- expect(findContentWrapper().isVisible()).toBe(true);
- expect(findExploreSkeleton().exists()).toBe(false);
+ expect(findContentWrapper().exists()).toBe(true);
+ expect(findSpinner().exists()).toBe(false);
});
});
@@ -99,7 +83,7 @@ describe('Skeleton component', () => {
await nextTick();
expect(findAlert().exists()).toBe(true);
- expect(findContentWrapper().isVisible()).toBe(false);
+ expect(findContentWrapper().exists()).toBe(false);
});
it('shows the error dialog if content fails to load', async () => {
@@ -110,7 +94,7 @@ describe('Skeleton component', () => {
await nextTick();
expect(findAlert().exists()).toBe(true);
- expect(findContentWrapper().isVisible()).toBe(false);
+ expect(findContentWrapper().exists()).toBe(false);
});
it('does not show the error dialog if content has loaded within TIMEOUT_MS', async () => {
@@ -120,36 +104,28 @@ describe('Skeleton component', () => {
await nextTick();
expect(findAlert().exists()).toBe(false);
- expect(findContentWrapper().isVisible()).toBe(true);
+ expect(findContentWrapper().exists()).toBe(true);
});
});
});
describe('skeleton variant', () => {
- it.each`
- skeletonType | condition | variant
- ${'dashboards'} | ${'variant is dashboards'} | ${SKELETON_VARIANTS[0]}
- ${'explore'} | ${'variant is explore'} | ${SKELETON_VARIANTS[1]}
- ${'manage'} | ${'variant is manage'} | ${SKELETON_VARIANTS[2]}
- ${'embed'} | ${'variant is embed'} | ${SKELETON_VARIANT_EMBED}
- ${'spinner'} | ${'variant is spinner'} | ${'spinner'}
- ${'default'} | ${'variant is not manage, dashboards or explore'} | ${'unknown'}
- `('should render $skeletonType skeleton if $condition', async ({ skeletonType, variant }) => {
- mountComponent({ variant });
+ it('shows only the spinner variant when variant is spinner', async () => {
+ mountComponent({ variant: 'spinner' });
jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
await nextTick();
- const showsDefaultSkeleton = ![...SKELETON_VARIANTS, SKELETON_VARIANT_EMBED].includes(
- variant,
- );
- expect(findDashboardsSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[0]);
- expect(findExploreSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[1]);
- expect(findManageSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANTS[2]);
- expect(findEmbedSkeleton().exists()).toBe(skeletonType === SKELETON_VARIANT_EMBED);
+ expect(findSpinner().exists()).toBe(true);
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(false);
+ });
- expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(showsDefaultSkeleton);
+ it('shows only the default variant when variant is not spinner', async () => {
+ mountComponent({ variant: 'unknown' });
+ jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
+ await nextTick();
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(variant === 'spinner');
+ expect(findSpinner().exists()).toBe(false);
+ expect(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(true);
});
});
diff --git a/spec/frontend/organizations/index/components/app_spec.js b/spec/frontend/organizations/index/components/app_spec.js
new file mode 100644
index 00000000000..175b1e1c552
--- /dev/null
+++ b/spec/frontend/organizations/index/components/app_spec.js
@@ -0,0 +1,87 @@
+import { GlButton } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/alert';
+import { organizations } from '~/organizations/mock_data';
+import resolvers from '~/organizations/shared/graphql/resolvers';
+import organizationsQuery from '~/organizations/index/graphql/organizations.query.graphql';
+import OrganizationsIndexApp from '~/organizations/index/components/app.vue';
+import OrganizationsView from '~/organizations/index/components/organizations_view.vue';
+import { MOCK_NEW_ORG_URL } from '../mock_data';
+
+jest.mock('~/alert');
+
+Vue.use(VueApollo);
+
+describe('OrganizationsIndexApp', () => {
+ let wrapper;
+ let mockApollo;
+
+ const createComponent = (mockResolvers = resolvers) => {
+ mockApollo = createMockApollo([[organizationsQuery, mockResolvers]]);
+
+ wrapper = shallowMountExtended(OrganizationsIndexApp, {
+ apolloProvider: mockApollo,
+ provide: {
+ newOrganizationUrl: MOCK_NEW_ORG_URL,
+ },
+ });
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ const findOrganizationHeaderText = () => wrapper.findByText('Organizations');
+ const findNewOrganizationButton = () => wrapper.findComponent(GlButton);
+ const findOrganizationsView = () => wrapper.findComponent(OrganizationsView);
+
+ const loadingResolver = jest.fn().mockReturnValue(new Promise(() => {}));
+ const successfulResolver = (nodes) =>
+ jest.fn().mockResolvedValue({
+ data: { currentUser: { id: 1, organizations: { nodes } } },
+ });
+ const errorResolver = jest.fn().mockRejectedValue('error');
+
+ describe.each`
+ description | mockResolver | headerText | newOrgLink | loading | orgsData | error
+ ${'when API call is loading'} | ${loadingResolver} | ${true} | ${MOCK_NEW_ORG_URL} | ${true} | ${[]} | ${false}
+ ${'when API returns successful with results'} | ${successfulResolver(organizations)} | ${true} | ${MOCK_NEW_ORG_URL} | ${false} | ${organizations} | ${false}
+ ${'when API returns successful without results'} | ${successfulResolver([])} | ${false} | ${false} | ${false} | ${[]} | ${false}
+ ${'when API returns error'} | ${errorResolver} | ${false} | ${false} | ${false} | ${[]} | ${true}
+ `('$description', ({ mockResolver, headerText, newOrgLink, loading, orgsData, error }) => {
+ beforeEach(async () => {
+ createComponent(mockResolver);
+ await waitForPromises();
+ });
+
+ it(`does ${headerText ? '' : 'not '}render the header text`, () => {
+ expect(findOrganizationHeaderText().exists()).toBe(headerText);
+ });
+
+ it(`does ${newOrgLink ? '' : 'not '}render new organization button with correct link`, () => {
+ expect(
+ findNewOrganizationButton().exists() && findNewOrganizationButton().attributes('href'),
+ ).toBe(newOrgLink);
+ });
+
+ it(`renders the organizations view with ${loading} loading prop`, () => {
+ expect(findOrganizationsView().props('loading')).toBe(loading);
+ });
+
+ it(`renders the organizations view with ${
+ orgsData ? 'correct' : 'empty'
+ } organizations array prop`, () => {
+ expect(findOrganizationsView().props('organizations')).toStrictEqual(orgsData);
+ });
+
+ it(`does ${error ? '' : 'not '}render an error message`, () => {
+ return error
+ ? expect(createAlert).toHaveBeenCalled()
+ : expect(createAlert).not.toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/organizations/index/components/organizations_list_item_spec.js b/spec/frontend/organizations/index/components/organizations_list_item_spec.js
new file mode 100644
index 00000000000..b3bff5ed517
--- /dev/null
+++ b/spec/frontend/organizations/index/components/organizations_list_item_spec.js
@@ -0,0 +1,70 @@
+import { GlAvatarLabeled } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import OrganizationsListItem from '~/organizations/index/components/organizations_list_item.vue';
+import { organizations } from '~/organizations/mock_data';
+
+const MOCK_ORGANIZATION = organizations[0];
+
+describe('OrganizationsListItem', () => {
+ let wrapper;
+
+ const defaultProps = {
+ organization: MOCK_ORGANIZATION,
+ };
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(OrganizationsListItem, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+ };
+
+ const findGlAvatarLabeled = () => wrapper.findComponent(GlAvatarLabeled);
+ const findHTMLOrganizationDescription = () =>
+ wrapper.findByTestId('organization-description-html');
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders GlAvatarLabeled with correct data', () => {
+ expect(findGlAvatarLabeled().attributes()).toMatchObject({
+ 'entity-id': getIdFromGraphQLId(MOCK_ORGANIZATION.id).toString(),
+ 'entity-name': MOCK_ORGANIZATION.name,
+ src: MOCK_ORGANIZATION.avatarUrl,
+ label: MOCK_ORGANIZATION.name,
+ labellink: MOCK_ORGANIZATION.webUrl,
+ });
+ });
+ });
+
+ describe('organization description', () => {
+ const descriptionHtml = '<p>Foo bar</p>';
+
+ describe('is a HTML description', () => {
+ beforeEach(() => {
+ createComponent({ organization: { ...MOCK_ORGANIZATION, descriptionHtml } });
+ });
+
+ it('renders HTML description', () => {
+ expect(findHTMLOrganizationDescription().html()).toContain(descriptionHtml);
+ });
+ });
+
+ describe('is not a HTML description', () => {
+ beforeEach(() => {
+ createComponent({
+ organization: { ...MOCK_ORGANIZATION, descriptionHtml: null },
+ });
+ });
+
+ it('does not render HTML description', () => {
+ expect(findHTMLOrganizationDescription().exists()).toBe(false);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/index/components/organizations_list_spec.js b/spec/frontend/organizations/index/components/organizations_list_spec.js
new file mode 100644
index 00000000000..0b59c212314
--- /dev/null
+++ b/spec/frontend/organizations/index/components/organizations_list_spec.js
@@ -0,0 +1,28 @@
+import { shallowMount } from '@vue/test-utils';
+import OrganizationsList from '~/organizations/index/components/organizations_list.vue';
+import OrganizationsListItem from '~/organizations/index/components/organizations_list_item.vue';
+import { organizations } from '~/organizations/mock_data';
+
+describe('OrganizationsList', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(OrganizationsList, {
+ propsData: {
+ organizations,
+ },
+ });
+ };
+
+ const findAllOrganizationsListItem = () => wrapper.findAllComponents(OrganizationsListItem);
+
+ describe('template', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders a list item for each organization', () => {
+ expect(findAllOrganizationsListItem()).toHaveLength(organizations.length);
+ });
+ });
+});
diff --git a/spec/frontend/organizations/index/components/organizations_view_spec.js b/spec/frontend/organizations/index/components/organizations_view_spec.js
new file mode 100644
index 00000000000..85a1c11a2b1
--- /dev/null
+++ b/spec/frontend/organizations/index/components/organizations_view_spec.js
@@ -0,0 +1,57 @@
+import { GlLoadingIcon, GlEmptyState } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { organizations } from '~/organizations/mock_data';
+import OrganizationsView from '~/organizations/index/components/organizations_view.vue';
+import OrganizationsList from '~/organizations/index/components/organizations_list.vue';
+import { MOCK_NEW_ORG_URL, MOCK_ORG_EMPTY_STATE_SVG } from '../mock_data';
+
+describe('OrganizationsView', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMount(OrganizationsView, {
+ propsData: {
+ ...props,
+ },
+ provide: {
+ newOrganizationUrl: MOCK_NEW_ORG_URL,
+ organizationsEmptyStateSvgPath: MOCK_ORG_EMPTY_STATE_SVG,
+ },
+ });
+ };
+
+ const findGlLoading = () => wrapper.findComponent(GlLoadingIcon);
+ const findOrganizationsList = () => wrapper.findComponent(OrganizationsList);
+ const findGlEmptyState = () => wrapper.findComponent(GlEmptyState);
+
+ describe.each`
+ description | loading | orgsData | emptyStateSvg | emptyStateUrl
+ ${'when loading'} | ${true} | ${[]} | ${false} | ${false}
+ ${'when not loading and has organizations'} | ${false} | ${organizations} | ${false} | ${false}
+ ${'when not loading and has no organizations'} | ${false} | ${[]} | ${MOCK_ORG_EMPTY_STATE_SVG} | ${MOCK_NEW_ORG_URL}
+ `('$description', ({ loading, orgsData, emptyStateSvg, emptyStateUrl }) => {
+ beforeEach(() => {
+ createComponent({ loading, organizations: orgsData });
+ });
+
+ it(`does ${loading ? '' : 'not '}render loading icon`, () => {
+ expect(findGlLoading().exists()).toBe(loading);
+ });
+
+ it(`does ${orgsData.length ? '' : 'not '}render organizations list`, () => {
+ expect(findOrganizationsList().exists()).toBe(Boolean(orgsData.length));
+ });
+
+ it(`does ${emptyStateSvg ? '' : 'not '}render empty state with SVG`, () => {
+ expect(findGlEmptyState().exists() && findGlEmptyState().attributes('svgpath')).toBe(
+ emptyStateSvg,
+ );
+ });
+
+ it(`does ${emptyStateUrl ? '' : 'not '}render empty state with URL`, () => {
+ expect(
+ findGlEmptyState().exists() && findGlEmptyState().attributes('primarybuttonlink'),
+ ).toBe(emptyStateUrl);
+ });
+ });
+});
diff --git a/spec/frontend/organizations/index/mock_data.js b/spec/frontend/organizations/index/mock_data.js
new file mode 100644
index 00000000000..50b20b4f79c
--- /dev/null
+++ b/spec/frontend/organizations/index/mock_data.js
@@ -0,0 +1,3 @@
+export const MOCK_NEW_ORG_URL = 'gitlab.com/organizations/new';
+
+export const MOCK_ORG_EMPTY_STATE_SVG = 'illustrations/empty-state/empty-organizations-md.svg';
diff --git a/spec/frontend/organizations/new/components/app_spec.js b/spec/frontend/organizations/new/components/app_spec.js
new file mode 100644
index 00000000000..06d30ad6b12
--- /dev/null
+++ b/spec/frontend/organizations/new/components/app_spec.js
@@ -0,0 +1,113 @@
+import VueApollo from 'vue-apollo';
+import Vue, { nextTick } from 'vue';
+
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import App from '~/organizations/new/components/app.vue';
+import resolvers from '~/organizations/shared/graphql/resolvers';
+import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
+import { visitUrlWithAlerts } from '~/lib/utils/url_utility';
+import { createOrganizationResponse } from '~/organizations/mock_data';
+import { createAlert } from '~/alert';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+Vue.use(VueApollo);
+jest.useFakeTimers();
+
+jest.mock('~/lib/utils/url_utility');
+jest.mock('~/alert');
+
+describe('OrganizationNewApp', () => {
+ let wrapper;
+ let mockApollo;
+
+ const createComponent = ({ mockResolvers = resolvers } = {}) => {
+ mockApollo = createMockApollo([], mockResolvers);
+
+ wrapper = shallowMountExtended(App, { apolloProvider: mockApollo });
+ };
+
+ const findForm = () => wrapper.findComponent(NewEditForm);
+ const submitForm = async () => {
+ findForm().vm.$emit('submit', { name: 'Foo bar', path: 'foo-bar' });
+ await nextTick();
+ };
+
+ afterEach(() => {
+ mockApollo = null;
+ });
+
+ it('renders form', () => {
+ createComponent();
+
+ expect(findForm().exists()).toBe(true);
+ });
+
+ describe('when form is submitted', () => {
+ describe('when API is loading', () => {
+ beforeEach(async () => {
+ const mockResolvers = {
+ Mutation: {
+ createOrganization: jest.fn().mockReturnValueOnce(new Promise(() => {})),
+ },
+ };
+
+ createComponent({ mockResolvers });
+
+ await submitForm();
+ });
+
+ it('sets `NewEditForm` `loading` prop to `true`', () => {
+ expect(findForm().props('loading')).toBe(true);
+ });
+ });
+
+ describe('when API request is successful', () => {
+ beforeEach(async () => {
+ createComponent();
+ await submitForm();
+ jest.runAllTimers();
+ await waitForPromises();
+ });
+
+ it('redirects user to organization path', () => {
+ expect(visitUrlWithAlerts).toHaveBeenCalledWith(
+ createOrganizationResponse.organization.path,
+ [
+ {
+ id: 'organization-successfully-created',
+ title: 'Organization successfully created.',
+ message: 'You can now start using your new organization.',
+ variant: 'success',
+ },
+ ],
+ );
+ });
+ });
+
+ describe('when API request is not successful', () => {
+ const error = new Error();
+
+ beforeEach(async () => {
+ const mockResolvers = {
+ Mutation: {
+ createOrganization: jest.fn().mockRejectedValueOnce(error),
+ },
+ };
+
+ createComponent({ mockResolvers });
+ await submitForm();
+ jest.runAllTimers();
+ await waitForPromises();
+ });
+
+ it('displays error alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred creating an organization. Please try again.',
+ error,
+ captureError: true,
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/organizations/shared/components/new_edit_form_spec.js b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
new file mode 100644
index 00000000000..43c099fbb1c
--- /dev/null
+++ b/spec/frontend/organizations/shared/components/new_edit_form_spec.js
@@ -0,0 +1,112 @@
+import { GlButton, GlInputGroupText, GlTruncate } from '@gitlab/ui';
+
+import NewEditForm from '~/organizations/shared/components/new_edit_form.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('NewEditForm', () => {
+ let wrapper;
+
+ const defaultProvide = {
+ organizationsPath: '/-/organizations',
+ rootUrl: 'http://127.0.0.1:3000/',
+ };
+
+ const defaultPropsData = {
+ loading: false,
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(NewEditForm, {
+ attachTo: document.body,
+ provide: defaultProvide,
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+ };
+
+ const findNameField = () => wrapper.findByLabelText('Organization name');
+ const findUrlField = () => wrapper.findByLabelText('Organization URL');
+ const submitForm = async () => {
+ await wrapper.findByRole('button', { name: 'Create organization' }).trigger('click');
+ };
+
+ it('renders `Organization name` field', () => {
+ createComponent();
+
+ expect(findNameField().exists()).toBe(true);
+ });
+
+ it('renders `Organization URL` field', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlInputGroupText).findComponent(GlTruncate).props('text')).toBe(
+ 'http://127.0.0.1:3000/-/organizations/',
+ );
+ expect(findUrlField().exists()).toBe(true);
+ });
+
+ describe('when form is submitted without filling in required fields', () => {
+ beforeEach(async () => {
+ createComponent();
+ await submitForm();
+ });
+
+ it('shows error messages', () => {
+ expect(wrapper.findByText('Organization name is required.').exists()).toBe(true);
+ expect(wrapper.findByText('Organization URL is required.').exists()).toBe(true);
+ });
+ });
+
+ describe('when form is submitted successfully', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await findNameField().setValue('Foo bar');
+ await findUrlField().setValue('foo-bar');
+ await submitForm();
+ });
+
+ it('emits `submit` event with form values', () => {
+ expect(wrapper.emitted('submit')).toEqual([[{ name: 'Foo bar', path: 'foo-bar' }]]);
+ });
+ });
+
+ describe('when `Organization URL` has not been manually set', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await findNameField().setValue('Foo bar');
+ await submitForm();
+ });
+
+ it('sets `Organization URL` when typing in `Organization name`', () => {
+ expect(findUrlField().element.value).toBe('foo-bar');
+ });
+ });
+
+ describe('when `Organization URL` has been manually set', () => {
+ beforeEach(async () => {
+ createComponent();
+
+ await findUrlField().setValue('foo-bar-baz');
+ await findNameField().setValue('Foo bar');
+ await submitForm();
+ });
+
+ it('does not modify `Organization URL` when typing in `Organization name`', () => {
+ expect(findUrlField().element.value).toBe('foo-bar-baz');
+ });
+ });
+
+ describe('when `loading` prop is `true`', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { loading: true } });
+ });
+
+ it('shows button with loading icon', () => {
+ expect(wrapper.findComponent(GlButton).props('loading')).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
index 7f26ed778a5..6af9e38192e 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -9,51 +9,44 @@ exports[`packages_list_app renders 1`] = `
<infrastructure-search-stub />
<div>
<section
- class="empty-state gl-display-flex gl-flex-direction-column gl-text-center"
+ class="gl-display-flex gl-empty-state gl-flex-direction-column gl-text-center"
>
<div
class="gl-max-w-full"
>
- <div
- class="svg-250 svg-content"
- >
- <img
- alt=""
- class="gl-dark-invert-keep-hue gl-max-w-full"
- role="img"
- src="helpSvg"
- />
- </div>
+ <img
+ alt=""
+ class="gl-dark-invert-keep-hue gl-max-w-full"
+ height="144"
+ role="img"
+ src="helpSvg"
+ />
</div>
<div
- class="gl-m-auto gl-max-w-full"
+ class="gl-empty-state-content gl-m-auto gl-mx-auto gl-my-0 gl-p-5"
data-testid="gl-empty-state-content"
>
- <div
- class="gl-mx-auto gl-my-0 gl-p-5"
+ <h1
+ class="gl-font-size-h-display gl-line-height-36 gl-mb-0 gl-mt-0 h4"
>
- <h1
- class="gl-font-size-h-display gl-line-height-36 h4"
- >
- There are no packages yet
- </h1>
- <p
- class="gl-mt-3"
+ There are no packages yet
+ </h1>
+ <p
+ class="gl-mb-0 gl-mt-4"
+ >
+ Learn how to
+ <b-link-stub
+ class="gl-link"
+ href="helpUrl"
+ target="_blank"
>
- Learn how to
- <b-link-stub
- class="gl-link"
- href="helpUrl"
- target="_blank"
- >
- publish and share your packages
- </b-link-stub>
- with GitLab.
- </p>
- <div
- class="gl-display-flex gl-flex-wrap gl-justify-content-center"
- />
- </div>
+ publish and share your packages
+ </b-link-stub>
+ with GitLab.
+ </p>
+ <div
+ class="gl-display-flex gl-flex-wrap gl-justify-content-center gl-mt-5"
+ />
</div>
</section>
</div>
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
index 05a5a718e52..17acf7381c0 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap
@@ -78,7 +78,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
tabindex="-1"
>
<span
- class="gl-bg-gray-50! gl-new-dropdown-item-content"
+ class="gl-new-dropdown-item-content"
>
<svg
aria-hidden="true"
diff --git a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
index 0037934cbc5..be50858bc88 100644
--- a/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
+++ b/spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js
@@ -29,6 +29,7 @@ describe('BulkImportsHistoryApp', () => {
source_full_path: 'top-level-group-12',
destination_full_path: 'h5bp/top-level-group-12',
destination_name: 'top-level-group-12',
+ destination_slug: 'top-level-group-12',
destination_namespace: 'h5bp',
created_at: '2021-07-08T10:03:44.743Z',
failures: [],
@@ -40,6 +41,7 @@ describe('BulkImportsHistoryApp', () => {
entity_type: 'project',
source_full_path: 'autodevops-demo',
destination_name: 'autodevops-demo',
+ destination_slug: 'autodevops-demo',
destination_full_path: 'some-group/autodevops-demo',
destination_namespace: 'flightjs',
parent_id: null,
@@ -141,6 +143,25 @@ describe('BulkImportsHistoryApp', () => {
);
});
+ it('resets page to 1 when page size is changed', async () => {
+ const NEW_PAGE_SIZE = 4;
+
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ createComponent();
+ await axios.waitForAll();
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page', 2);
+ await axios.waitForAll();
+ mock.resetHistory();
+
+ wrapper.findComponent(PaginationBar).vm.$emit('set-page-size', NEW_PAGE_SIZE);
+ await axios.waitForAll();
+
+ expect(mock.history.get.length).toBe(1);
+ expect(mock.history.get[0].params).toStrictEqual(
+ expect.objectContaining({ per_page: NEW_PAGE_SIZE, page: 1 }),
+ );
+ });
+
it('sets up the local storage sync correctly', async () => {
const NEW_PAGE_SIZE = 4;
@@ -154,7 +175,7 @@ describe('BulkImportsHistoryApp', () => {
expect(findLocalStorageSync().props('value')).toBe(NEW_PAGE_SIZE);
});
- it('renders correct url for destination group when relative_url is empty', async () => {
+ it('renders link to destination_full_path for destination group', async () => {
createComponent({ shallow: false });
await axios.waitForAll();
@@ -163,14 +184,17 @@ describe('BulkImportsHistoryApp', () => {
);
});
- it('renders loading icon when destination namespace is not defined', async () => {
+ it('renders destination as text when destination_full_path is not defined', async () => {
const RESPONSE = [{ ...DUMMY_RESPONSE[0], destination_full_path: null }];
mock.onGet(API_URL).reply(HTTP_STATUS_OK, RESPONSE, DEFAULT_HEADERS);
createComponent({ shallow: false });
await axios.waitForAll();
- expect(wrapper.find('tbody tr').findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.find('tbody tr a').exists()).toBe(false);
+ expect(wrapper.find('tbody tr span').text()).toBe(
+ `${DUMMY_RESPONSE[0].destination_namespace}/${DUMMY_RESPONSE[0].destination_slug}/`,
+ );
});
it('adds slash to group urls', async () => {
diff --git a/spec/frontend/pages/projects/find_file/ref_switcher/ref_switcher_utils_spec.js b/spec/frontend/pages/projects/find_file/ref_switcher/ref_switcher_utils_spec.js
index ef2e5d779d8..62eae19ce4c 100644
--- a/spec/frontend/pages/projects/find_file/ref_switcher/ref_switcher_utils_spec.js
+++ b/spec/frontend/pages/projects/find_file/ref_switcher/ref_switcher_utils_spec.js
@@ -10,7 +10,7 @@ describe('generateRefDestinationPath', () => {
${`${projectRootPath}/-/find_file/flightjs/Flight`} | ${`http://test.host/${projectRootPath}/-/find_file/${selectedRef}`}
${`${projectRootPath}/-/find_file/test/test1?test=something`} | ${`http://test.host/${projectRootPath}/-/find_file/${selectedRef}?test=something`}
${`${projectRootPath}/-/find_file/simpletest?test=something&test=it`} | ${`http://test.host/${projectRootPath}/-/find_file/${selectedRef}?test=something&test=it`}
- ${`${projectRootPath}/-/find_file/some_random_char?test=something&test[]=it&test[]=is`} | ${`http://test.host/${projectRootPath}/-/find_file/${selectedRef}?test=something&test[]=it&test[]=is`}
+ ${`${projectRootPath}/-/find_file/some_random_char?test=something&test[]=it&test[]=is`} | ${`http://test.host/${projectRootPath}/-/find_file/${selectedRef}?test=something&test%5B%5D=it&test%5B%5D=is`}
`('generates the correct destination path for $currentPath', ({ currentPath, result }) => {
setWindowLocation(currentPath);
expect(generateRefDestinationPath(selectedRef, '/-/find_file')).toBe(result);
@@ -36,4 +36,11 @@ describe('generateRefDestinationPath', () => {
`http://test.host/${projectRootPath}/-/find_file/flightjs/Flight`,
);
});
+
+ it('removes ref_type from the destination url if ref is neither a branch or tag', () => {
+ setWindowLocation(`${projectRootPath}/-/find_file/somebranch?ref_type=heads`);
+ expect(generateRefDestinationPath('8e90e533', '/-/find_file')).toBe(
+ `http://test.host/${projectRootPath}/-/find_file/8e90e533`,
+ );
+ });
});
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index f5a7dfe6d11..50d09481b93 100644
--- a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
+++ b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
@@ -1,6 +1,5 @@
-import { GlIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
import IntervalPatternInput from '~/pages/projects/pipeline_schedules/shared/components/interval_pattern_input.vue';
@@ -21,15 +20,15 @@ describe('Interval Pattern Input Component', () => {
const everyDayKey = 'everyDay';
const cronIntervalNotInPreset = `0 12 * * *`;
- const findEveryDayRadio = () => wrapper.find(`[data-testid=${everyDayKey}]`);
- const findEveryWeekRadio = () => wrapper.find('[data-testid="everyWeek"]');
- const findEveryMonthRadio = () => wrapper.find('[data-testid="everyMonth"]');
- const findCustomRadio = () => wrapper.find(`[data-testid="${customKey}"]`);
+ const findEveryDayRadio = () => wrapper.findByTestId(everyDayKey);
+ const findEveryWeekRadio = () => wrapper.findByTestId('everyWeek');
+ const findEveryMonthRadio = () => wrapper.findByTestId('everyMonth');
+ const findCustomRadio = () => wrapper.findByTestId(customKey);
const findCustomInput = () => wrapper.find('#schedule_cron');
const findAllLabels = () => wrapper.findAll('label');
const findSelectedRadio = () =>
wrapper.findAll('input[type="radio"]').wrappers.find((x) => x.element.checked);
- const findIcon = () => wrapper.findComponent(GlIcon);
+ const findIcon = () => wrapper.findByTestId('daily-limit');
const findSelectedRadioKey = () => findSelectedRadio()?.attributes('data-testid');
const selectEveryDayRadio = () => findEveryDayRadio().setChecked(true);
const selectEveryWeekRadio = () => findEveryWeekRadio().setChecked(true);
@@ -37,7 +36,7 @@ describe('Interval Pattern Input Component', () => {
const selectCustomRadio = () => findCustomRadio().setChecked(true);
const createWrapper = (props = {}, data = {}) => {
- wrapper = mount(IntervalPatternInput, {
+ wrapper = mountExtended(IntervalPatternInput, {
propsData: { ...props },
data() {
return {
@@ -132,7 +131,7 @@ describe('Interval Pattern Input Component', () => {
'Every day (at 4:00am)',
'Every week (Monday at 4:00am)',
'Every month (Day 1 at 4:00am)',
- 'Custom (Learn more.)',
+ 'Custom',
]);
});
});
diff --git a/spec/frontend/performance_bar/components/request_warning_spec.js b/spec/frontend/performance_bar/components/request_warning_spec.js
index 7b6d8ff695d..a4f0d388e33 100644
--- a/spec/frontend/performance_bar/components/request_warning_spec.js
+++ b/spec/frontend/performance_bar/components/request_warning_spec.js
@@ -1,6 +1,9 @@
+import Vue from 'vue';
import { shallowMount } from '@vue/test-utils';
import RequestWarning from '~/performance_bar/components/request_warning.vue';
+Vue.config.ignoredElements = ['gl-emoji'];
+
describe('request warning', () => {
let wrapper;
const htmlId = 'request-123';
@@ -16,8 +19,8 @@ describe('request warning', () => {
});
it('adds a warning emoji with the correct ID', () => {
- expect(wrapper.find('span[id]').attributes('id')).toEqual(htmlId);
- expect(wrapper.find('span[id] gl-emoji').element.dataset.name).toEqual('warning');
+ expect(wrapper.find('span gl-emoji[id]').attributes('id')).toEqual(htmlId);
+ expect(wrapper.find('span gl-emoji[id]').element.dataset.name).toEqual('warning');
});
});
diff --git a/spec/frontend/performance_bar/index_spec.js b/spec/frontend/performance_bar/index_spec.js
index 1849c373326..cfc752655bd 100644
--- a/spec/frontend/performance_bar/index_spec.js
+++ b/spec/frontend/performance_bar/index_spec.js
@@ -1,3 +1,4 @@
+import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
@@ -6,6 +7,8 @@ import '~/performance_bar/components/performance_bar_app.vue';
import performanceBar from '~/performance_bar';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
+Vue.config.ignoredElements = ['gl-emoji'];
+
jest.mock('~/performance_bar/performance_bar_log');
describe('performance bar wrapper', () => {
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 479530c1d38..b39644c51eb 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
@@ -24,7 +24,7 @@ exports[`Project remove modal initialized matches the snapshot 1`] = `
<gl-button-stub
buttontextclasses=""
category="primary"
- data-qa-selector="delete_button"
+ data-testid="delete-button"
icon=""
size="medium"
variant="danger"
diff --git a/spec/frontend/projects/project_find_file_spec.js b/spec/frontend/projects/project_find_file_spec.js
index efc9d411a98..9dae2bdc5bb 100644
--- a/spec/frontend/projects/project_find_file_spec.js
+++ b/spec/frontend/projects/project_find_file_spec.js
@@ -30,12 +30,13 @@ describe('ProjectFindFile', () => {
let element;
let mock;
- const getProjectFindFileInstance = () =>
- new ProjectFindFile(element, {
- url: FILE_FIND_URL,
+ const getProjectFindFileInstance = (extraOptions) => {
+ return new ProjectFindFile(element, {
treeUrl: FIND_TREE_URL,
blobUrlTemplate: BLOB_URL_TEMPLATE,
+ ...extraOptions,
});
+ };
const findFiles = () =>
element
@@ -64,9 +65,6 @@ describe('ProjectFindFile', () => {
HTTP_STATUS_OK,
files.map((x) => x.path),
);
- getProjectFindFileInstance(); // This triggers a load / axios call + subsequent render in the constructor
-
- return waitForPromises();
});
afterEach(() => {
@@ -75,19 +73,44 @@ describe('ProjectFindFile', () => {
sanitize.mockClear();
});
- it('loads and renders elements from remote server', () => {
- expect(findFiles()).toEqual(
- files.map(({ path, escaped }) => ({
- text: path,
- href: `${BLOB_URL_TEMPLATE}/${escaped}`,
- })),
- );
+ describe('rendering without refType', () => {
+ beforeEach(() => {
+ const instance = getProjectFindFileInstance();
+ instance.load(FILE_FIND_URL); // axios call + subsequent render
+ return waitForPromises();
+ });
+
+ it('loads and renders elements from remote server', () => {
+ expect(findFiles()).toEqual(
+ files.map(({ path, escaped }) => ({
+ text: path,
+ href: `${BLOB_URL_TEMPLATE}/${escaped}`,
+ })),
+ );
+ });
+
+ it('sanitizes search text', () => {
+ const searchText = element.find('.file-finder-input').val();
+
+ expect(sanitize).toHaveBeenCalledTimes(1);
+ expect(sanitize).toHaveBeenCalledWith(searchText);
+ });
});
- it('sanitizes search text', () => {
- const searchText = element.find('.file-finder-input').val();
+ describe('with refType option', () => {
+ beforeEach(() => {
+ const instance = getProjectFindFileInstance({ refType: 'heads' });
+ instance.load(FILE_FIND_URL); // axios call + subsequent render
+ return waitForPromises();
+ });
- expect(sanitize).toHaveBeenCalledTimes(1);
- expect(sanitize).toHaveBeenCalledWith(searchText);
+ it('loads and renders elements from remote server', () => {
+ expect(findFiles()).toEqual(
+ files.map(({ path, escaped }) => ({
+ text: path,
+ href: `${BLOB_URL_TEMPLATE}/${escaped}?ref_type=heads`,
+ })),
+ );
+ });
});
});
diff --git a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
index 0ed2e51e8c3..7c8cc1bb38d 100644
--- a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
+++ b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
@@ -14,11 +14,13 @@ import AccessDropdown, { i18n } from '~/projects/settings/components/access_drop
import { ACCESS_LEVELS, LEVEL_TYPES } from '~/projects/settings/constants';
jest.mock('~/projects/settings/api/access_dropdown_api', () => ({
- getGroups: jest.fn().mockResolvedValue([
- { id: 4, name: 'group4' },
- { id: 5, name: 'group5' },
- { id: 6, name: 'group6' },
- ]),
+ getGroups: jest.fn().mockResolvedValue({
+ data: [
+ { id: 4, name: 'group4' },
+ { id: 5, name: 'group5' },
+ { id: 6, name: 'group6' },
+ ],
+ }),
getUsers: jest.fn().mockResolvedValue({
data: [
{ id: 7, name: 'user7' },
diff --git a/spec/frontend/ref/components/ambiguous_ref_modal_spec.js b/spec/frontend/ref/components/ambiguous_ref_modal_spec.js
new file mode 100644
index 00000000000..bb3fd0fa1f0
--- /dev/null
+++ b/spec/frontend/ref/components/ambiguous_ref_modal_spec.js
@@ -0,0 +1,64 @@
+import { GlModal, GlSprintf } from '@gitlab/ui';
+import AmbiguousRefModal from '~/ref/components/ambiguous_ref_modal.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { TEST_HOST } from 'spec/test_constants';
+
+jest.mock('~/lib/utils/url_utility');
+
+describe('AmbiguousRefModal component', () => {
+ let wrapper;
+ const showModalSpy = jest.fn();
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(AmbiguousRefModal, {
+ propsData: { refName: 'main' },
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ methods: {
+ show: showModalSpy,
+ },
+ template: RENDER_ALL_SLOTS_TEMPLATE,
+ }),
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => createComponent());
+
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findByText = (text) => wrapper.findByText(text);
+ const findViewTagButton = () => findByText('View tag');
+ const findViewBranchButton = () => findByText('View branch');
+
+ it('renders a GlModal component with the correct props', () => {
+ expect(showModalSpy).toHaveBeenCalled();
+ expect(findModal().props('title')).toBe('Which reference do you want to view?');
+ });
+
+ it('renders a description', () => {
+ expect(wrapper.text()).toContain('There is a branch and a tag with the same name of main.');
+ expect(wrapper.text()).toContain('Which reference would you like to view?');
+ });
+
+ it('renders action buttons', () => {
+ expect(findViewTagButton().exists()).toBe(true);
+ expect(findViewBranchButton().exists()).toBe(true);
+ });
+
+ describe('when clicking the action buttons', () => {
+ it('redirects to the tag ref when tag button is clicked', () => {
+ findViewTagButton().vm.$emit('click');
+
+ expect(visitUrl).toHaveBeenCalledWith(`${TEST_HOST}/?ref_type=tags`);
+ });
+
+ it('redirects to the branch ref when branch button is clicked', () => {
+ findViewBranchButton().vm.$emit('click');
+
+ expect(visitUrl).toHaveBeenCalledWith(`${TEST_HOST}/?ref_type=heads`);
+ });
+ });
+});
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index 12ca0d053e9..26010a1cfa6 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -23,13 +23,17 @@ import {
REF_TYPE_BRANCHES,
REF_TYPE_TAGS,
REF_TYPE_COMMITS,
+ BRANCH_REF_TYPE_ICON,
+ TAG_REF_TYPE_ICON,
} from '~/ref/constants';
import createStore from '~/ref/stores/';
Vue.use(Vuex);
describe('Ref selector component', () => {
- const fixtures = { branches, tags, commit };
+ const branchRefTypeMock = { name: 'refs/heads/test_branch' };
+ const tagRefTypeMock = { name: 'refs/tags/test_tag' };
+ const fixtures = { branches: [branchRefTypeMock, tagRefTypeMock, ...branches], tags, commit };
const projectId = '8';
const totalBranchesCount = 123;
@@ -614,6 +618,19 @@ describe('Ref selector component', () => {
});
it.each`
+ selectedBranch | icon
+ ${branchRefTypeMock.name} | ${BRANCH_REF_TYPE_ICON}
+ ${tagRefTypeMock.name} | ${TAG_REF_TYPE_ICON}
+ ${branches[0].name} | ${''}
+ `('renders the correct icon for the selected ref', async ({ selectedBranch, icon }) => {
+ createComponent();
+ findListbox().vm.$emit('select', selectedBranch);
+ await nextTick();
+
+ expect(findListbox().props('icon')).toBe(icon);
+ });
+
+ it.each`
enabledRefType | findVisibleSection | findHiddenSections
${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]}
${REF_TYPE_TAGS} | ${findTagsSection} | ${[findBranchesSection, findCommitsSection]}
diff --git a/spec/frontend/ref/init_ambiguous_ref_modal_spec.js b/spec/frontend/ref/init_ambiguous_ref_modal_spec.js
new file mode 100644
index 00000000000..322978f598f
--- /dev/null
+++ b/spec/frontend/ref/init_ambiguous_ref_modal_spec.js
@@ -0,0 +1,48 @@
+import Vue from 'vue';
+import initAmbiguousRefModal from '~/ref/init_ambiguous_ref_modal';
+import AmbiguousRefModal from '~/ref/components/ambiguous_ref_modal.vue';
+import { setHTMLFixture } from 'helpers/fixtures';
+import setWindowLocation from 'helpers/set_window_location_helper';
+
+const generateFixture = (isAmbiguous) => {
+ return `<div id="js-ambiguous-ref-modal" data-ambiguous="${isAmbiguous}" data-ref="main"></div>`;
+};
+
+const init = ({ isAmbiguous, htmlFixture = generateFixture(isAmbiguous) }) => {
+ setHTMLFixture(htmlFixture);
+ initAmbiguousRefModal();
+};
+
+beforeEach(() => jest.spyOn(Vue, 'extend'));
+
+describe('initAmbiguousRefModal', () => {
+ it('inits a new AmbiguousRefModal Vue component', () => {
+ init({ isAmbiguous: true });
+ expect(Vue.extend).toHaveBeenCalledWith(AmbiguousRefModal);
+ });
+
+ it.each(['<div></div>', '', null])(
+ 'does not render a new AmbiguousRefModal Vue component when root element is %s',
+ (htmlFixture) => {
+ init({ isAmbiguous: true, htmlFixture });
+
+ expect(Vue.extend).not.toHaveBeenCalledWith(AmbiguousRefModal);
+ },
+ );
+
+ it('does not render a new AmbiguousRefModal Vue component "ambiguous" data attribute is "false"', () => {
+ init({ isAmbiguous: false });
+
+ expect(Vue.extend).not.toHaveBeenCalledWith(AmbiguousRefModal);
+ });
+
+ it.each(['tags', 'heads'])(
+ 'does not render a new AmbiguousRefModal Vue component when "ref_type" param is set to %s',
+ (refType) => {
+ setWindowLocation(`?ref_type=${refType}`);
+ init({ isAmbiguous: true });
+
+ expect(Vue.extend).not.toHaveBeenCalledWith(AmbiguousRefModal);
+ },
+ );
+});
diff --git a/spec/frontend/releases/components/tag_field_new_spec.js b/spec/frontend/releases/components/tag_field_new_spec.js
index 3468338b8a7..e155cdbbd3c 100644
--- a/spec/frontend/releases/components/tag_field_new_spec.js
+++ b/spec/frontend/releases/components/tag_field_new_spec.js
@@ -1,4 +1,4 @@
-import { GlFormGroup, GlDropdown, GlPopover } from '@gitlab/ui';
+import { GlFormGroup, GlTruncate, GlPopover } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
@@ -60,7 +60,7 @@ describe('releases/components/tag_field_new', () => {
afterEach(() => mock.restore());
const findTagNameFormGroup = () => wrapper.findComponent(GlFormGroup);
- const findTagNameInput = () => wrapper.findComponent(GlDropdown);
+ const findTagNameInputText = () => wrapper.findComponent(GlTruncate);
const findTagNamePopover = () => wrapper.findComponent(GlPopover);
const findTagNameSearch = () => wrapper.findComponent(TagSearch);
const findTagNameCreate = () => wrapper.findComponent(TagCreate);
@@ -99,9 +99,10 @@ describe('releases/components/tag_field_new', () => {
it("updates the store's release.tagName property", async () => {
findTagNameCreate().vm.$emit('change', NONEXISTENT_TAG_NAME);
await findTagNameCreate().vm.$emit('create');
-
expect(store.state.editNew.release.tagName).toBe(NONEXISTENT_TAG_NAME);
- expect(findTagNameInput().props('text')).toBe(NONEXISTENT_TAG_NAME);
+
+ const text = findTagNameInputText();
+ expect(text.props('text')).toBe(NONEXISTENT_TAG_NAME);
});
});
@@ -114,8 +115,10 @@ describe('releases/components/tag_field_new', () => {
});
it("updates the store's release.tagName property", () => {
+ const buttonText = findTagNameInputText();
expect(store.state.editNew.release.tagName).toBe(updatedTagName);
- expect(findTagNameInput().props('text')).toBe(updatedTagName);
+
+ expect(buttonText.props('text')).toBe(updatedTagName);
});
it('hides the "Create from" field', () => {
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index 1d164b9f5c1..d18437ccec3 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -1,9 +1,11 @@
import { cloneDeep } from 'lodash';
import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
import testAction from 'helpers/vuex_action_helper';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import { getTag } from '~/api/tags_api';
import { createAlert } from '~/alert';
import { redirectTo } from '~/lib/utils/url_utility'; // eslint-disable-line import/no-deprecated
+import AccessorUtilities from '~/lib/utils/accessor';
import { s__ } from '~/locale';
import { ASSET_LINK_TYPE } from '~/releases/constants';
import createReleaseAssetLinkMutation from '~/releases/graphql/mutations/create_release_link.mutation.graphql';
@@ -20,6 +22,7 @@ jest.mock('~/api/tags_api');
jest.mock('~/alert');
+jest.mock('~/lib/utils/accessor');
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
@@ -34,78 +37,203 @@ jest.mock('~/releases/util', () => ({
}));
describe('Release edit/new actions', () => {
+ useLocalStorageSpy();
+
let state;
let releaseResponse;
let error;
const projectPath = 'test/project-path';
+ const draftActions = [{ type: 'saveDraftRelease' }, { type: 'saveDraftCreateFrom' }];
const setupState = (updates = {}) => {
state = {
...createState({
projectPath,
projectId: '18',
- isExistingRelease: true,
+ isExistingRelease: false,
tagName: releaseResponse.tag_name,
releasesPagePath: 'path/to/releases/page',
markdownDocsPath: 'path/to/markdown/docs',
markdownPreviewPath: 'path/to/markdown/preview',
}),
+ localStorageKey: `${projectPath}/release/new`,
+ localStorageCreateFromKey: `${projectPath}/release/new/createFrom`,
...updates,
};
};
beforeEach(() => {
+ AccessorUtilities.canUseLocalStorage.mockReturnValue(true);
releaseResponse = cloneDeep(originalOneReleaseForEditingQueryResponse);
gon.api_version = 'v4';
error = new Error('Yikes!');
createAlert.mockClear();
});
+ afterEach(() => {
+ jest.clearAllMocks();
+ });
+
describe('when creating a new release', () => {
beforeEach(() => {
setupState({ isExistingRelease: false });
});
describe('initializeRelease', () => {
- it(`commits ${types.INITIALIZE_EMPTY_RELEASE}`, () => {
- testAction(actions.initializeRelease, undefined, state, [
- { type: types.INITIALIZE_EMPTY_RELEASE },
- ]);
+ it('dispatches loadDraftRelease', () => {
+ return testAction({
+ action: actions.initializeRelease,
+ state,
+ expectedMutations: [],
+ expectedActions: [{ type: 'loadDraftRelease' }],
+ });
+ });
+ });
+
+ describe('loadDraftRelease', () => {
+ it(`with no saved release, it commits ${types.INITIALIZE_EMPTY_RELEASE}`, () => {
+ testAction({
+ action: actions.loadDraftRelease,
+ state,
+ expectedMutations: [{ type: types.INITIALIZE_EMPTY_RELEASE }],
+ });
+ });
+
+ it('with saved release, loads the release from local storage', () => {
+ const release = {
+ tagName: 'v1.3',
+ tagMessage: 'hello',
+ name: '',
+ description: '',
+ milestones: [],
+ groupMilestones: [],
+ releasedAt: new Date(),
+ assets: {
+ links: [],
+ },
+ };
+ const createFrom = 'main';
+
+ window.localStorage.setItem(`${state.projectPath}/release/new`, JSON.stringify(release));
+ window.localStorage.setItem(
+ `${state.projectPath}/release/new/createFrom`,
+ JSON.stringify(createFrom),
+ );
+
+ return testAction({
+ action: actions.loadDraftRelease,
+ state,
+ expectedMutations: [
+ { type: types.INITIALIZE_RELEASE, payload: release },
+ { type: types.UPDATE_CREATE_FROM, payload: createFrom },
+ ],
+ });
+ });
+ });
+
+ describe('clearDraftRelease', () => {
+ it('calls window.localStorage.clear', () => {
+ return testAction({ action: actions.clearDraftRelease, state }).then(() => {
+ expect(window.localStorage.removeItem).toHaveBeenCalledTimes(2);
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith(state.localStorageKey);
+ expect(window.localStorage.removeItem).toHaveBeenCalledWith(
+ state.localStorageCreateFromKey,
+ );
+ });
+ });
+ });
+
+ describe('saveDraftCreateFrom', () => {
+ it('saves the create from to local storage', () => {
+ const createFrom = 'main';
+ setupState({ createFrom });
+ return testAction({ action: actions.saveDraftCreateFrom, state }).then(() => {
+ expect(window.localStorage.setItem).toHaveBeenCalledTimes(1);
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(
+ state.localStorageCreateFromKey,
+ JSON.stringify(createFrom),
+ );
+ });
+ });
+ });
+
+ describe('saveDraftRelease', () => {
+ let release;
+
+ beforeEach(() => {
+ release = {
+ tagName: 'v1.3',
+ tagMessage: 'hello',
+ name: '',
+ description: '',
+ milestones: [],
+ groupMilestones: [],
+ releasedAt: new Date(),
+ assets: {
+ links: [],
+ },
+ };
+ });
+
+ it('saves the draft release to local storage', () => {
+ setupState({ release, releasedAtChanged: true });
+
+ return testAction({ action: actions.saveDraftRelease, state }).then(() => {
+ expect(window.localStorage.setItem).toHaveBeenCalledTimes(1);
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(
+ state.localStorageKey,
+ JSON.stringify(state.release),
+ );
+ });
+ });
+
+ it('ignores the released at date if it has not been changed', () => {
+ setupState({ release, releasedAtChanged: false });
+
+ return testAction({ action: actions.saveDraftRelease, state }).then(() => {
+ expect(window.localStorage.setItem).toHaveBeenCalledTimes(1);
+ expect(window.localStorage.setItem).toHaveBeenCalledWith(
+ state.localStorageKey,
+ JSON.stringify({ ...state.release, releasedAt: undefined }),
+ );
+ });
});
});
describe('saveRelease', () => {
it(`commits ${types.REQUEST_SAVE_RELEASE} and then dispatched "createRelease"`, () => {
- testAction(
- actions.saveRelease,
- undefined,
+ testAction({
+ action: actions.saveRelease,
state,
- [{ type: types.REQUEST_SAVE_RELEASE }],
- [{ type: 'createRelease' }],
- );
+ expectedMutations: [{ type: types.REQUEST_SAVE_RELEASE }],
+ expectedActions: [{ type: 'createRelease' }],
+ });
});
});
});
describe('when editing an existing release', () => {
- beforeEach(setupState);
+ beforeEach(() => setupState({ isExistingRelease: true }));
describe('initializeRelease', () => {
it('dispatches "fetchRelease"', () => {
- testAction(actions.initializeRelease, undefined, state, [], [{ type: 'fetchRelease' }]);
+ testAction({
+ action: actions.initializeRelease,
+ state,
+ expectedActions: [{ type: 'fetchRelease' }],
+ });
});
});
describe('saveRelease', () => {
it(`commits ${types.REQUEST_SAVE_RELEASE} and then dispatched "updateRelease"`, () => {
- testAction(
- actions.saveRelease,
- undefined,
+ testAction({
+ action: actions.saveRelease,
state,
- [{ type: types.REQUEST_SAVE_RELEASE }],
- [{ type: 'updateRelease' }],
- );
+ expectedMutations: [{ type: types.REQUEST_SAVE_RELEASE }],
+ expectedActions: [{ type: 'updateRelease' }],
+ });
});
});
});
@@ -120,15 +248,19 @@ describe('Release edit/new actions', () => {
});
it(`commits ${types.REQUEST_RELEASE} and then commits ${types.RECEIVE_RELEASE_SUCCESS} with the converted release object`, () => {
- return testAction(actions.fetchRelease, undefined, state, [
- {
- type: types.REQUEST_RELEASE,
- },
- {
- type: types.RECEIVE_RELEASE_SUCCESS,
- payload: convertOneReleaseGraphQLResponse(releaseResponse).data,
- },
- ]);
+ return testAction({
+ action: actions.fetchRelease,
+ state,
+ expectedMutations: [
+ {
+ type: types.REQUEST_RELEASE,
+ },
+ {
+ type: types.RECEIVE_RELEASE_SUCCESS,
+ payload: convertOneReleaseGraphQLResponse(releaseResponse).data,
+ },
+ ],
+ });
});
});
@@ -138,15 +270,19 @@ describe('Release edit/new actions', () => {
});
it(`commits ${types.REQUEST_RELEASE} and then commits ${types.RECEIVE_RELEASE_ERROR} with an error object`, () => {
- return testAction(actions.fetchRelease, undefined, state, [
- {
- type: types.REQUEST_RELEASE,
- },
- {
- type: types.RECEIVE_RELEASE_ERROR,
- payload: expect.any(Error),
- },
- ]);
+ return testAction({
+ action: actions.fetchRelease,
+ state,
+ expectedMutations: [
+ {
+ type: types.REQUEST_RELEASE,
+ },
+ {
+ type: types.RECEIVE_RELEASE_ERROR,
+ payload: expect.any(Error),
+ },
+ ],
+ });
});
it(`shows an alert message`, () => {
@@ -163,89 +299,140 @@ describe('Release edit/new actions', () => {
describe('updateReleaseTagName', () => {
it(`commits ${types.UPDATE_RELEASE_TAG_NAME} with the updated tag name`, () => {
const newTag = 'updated-tag-name';
- return testAction(actions.updateReleaseTagName, newTag, state, [
- { type: types.UPDATE_RELEASE_TAG_NAME, payload: newTag },
- ]);
+ return testAction({
+ action: actions.updateReleaseTagName,
+ payload: newTag,
+ state,
+ expectedMutations: [{ type: types.UPDATE_RELEASE_TAG_NAME, payload: newTag }],
+ expectedActions: draftActions,
+ });
+ });
+ it('does not save drafts when editing', () => {
+ const newTag = 'updated-tag-name';
+ return testAction({
+ action: actions.updateReleaseTagName,
+ payload: newTag,
+ state: { ...state, isExistingRelease: true },
+ expectedMutations: [{ type: types.UPDATE_RELEASE_TAG_NAME, payload: newTag }],
+ });
});
});
describe('updateReleaseTagMessage', () => {
it(`commits ${types.UPDATE_RELEASE_TAG_MESSAGE} with the updated tag name`, () => {
const newMessage = 'updated-tag-message';
- return testAction(actions.updateReleaseTagMessage, newMessage, state, [
- { type: types.UPDATE_RELEASE_TAG_MESSAGE, payload: newMessage },
- ]);
+ return testAction({
+ action: actions.updateReleaseTagMessage,
+ payload: newMessage,
+ state,
+ expectedMutations: [{ type: types.UPDATE_RELEASE_TAG_MESSAGE, payload: newMessage }],
+ expectedActions: draftActions,
+ });
});
});
describe('updateReleasedAt', () => {
it(`commits ${types.UPDATE_RELEASED_AT} with the updated date`, () => {
const newDate = new Date();
- return testAction(actions.updateReleasedAt, newDate, state, [
- { type: types.UPDATE_RELEASED_AT, payload: newDate },
- ]);
+ return testAction({
+ action: actions.updateReleasedAt,
+ payload: newDate,
+ state,
+ expectedMutations: [{ type: types.UPDATE_RELEASED_AT, payload: newDate }],
+ expectedActions: draftActions,
+ });
});
});
describe('updateCreateFrom', () => {
it(`commits ${types.UPDATE_CREATE_FROM} with the updated ref`, () => {
const newRef = 'my-feature-branch';
- return testAction(actions.updateCreateFrom, newRef, state, [
- { type: types.UPDATE_CREATE_FROM, payload: newRef },
- ]);
+ return testAction({
+ action: actions.updateCreateFrom,
+ payload: newRef,
+ state,
+ expectedMutations: [{ type: types.UPDATE_CREATE_FROM, payload: newRef }],
+ expectedActions: draftActions,
+ });
});
});
describe('updateShowCreateFrom', () => {
it(`commits ${types.UPDATE_SHOW_CREATE_FROM} with the updated ref`, () => {
const newRef = 'my-feature-branch';
- return testAction(actions.updateShowCreateFrom, newRef, state, [
- { type: types.UPDATE_SHOW_CREATE_FROM, payload: newRef },
- ]);
+ return testAction({
+ action: actions.updateShowCreateFrom,
+ payload: newRef,
+ state,
+ expectedMutations: [{ type: types.UPDATE_SHOW_CREATE_FROM, payload: newRef }],
+ });
});
});
describe('updateReleaseTitle', () => {
it(`commits ${types.UPDATE_RELEASE_TITLE} with the updated release title`, () => {
const newTitle = 'The new release title';
- return testAction(actions.updateReleaseTitle, newTitle, state, [
- { type: types.UPDATE_RELEASE_TITLE, payload: newTitle },
- ]);
+ return testAction({
+ action: actions.updateReleaseTitle,
+ payload: newTitle,
+ state,
+ expectedMutations: [{ type: types.UPDATE_RELEASE_TITLE, payload: newTitle }],
+ expectedActions: draftActions,
+ });
});
});
describe('updateReleaseNotes', () => {
it(`commits ${types.UPDATE_RELEASE_NOTES} with the updated release notes`, () => {
const newReleaseNotes = 'The new release notes';
- return testAction(actions.updateReleaseNotes, newReleaseNotes, state, [
- { type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes },
- ]);
+ return testAction({
+ action: actions.updateReleaseNotes,
+ payload: newReleaseNotes,
+ state,
+ expectedMutations: [{ type: types.UPDATE_RELEASE_NOTES, payload: newReleaseNotes }],
+ expectedActions: draftActions,
+ });
});
});
describe('updateReleaseMilestones', () => {
it(`commits ${types.UPDATE_RELEASE_MILESTONES} with the updated release milestones`, () => {
const newReleaseMilestones = ['v0.0', 'v0.1'];
- return testAction(actions.updateReleaseMilestones, newReleaseMilestones, state, [
- { type: types.UPDATE_RELEASE_MILESTONES, payload: newReleaseMilestones },
- ]);
+ return testAction({
+ action: actions.updateReleaseMilestones,
+ payload: newReleaseMilestones,
+ state,
+ expectedMutations: [
+ { type: types.UPDATE_RELEASE_MILESTONES, payload: newReleaseMilestones },
+ ],
+ expectedActions: draftActions,
+ });
});
});
describe('updateReleaseGroupMilestones', () => {
it(`commits ${types.UPDATE_RELEASE_GROUP_MILESTONES} with the updated release group milestones`, () => {
const newReleaseGroupMilestones = ['v0.0', 'v0.1'];
- return testAction(actions.updateReleaseGroupMilestones, newReleaseGroupMilestones, state, [
- { type: types.UPDATE_RELEASE_GROUP_MILESTONES, payload: newReleaseGroupMilestones },
- ]);
+ return testAction({
+ action: actions.updateReleaseGroupMilestones,
+ payload: newReleaseGroupMilestones,
+ state,
+ expectedMutations: [
+ { type: types.UPDATE_RELEASE_GROUP_MILESTONES, payload: newReleaseGroupMilestones },
+ ],
+ expectedActions: draftActions,
+ });
});
});
describe('addEmptyAssetLink', () => {
it(`commits ${types.ADD_EMPTY_ASSET_LINK}`, () => {
- return testAction(actions.addEmptyAssetLink, undefined, state, [
- { type: types.ADD_EMPTY_ASSET_LINK },
- ]);
+ return testAction({
+ action: actions.addEmptyAssetLink,
+ state,
+ expectedMutations: [{ type: types.ADD_EMPTY_ASSET_LINK }],
+ expectedActions: draftActions,
+ });
});
});
@@ -256,9 +443,13 @@ describe('Release edit/new actions', () => {
newUrl: 'https://example.com/updated',
};
- return testAction(actions.updateAssetLinkUrl, params, state, [
- { type: types.UPDATE_ASSET_LINK_URL, payload: params },
- ]);
+ return testAction({
+ action: actions.updateAssetLinkUrl,
+ payload: params,
+ state,
+ expectedMutations: [{ type: types.UPDATE_ASSET_LINK_URL, payload: params }],
+ expectedActions: draftActions,
+ });
});
});
@@ -269,9 +460,13 @@ describe('Release edit/new actions', () => {
newName: 'Updated link name',
};
- return testAction(actions.updateAssetLinkName, params, state, [
- { type: types.UPDATE_ASSET_LINK_NAME, payload: params },
- ]);
+ return testAction({
+ action: actions.updateAssetLinkName,
+ payload: params,
+ state,
+ expectedMutations: [{ type: types.UPDATE_ASSET_LINK_NAME, payload: params }],
+ expectedActions: draftActions,
+ });
});
});
@@ -282,30 +477,45 @@ describe('Release edit/new actions', () => {
newType: ASSET_LINK_TYPE.RUNBOOK,
};
- return testAction(actions.updateAssetLinkType, params, state, [
- { type: types.UPDATE_ASSET_LINK_TYPE, payload: params },
- ]);
+ return testAction({
+ action: actions.updateAssetLinkType,
+ payload: params,
+ state,
+ expectedMutations: [{ type: types.UPDATE_ASSET_LINK_TYPE, payload: params }],
+ expectedActions: draftActions,
+ });
});
});
describe('removeAssetLink', () => {
it(`commits ${types.REMOVE_ASSET_LINK} with the ID of the asset link to remove`, () => {
const idToRemove = 2;
- return testAction(actions.removeAssetLink, idToRemove, state, [
- { type: types.REMOVE_ASSET_LINK, payload: idToRemove },
- ]);
+ return testAction({
+ action: actions.removeAssetLink,
+ payload: idToRemove,
+ state,
+ expectedMutations: [{ type: types.REMOVE_ASSET_LINK, payload: idToRemove }],
+ expectedActions: draftActions,
+ });
});
});
describe('receiveSaveReleaseSuccess', () => {
- it(`commits ${types.RECEIVE_SAVE_RELEASE_SUCCESS}`, () =>
- testAction(actions.receiveSaveReleaseSuccess, releaseResponse, state, [
- { type: types.RECEIVE_SAVE_RELEASE_SUCCESS },
- ]));
+ it(`commits ${types.RECEIVE_SAVE_RELEASE_SUCCESS} and dispatches clearDraftRelease`, () =>
+ testAction({
+ action: actions.receiveSaveReleaseSuccess,
+ payload: releaseResponse,
+ state,
+ expectedMutations: [{ type: types.RECEIVE_SAVE_RELEASE_SUCCESS }],
+ expectedActions: [{ type: 'clearDraftRelease' }],
+ }));
it("redirects to the release's dedicated page", () => {
const { selfUrl } = releaseResponse.data.project.release.links;
- actions.receiveSaveReleaseSuccess({ commit: jest.fn(), state }, selfUrl);
+ actions.receiveSaveReleaseSuccess(
+ { commit: jest.fn(), state, dispatch: jest.fn() },
+ selfUrl,
+ );
expect(redirectTo).toHaveBeenCalledTimes(1); // eslint-disable-line import/no-deprecated
expect(redirectTo).toHaveBeenCalledWith(selfUrl); // eslint-disable-line import/no-deprecated
});
@@ -346,18 +556,16 @@ describe('Release edit/new actions', () => {
});
it(`dispatches "receiveSaveReleaseSuccess" with the converted release object`, () => {
- return testAction(
- actions.createRelease,
- undefined,
+ return testAction({
+ action: actions.createRelease,
state,
- [],
- [
+ expectedActions: [
{
type: 'receiveSaveReleaseSuccess',
payload: selfUrl,
},
],
- );
+ });
});
});
@@ -367,12 +575,16 @@ describe('Release edit/new actions', () => {
});
it(`commits ${types.RECEIVE_SAVE_RELEASE_ERROR} with an error object`, () => {
- return testAction(actions.createRelease, undefined, state, [
- {
- type: types.RECEIVE_SAVE_RELEASE_ERROR,
- payload: expect.any(Error),
- },
- ]);
+ return testAction({
+ action: actions.createRelease,
+ state,
+ expectedMutations: [
+ {
+ type: types.RECEIVE_SAVE_RELEASE_ERROR,
+ payload: expect.any(Error),
+ },
+ ],
+ });
});
it(`shows an alert message`, () => {
@@ -393,12 +605,16 @@ describe('Release edit/new actions', () => {
});
it(`commits ${types.RECEIVE_SAVE_RELEASE_ERROR} with an error object`, () => {
- return testAction(actions.createRelease, undefined, state, [
- {
- type: types.RECEIVE_SAVE_RELEASE_ERROR,
- payload: expect.any(Error),
- },
- ]);
+ return testAction({
+ action: actions.createRelease,
+ state,
+ expectedMutations: [
+ {
+ type: types.RECEIVE_SAVE_RELEASE_ERROR,
+ payload: expect.any(Error),
+ },
+ ],
+ });
});
it(`shows an alert message`, () => {
@@ -760,16 +976,15 @@ describe('Release edit/new actions', () => {
const tag = { message: 'this is a tag' };
getTag.mockResolvedValue({ data: tag });
- await testAction(
- actions.fetchTagNotes,
- tagName,
+ await testAction({
+ action: actions.fetchTagNotes,
+ payload: tagName,
state,
- [
+ expectedMutations: [
{ type: types.REQUEST_TAG_NOTES },
{ type: types.RECEIVE_TAG_NOTES_SUCCESS, payload: tag },
],
- [],
- );
+ });
expect(getTag).toHaveBeenCalledWith(state.projectId, tagName);
});
@@ -777,16 +992,15 @@ describe('Release edit/new actions', () => {
error = new Error();
getTag.mockRejectedValue(error);
- await testAction(
- actions.fetchTagNotes,
- tagName,
+ await testAction({
+ action: actions.fetchTagNotes,
+ payload: tagName,
state,
- [
+ expectedMutations: [
{ type: types.REQUEST_TAG_NOTES },
{ type: types.RECEIVE_TAG_NOTES_ERROR, payload: error },
],
- [],
- );
+ });
expect(createAlert).toHaveBeenCalledWith({
message: s__('Release|Unable to fetch the tag notes.'),
diff --git a/spec/frontend/releases/stores/modules/detail/getters_spec.js b/spec/frontend/releases/stores/modules/detail/getters_spec.js
index 736eae13fb3..24490e19296 100644
--- a/spec/frontend/releases/stores/modules/detail/getters_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/getters_spec.js
@@ -470,4 +470,20 @@ describe('Release edit/new getters', () => {
expect(getters.releasedAtChanged({ originalReleasedAt, release: { releasedAt } })).toBe(true);
});
});
+
+ describe('localStorageKey', () => {
+ it('returns a string key with the project path for local storage', () => {
+ const projectPath = 'test/project';
+ expect(getters.localStorageKey({ projectPath })).toBe('test/project/release/new');
+ });
+ });
+
+ describe('localStorageCreateFromKey', () => {
+ it('returns a string key with the project path for local storage', () => {
+ const projectPath = 'test/project';
+ expect(getters.localStorageCreateFromKey({ projectPath })).toBe(
+ 'test/project/release/new/createFrom',
+ );
+ });
+ });
});
diff --git a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
index 3f901dc61b8..1a5301c5525 100644
--- a/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
+++ b/spec/frontend/repository/components/__snapshots__/last_commit_spec.js.snap
@@ -1,97 +1,50 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Repository last commit component renders commit widget 1`] = `
-<div
- class="commit gl-display-flex gl-p-5 gl-w-full well-segment"
+<commit-info-stub
+ commit="[object Object]"
>
- <user-avatar-link-stub
- class="gl-mr-4 gl-my-2"
- imgalt=""
- imgcssclasses=""
- imgcsswrapperclasses=""
- imgsize="32"
- imgsrc="https://test.com"
- linkhref="/test"
- popoveruserid=""
- popoverusername=""
- tooltipplacement="top"
- tooltiptext=""
- username=""
- />
<div
- class="commit-detail flex-list gl-align-items-center gl-display-flex gl-flex-grow-1 gl-justify-content-space-between gl-min-w-0"
+ class="commit-actions gl-align-items-center gl-display-flex gl-flex-align gl-flex-direction-row"
>
<div
- class="commit-content"
- data-qa-selector="commit_content"
+ class="ci-status-link"
>
- <gl-link-stub
- class="commit-row-message item-title"
- href="/commit/123"
- >
- Commit title
- </gl-link-stub>
- <div
- class="committer"
- >
- <gl-link-stub
- class="commit-author-link js-user-link"
- href="/test"
- >
- Test
- </gl-link-stub>
- authored
- <timeago-tooltip-stub
- cssclass=""
- datetimeformat="DATE_WITH_TIME_FORMAT"
- time="2019-01-01"
- tooltipplacement="bottom"
- />
- </div>
+ <ci-badge-link-stub
+ aria-label="Pipeline: failed"
+ class="js-commit-pipeline"
+ details-path="https://test.com/pipeline"
+ showtooltip="true"
+ size="md"
+ status="[object Object]"
+ uselink="true"
+ />
</div>
- <div
- class="gl-flex-grow-1"
- />
- <div
- class="commit-actions gl-align-items-center gl-display-flex gl-flex-align gl-flex-direction-row"
+ <gl-button-group-stub
+ class="gl-ml-4 js-commit-sha-group"
>
- <div
- class="ci-status-link"
- >
- <ci-badge-link-stub
- aria-label="Pipeline: failed"
- class="js-commit-pipeline"
- details-path="https://test.com/pipeline"
- size="lg"
- status="[object Object]"
- />
- </div>
- <gl-button-group-stub
- class="gl-ml-4 js-commit-sha-group"
+ <gl-button-stub
+ buttontextclasses=""
+ category="primary"
+ class="gl-font-monospace"
+ data-testid="last-commit-id-label"
+ icon=""
+ label="true"
+ size="medium"
+ variant="default"
>
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- class="gl-font-monospace"
- data-testid="last-commit-id-label"
- icon=""
- label="true"
- size="medium"
- variant="default"
- >
- 12345678
- </gl-button-stub>
- <clipboard-button-stub
- category="secondary"
- class="input-group-text"
- size="medium"
- text="123456789"
- title="Copy commit SHA"
- tooltipplacement="top"
- variant="default"
- />
- </gl-button-group-stub>
- </div>
+ 12345678
+ </gl-button-stub>
+ <clipboard-button-stub
+ category="secondary"
+ class="input-group-text"
+ size="medium"
+ text="123456789"
+ title="Copy commit SHA"
+ tooltipplacement="top"
+ variant="default"
+ />
+ </gl-button-group-stub>
</div>
-</div>
+</commit-info-stub>
`;
diff --git a/spec/frontend/repository/components/commit_info_spec.js b/spec/frontend/repository/components/commit_info_spec.js
new file mode 100644
index 00000000000..34e941aa858
--- /dev/null
+++ b/spec/frontend/repository/components/commit_info_spec.js
@@ -0,0 +1,87 @@
+import { nextTick } from 'vue';
+import { GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CommitInfo from '~/repository/components/commit_info.vue';
+import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
+
+let wrapper;
+const commit = {
+ title: 'Commit title',
+ titleHtml: 'Commit title html',
+ message: 'Commit message',
+ authoredDate: '2019-01-01',
+ authorName: 'Test authorName',
+ author: { name: 'Test name', avatarUrl: 'https://test.com', webPath: '/test' },
+};
+
+const findTextExpander = () => wrapper.findComponent(GlButton);
+const findUserLink = () => wrapper.findByText(commit.author.name);
+const findUserAvatarLink = () => wrapper.findComponent(UserAvatarLink);
+const findAuthorName = () => wrapper.findByText(`${commit.authorName} authored`);
+const findCommitRowDescription = () => wrapper.find('pre');
+const findTitleHtml = () => wrapper.findByText(commit.titleHtml);
+
+const createComponent = async ({ commitMock = {} } = {}) => {
+ wrapper = shallowMountExtended(CommitInfo, {
+ propsData: { commit: { ...commit, ...commitMock } },
+ });
+
+ await nextTick();
+};
+
+describe('Repository last commit component', () => {
+ it('renders author info', () => {
+ createComponent();
+
+ expect(findUserLink().exists()).toBe(true);
+ expect(findUserAvatarLink().exists()).toBe(true);
+ });
+
+ it('hides author component when author does not exist', () => {
+ createComponent({ commitMock: { author: null } });
+
+ expect(findUserLink().exists()).toBe(false);
+ expect(findUserAvatarLink().exists()).toBe(false);
+ expect(findAuthorName().exists()).toBe(true);
+ });
+
+ it('does not render description expander when description is null', () => {
+ createComponent();
+
+ expect(findTextExpander().exists()).toBe(false);
+ expect(findCommitRowDescription().exists()).toBe(false);
+ });
+
+ describe('when the description is present', () => {
+ beforeEach(() => {
+ createComponent({ commitMock: { descriptionHtml: '&#x000A;Update ADOPTERS.md' } });
+ });
+
+ it('strips the first newline of the description', () => {
+ expect(findCommitRowDescription().html()).toBe(
+ '<pre class="commit-row-description gl-mb-3 gl-white-space-pre-line">Update ADOPTERS.md</pre>',
+ );
+ });
+
+ it('renders commit description collapsed by default', () => {
+ expect(findCommitRowDescription().classes('gl-display-block!')).toBe(false);
+ expect(findTextExpander().classes('open')).toBe(false);
+ expect(findTextExpander().props('selected')).toBe(false);
+ });
+
+ it('expands commit description when clicking expander', async () => {
+ findTextExpander().vm.$emit('click');
+ await nextTick();
+
+ expect(findCommitRowDescription().classes('gl-display-block!')).toBe(true);
+ expect(findTextExpander().classes('open')).toBe(true);
+ expect(findTextExpander().props('selected')).toBe(true);
+ });
+ });
+
+ it('sets correct CSS class if the commit message is empty', () => {
+ createComponent({ commitMock: { message: '' } });
+
+ expect(findTitleHtml().classes()).toContain('gl-font-style-italic');
+ });
+});
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index c207d32d61d..d5ec34b1f6d 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -1,29 +1,26 @@
import Vue, { nextTick } from 'vue';
-import VueApollo from 'vue-apollo';
import { GlLoadingIcon } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import LastCommit from '~/repository/components/last_commit.vue';
+import CommitInfo from '~/repository/components/commit_info.vue';
import SignatureBadge from '~/commit/components/signature_badge.vue';
-import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import eventHub from '~/repository/event_hub';
import pathLastCommitQuery from 'shared_queries/repository/path_last_commit.query.graphql';
import { FORK_UPDATED_EVENT } from '~/repository/constants';
import { refMock } from '../mock_data';
let wrapper;
+let commitData;
let mockResolver;
const findPipeline = () => wrapper.find('.js-commit-pipeline');
-const findTextExpander = () => wrapper.find('.text-expander');
-const findUserLink = () => wrapper.find('.js-user-link');
-const findUserAvatarLink = () => wrapper.findComponent(UserAvatarLink);
const findLastCommitLabel = () => wrapper.findByTestId('last-commit-id-label');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
-const findCommitRowDescription = () => wrapper.find('.commit-row-description');
const findStatusBox = () => wrapper.findComponent(SignatureBadge);
-const findItemTitle = () => wrapper.find('.item-title');
+const findCommitInfo = () => wrapper.findComponent(CommitInfo);
const defaultPipelineEdges = [
{
@@ -44,23 +41,7 @@ const defaultPipelineEdges = [
},
];
-const defaultAuthor = {
- __typename: 'UserCore',
- id: 'gid://gitlab/User/1',
- name: 'Test',
- avatarUrl: 'https://test.com',
- webPath: '/test',
-};
-
-const defaultMessage = 'Commit title';
-
-const createCommitData = ({
- pipelineEdges = defaultPipelineEdges,
- author = defaultAuthor,
- descriptionHtml = '',
- signature = null,
- message = defaultMessage,
-}) => {
+const createCommitData = ({ pipelineEdges = defaultPipelineEdges, signature = null }) => {
return {
data: {
project: {
@@ -79,13 +60,19 @@ const createCommitData = ({
sha: '123456789',
title: 'Commit title',
titleHtml: 'Commit title',
- descriptionHtml,
- message,
+ descriptionHtml: '',
+ message: '',
webPath: '/commit/123',
authoredDate: '2019-01-01',
authorName: 'Test',
authorGravatar: 'https://test.com',
- author,
+ author: {
+ __typename: 'UserCore',
+ id: 'gid://gitlab/User/1',
+ name: 'Test',
+ avatarUrl: 'https://test.com',
+ webPath: '/test',
+ },
signature,
pipelines: {
__typename: 'PipelineConnection',
@@ -101,12 +88,13 @@ const createCommitData = ({
};
};
-const createComponent = (data = {}) => {
+const createComponent = async (data = {}) => {
Vue.use(VueApollo);
const currentPath = 'path';
- mockResolver = jest.fn().mockResolvedValue(createCommitData(data));
+ commitData = createCommitData(data);
+ mockResolver = jest.fn().mockResolvedValue(commitData);
wrapper = shallowMountExtended(LastCommit, {
apolloProvider: createMockApollo([[pathLastCommitQuery, mockResolver]]),
@@ -116,8 +104,13 @@ const createComponent = (data = {}) => {
SignatureBadge,
},
});
+
+ await waitForPromises();
+ await nextTick();
};
+beforeEach(() => createComponent());
+
afterEach(() => {
mockResolver = null;
});
@@ -137,17 +130,17 @@ describe('Repository last commit component', () => {
expect(findLoadingIcon().exists()).toBe(loading);
});
- it('renders commit widget', async () => {
- createComponent();
- await waitForPromises();
+ it('renders a CommitInfo component', () => {
+ const commit = { ...commitData.project?.repository.paginatedTree.nodes[0].lastCommit };
- expect(wrapper.element).toMatchSnapshot();
+ expect(findCommitInfo().props().commit).toMatchObject(commit);
});
- it('renders short commit ID', async () => {
- createComponent();
- await waitForPromises();
+ it('renders commit widget', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ it('renders short commit ID', () => {
expect(findLastCommitLabel().text()).toBe('12345678');
});
@@ -158,29 +151,10 @@ describe('Repository last commit component', () => {
expect(findPipeline().exists()).toBe(false);
});
- it('renders pipeline components when pipeline exists', async () => {
- createComponent();
- await waitForPromises();
-
+ it('renders pipeline components when pipeline exists', () => {
expect(findPipeline().exists()).toBe(true);
});
- it('hides author component when author does not exist', async () => {
- createComponent({ author: null });
- await waitForPromises();
-
- expect(findUserLink().exists()).toBe(false);
- expect(findUserAvatarLink().exists()).toBe(false);
- });
-
- it('does not render description expander when description is null', async () => {
- createComponent();
- await waitForPromises();
-
- expect(findTextExpander().exists()).toBe(false);
- expect(findCommitRowDescription().exists()).toBe(false);
- });
-
describe('created', () => {
it('binds `epicsListScrolled` event listener via eventHub', () => {
jest.spyOn(eventHub, '$on').mockImplementation(() => {});
@@ -200,32 +174,6 @@ describe('Repository last commit component', () => {
});
});
- describe('when the description is present', () => {
- beforeEach(async () => {
- createComponent({ descriptionHtml: '&#x000A;Update ADOPTERS.md' });
- await waitForPromises();
- });
-
- it('strips the first newline of the description', () => {
- expect(findCommitRowDescription().html()).toBe(
- '<pre class="commit-row-description gl-mb-3 gl-white-space-pre-line">Update ADOPTERS.md</pre>',
- );
- });
-
- it('expands commit description when clicking expander', async () => {
- expect(findCommitRowDescription().classes('d-block')).toBe(false);
- expect(findTextExpander().classes('open')).toBe(false);
- expect(findTextExpander().props('selected')).toBe(false);
-
- findTextExpander().vm.$emit('click');
- await nextTick();
-
- expect(findCommitRowDescription().classes('d-block')).toBe(true);
- expect(findTextExpander().classes('open')).toBe(true);
- expect(findTextExpander().props('selected')).toBe(true);
- });
- });
-
it('renders the signature HTML as returned by the backend', async () => {
const signatureResponse = {
__typename: 'GpgSignature',
@@ -241,11 +189,4 @@ describe('Repository last commit component', () => {
expect(findStatusBox().props()).toMatchObject({ signature: signatureResponse });
});
-
- it('sets correct CSS class if the commit message is empty', async () => {
- createComponent({ message: '' });
- await waitForPromises();
-
- expect(findItemTitle().classes()).toContain('font-italic');
- });
});
diff --git a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
index 17ebdf8725d..af7eca6a52d 100644
--- a/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
+++ b/spec/frontend/repository/components/table/__snapshots__/row_spec.js.snap
@@ -9,7 +9,7 @@ exports[`Repository table row component renders a symlink table row 1`] = `
>
<a
class="str-truncated tree-item-link"
- data-qa-selector="file_name_link"
+ data-testid="file-name-link"
href="https://test.com"
title="test"
>
@@ -65,7 +65,7 @@ exports[`Repository table row component renders table row 1`] = `
>
<a
class="str-truncated tree-item-link"
- data-qa-selector="file_name_link"
+ data-testid="file-name-link"
href="https://test.com"
title="test"
>
@@ -121,7 +121,7 @@ exports[`Repository table row component renders table row for path with special
>
<a
class="str-truncated tree-item-link"
- data-qa-selector="file_name_link"
+ data-testid="file-name-link"
href="https://test.com"
title="test"
>
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index 8e23f9c1680..d8d2492209e 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -16,6 +16,7 @@ import BlobsFilters from '~/search/sidebar/components/blobs_filters.vue';
import ProjectsFilters from '~/search/sidebar/components/projects_filters.vue';
import NotesFilters from '~/search/sidebar/components/notes_filters.vue';
import CommitsFilters from '~/search/sidebar/components/commits_filters.vue';
+import MilestonesFilters from '~/search/sidebar/components/milestones_filters.vue';
import ScopeLegacyNavigation from '~/search/sidebar/components/scope_legacy_navigation.vue';
import SmallScreenDrawerNavigation from '~/search/sidebar/components/small_screen_drawer_navigation.vue';
import ScopeSidebarNavigation from '~/search/sidebar/components/scope_sidebar_navigation.vue';
@@ -47,6 +48,7 @@ describe('GlobalSearchSidebar', () => {
glFeatures: {
searchNotesHideArchivedProjects: true,
searchCommitsHideArchivedProjects: true,
+ searchMilestonesHideArchivedProjects: true,
},
},
});
@@ -59,6 +61,7 @@ describe('GlobalSearchSidebar', () => {
const findProjectsFilters = () => wrapper.findComponent(ProjectsFilters);
const findNotesFilters = () => wrapper.findComponent(NotesFilters);
const findCommitsFilters = () => wrapper.findComponent(CommitsFilters);
+ const findMilestonesFilters = () => wrapper.findComponent(MilestonesFilters);
const findScopeLegacyNavigation = () => wrapper.findComponent(ScopeLegacyNavigation);
const findSmallScreenDrawerNavigation = () => wrapper.findComponent(SmallScreenDrawerNavigation);
const findScopeSidebarNavigation = () => wrapper.findComponent(ScopeSidebarNavigation);
@@ -83,10 +86,12 @@ describe('GlobalSearchSidebar', () => {
${'blobs'} | ${findBlobsFilters} | ${SEARCH_TYPE_BASIC} | ${false}
${'blobs'} | ${findBlobsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
${'blobs'} | ${findBlobsFilters} | ${SEARCH_TYPE_ZOEKT} | ${false}
- ${'notes'} | ${findNotesFilters} | ${SEARCH_TYPE_BASIC} | ${false}
+ ${'notes'} | ${findNotesFilters} | ${SEARCH_TYPE_BASIC} | ${true}
${'notes'} | ${findNotesFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
- ${'commits'} | ${findCommitsFilters} | ${SEARCH_TYPE_BASIC} | ${false}
+ ${'commits'} | ${findCommitsFilters} | ${SEARCH_TYPE_BASIC} | ${true}
${'commits'} | ${findCommitsFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
+ ${'milestones'} | ${findMilestonesFilters} | ${SEARCH_TYPE_BASIC} | ${true}
+ ${'milestones'} | ${findMilestonesFilters} | ${SEARCH_TYPE_ADVANCED} | ${true}
`('with sidebar $scope scope:', ({ scope, filter, searchType, isShown }) => {
beforeEach(() => {
getterSpies.currentScope = jest.fn(() => scope);
diff --git a/spec/frontend/search/sidebar/components/archived_filter_spec.js b/spec/frontend/search/sidebar/components/archived_filter_spec.js
index 69bf2ebd72e..9ed677ca297 100644
--- a/spec/frontend/search/sidebar/components/archived_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/archived_filter_spec.js
@@ -1,8 +1,9 @@
-import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
// eslint-disable-next-line no-restricted-imports
import Vuex from 'vuex';
import { GlFormCheckboxGroup } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import ArchivedFilter from '~/search/sidebar/components/archived_filter/index.vue';
import { archivedFilterData } from '~/search/sidebar/components/archived_filter/data';
@@ -12,17 +13,26 @@ Vue.use(Vuex);
describe('ArchivedFilter', () => {
let wrapper;
+ const defaultActions = {
+ setQuery: jest.fn(),
+ };
+
const createComponent = (state) => {
const store = new Vuex.Store({
state,
+ actions: defaultActions,
});
- wrapper = shallowMount(ArchivedFilter, {
+ wrapper = shallowMountExtended(ArchivedFilter, {
store,
+ directives: {
+ GlTooltip: createMockDirective('gl-tooltip'),
+ },
});
};
const findCheckboxFilter = () => wrapper.findComponent(GlFormCheckboxGroup);
+ const findCheckboxFilterLabel = () => wrapper.findByTestId('label');
const findH5 = () => wrapper.findComponent('h5');
describe('old sidebar', () => {
@@ -38,6 +48,12 @@ describe('ArchivedFilter', () => {
expect(findH5().exists()).toBe(true);
expect(findH5().text()).toBe(archivedFilterData.headerLabel);
});
+
+ it('wraps the label element with a tooltip', () => {
+ const tooltip = getBinding(findCheckboxFilterLabel().element, 'gl-tooltip');
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe('Include search results from archived projects');
+ });
});
describe('new sidebar', () => {
@@ -53,6 +69,12 @@ describe('ArchivedFilter', () => {
expect(findH5().exists()).toBe(true);
expect(findH5().text()).toBe(archivedFilterData.headerLabel);
});
+
+ it('wraps the label element with a tooltip', () => {
+ const tooltip = getBinding(findCheckboxFilterLabel().element, 'gl-tooltip');
+ expect(tooltip).toBeDefined();
+ expect(tooltip.value).toBe('Include search results from archived projects');
+ });
});
describe.each`
@@ -70,4 +92,20 @@ describe('ArchivedFilter', () => {
expect(findCheckboxFilter().attributes('checked')).toBe(checkboxState);
});
});
+
+ describe('selectedFilter logic', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('correctly executes setQuery without mutating the input', () => {
+ const selectedFilter = [false];
+ findCheckboxFilter().vm.$emit('input', selectedFilter);
+ expect(defaultActions.setQuery).toHaveBeenCalledWith(expect.any(Object), {
+ key: 'include_archived',
+ value: 'false',
+ });
+ expect(selectedFilter).toEqual([false]);
+ });
+ });
});
diff --git a/spec/frontend/search/sidebar/components/issues_filters_spec.js b/spec/frontend/search/sidebar/components/issues_filters_spec.js
index 39d10cbb8b4..c3b3a93e362 100644
--- a/spec/frontend/search/sidebar/components/issues_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/issues_filters_spec.js
@@ -111,11 +111,11 @@ describe('GlobalSearch IssuesFilters', () => {
});
it("doesn't render ArchivedFilter", () => {
- expect(findArchivedFilter().exists()).toBe(false);
+ expect(findArchivedFilter().exists()).toBe(true);
});
it('renders 1 divider', () => {
- expect(findDividers()).toHaveLength(1);
+ expect(findDividers()).toHaveLength(2);
});
});
diff --git a/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js b/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js
index b50f348be69..278249c2660 100644
--- a/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js
+++ b/spec/frontend/search/sidebar/components/merge_requests_filters_spec.js
@@ -79,12 +79,12 @@ describe('GlobalSearch MergeRequestsFilters', () => {
expect(findStatusFilter().exists()).toBe(true);
});
- it("doesn't render ArchivedFilter", () => {
- expect(findArchivedFilter().exists()).toBe(false);
+ it('renders render ArchivedFilter', () => {
+ expect(findArchivedFilter().exists()).toBe(true);
});
it('renders 1 divider', () => {
- expect(findDividers()).toHaveLength(0);
+ expect(findDividers()).toHaveLength(1);
});
});
diff --git a/spec/frontend/search/sidebar/components/milestones_filters_spec.js b/spec/frontend/search/sidebar/components/milestones_filters_spec.js
new file mode 100644
index 00000000000..e7fcfb030f4
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/milestones_filters_spec.js
@@ -0,0 +1,28 @@
+import { shallowMount } from '@vue/test-utils';
+import MilestonesFilters from '~/search/sidebar/components/milestones_filters.vue';
+import ArchivedFilter from '~/search/sidebar/components/archived_filter/index.vue';
+import FiltersTemplate from '~/search/sidebar/components/filters_template.vue';
+
+describe('GlobalSearch MilestonesFilters', () => {
+ let wrapper;
+
+ const findArchivedFilter = () => wrapper.findComponent(ArchivedFilter);
+ const findFiltersTemplate = () => wrapper.findComponent(FiltersTemplate);
+
+ const createComponent = () => {
+ wrapper = shallowMount(MilestonesFilters);
+ };
+
+ describe('Renders correctly', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+ it('renders ArchivedFilter', () => {
+ expect(findArchivedFilter().exists()).toBe(true);
+ });
+
+ it('renders FiltersTemplate', () => {
+ expect(findFiltersTemplate().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/search/topbar/components/app_spec.js b/spec/frontend/search/topbar/components/app_spec.js
index 62d0e377d74..9704277c86b 100644
--- a/spec/frontend/search/topbar/components/app_spec.js
+++ b/spec/frontend/search/topbar/components/app_spec.js
@@ -9,7 +9,10 @@ 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';
import MarkdownDrawer from '~/vue_shared/components/markdown_drawer/markdown_drawer.vue';
-import { SYNTAX_OPTIONS_DOCUMENT } from '~/search/topbar/constants';
+import {
+ SYNTAX_OPTIONS_ADVANCED_DOCUMENT,
+ SYNTAX_OPTIONS_ZOEKT_DOCUMENT,
+} from '~/search/topbar/constants';
Vue.use(Vuex);
@@ -22,7 +25,7 @@ describe('GlobalSearchTopbar', () => {
preloadStoredFrequentItems: jest.fn(),
};
- const createComponent = (initialState, props, stubs) => {
+ const createComponent = (initialState = {}, defaultBranchName = '', stubs = {}) => {
const store = new Vuex.Store({
state: {
query: MOCK_QUERY,
@@ -33,7 +36,7 @@ describe('GlobalSearchTopbar', () => {
wrapper = shallowMount(GlobalSearchTopbar, {
store,
- propsData: props,
+ propsData: { defaultBranchName },
stubs,
});
};
@@ -76,80 +79,82 @@ describe('GlobalSearchTopbar', () => {
});
});
- describe('syntax option feature', () => {
- describe('template', () => {
- beforeEach(() => {
- createComponent(
- { query: { repository_ref: '' } },
- { elasticsearchEnabled: true, defaultBranchName: '' },
- );
- });
+ describe.each`
+ searchType | showSyntaxOptions
+ ${'basic'} | ${false}
+ ${'advanced'} | ${true}
+ ${'zoekt'} | ${true}
+ `('syntax options drawer with searchType: $searchType', ({ searchType, showSyntaxOptions }) => {
+ beforeEach(() => {
+ createComponent({ query: { repository_ref: '' }, searchType });
+ });
- it('renders button correctly', () => {
- expect(findSyntaxOptionButton().exists()).toBe(true);
- });
+ it('renders button correctly', () => {
+ expect(findSyntaxOptionButton().exists()).toBe(showSyntaxOptions);
+ });
- it('renders drawer correctly', () => {
- expect(findSyntaxOptionDrawer().exists()).toBe(true);
- expect(findSyntaxOptionDrawer().attributes('documentpath')).toBe(SYNTAX_OPTIONS_DOCUMENT);
- });
+ it('renders drawer correctly', () => {
+ expect(findSyntaxOptionDrawer().exists()).toBe(showSyntaxOptions);
+ });
+ });
+
+ describe.each`
+ searchType | documentPath
+ ${'advanced'} | ${SYNTAX_OPTIONS_ADVANCED_DOCUMENT}
+ ${'zoekt'} | ${SYNTAX_OPTIONS_ZOEKT_DOCUMENT}
+ `('syntax options drawer with searchType: $searchType', ({ searchType, documentPath }) => {
+ beforeEach(() => {
+ createComponent({ query: { repository_ref: '' }, searchType });
+ });
- it('dispatched correct click action', () => {
- const drawerToggleSpy = jest.fn();
-
- createComponent(
- { query: { repository_ref: '' } },
- { elasticsearchEnabled: true, defaultBranchName: '' },
- {
- MarkdownDrawer: stubComponent(MarkdownDrawer, {
- methods: { toggleDrawer: drawerToggleSpy },
- }),
- },
- );
-
- findSyntaxOptionButton().vm.$emit('click');
- expect(drawerToggleSpy).toHaveBeenCalled();
+ it('renders drawer with correct document', () => {
+ expect(findSyntaxOptionDrawer()?.attributes('documentpath')).toBe(documentPath);
+ });
+ });
+
+ describe('actions', () => {
+ it('dispatched correct click action', () => {
+ const drawerToggleSpy = jest.fn();
+
+ createComponent({ query: { repository_ref: '' }, searchType: 'advanced' }, '', {
+ MarkdownDrawer: stubComponent(MarkdownDrawer, {
+ methods: { toggleDrawer: drawerToggleSpy },
+ }),
});
+
+ findSyntaxOptionButton().vm.$emit('click');
+ expect(drawerToggleSpy).toHaveBeenCalled();
});
+ });
- describe.each`
- query | propsData | hasSyntaxOptions
- ${null} | ${{ elasticsearchEnabled: false, defaultBranchName: '' }} | ${false}
- ${{ query: { repository_ref: '' } }} | ${{ elasticsearchEnabled: false, defaultBranchName: '' }} | ${false}
- ${{ query: { repository_ref: 'master' } }} | ${{ elasticsearchEnabled: false, defaultBranchName: 'master' }} | ${false}
- ${{ query: { repository_ref: 'master' } }} | ${{ elasticsearchEnabled: true, defaultBranchName: '' }} | ${false}
- ${{ query: { repository_ref: '' } }} | ${{ elasticsearchEnabled: true, defaultBranchName: 'master' }} | ${true}
- ${{ query: { repository_ref: '' } }} | ${{ elasticsearchEnabled: true, defaultBranchName: '' }} | ${true}
- ${{ query: { repository_ref: 'master' } }} | ${{ elasticsearchEnabled: true, defaultBranchName: 'master' }} | ${true}
- `(
- 'renders the syntax option based on component state',
- ({ query, propsData, hasSyntaxOptions }) => {
- beforeEach(() => {
- createComponent(query, { ...propsData });
- });
+ describe.each`
+ state | defaultBranchName | hasSyntaxOptions
+ ${{ query: { repository_ref: '' }, searchType: 'basic' }} | ${'master'} | ${false}
+ ${{ query: { repository_ref: 'v0.1' }, searchType: 'basic' }} | ${''} | ${false}
+ ${{ query: { repository_ref: 'master' }, searchType: 'basic' }} | ${'master'} | ${false}
+ ${{ query: { repository_ref: 'master' }, searchType: 'advanced' }} | ${''} | ${false}
+ ${{ query: { repository_ref: '' }, searchType: 'advanced' }} | ${'master'} | ${true}
+ ${{ query: { repository_ref: 'v0.1' }, searchType: 'advanced' }} | ${''} | ${false}
+ ${{ query: { repository_ref: 'master' }, searchType: 'advanced' }} | ${'master'} | ${true}
+ ${{ query: { repository_ref: 'master' }, searchType: 'zoekt' }} | ${'master'} | ${true}
+ `(
+ `the syntax option based on component state`,
+ ({ state, defaultBranchName, hasSyntaxOptions }) => {
+ beforeEach(() => {
+ createComponent({ ...state }, defaultBranchName);
+ });
- it(`does${
- hasSyntaxOptions ? '' : ' not'
- } have syntax option button when repository_ref: '${
- query?.query?.repository_ref
- }', elasticsearchEnabled: ${propsData.elasticsearchEnabled}, defaultBranchName: '${
- propsData.defaultBranchName
- }'`, () => {
+ describe(`repository: ${state.query.repository_ref}, searchType: ${state.searchType}`, () => {
+ it(`renders correctly button`, () => {
expect(findSyntaxOptionButton().exists()).toBe(hasSyntaxOptions);
});
- it(`does${
- hasSyntaxOptions ? '' : ' not'
- } have syntax option drawer when repository_ref: '${
- query?.query?.repository_ref
- }', elasticsearchEnabled: ${propsData.elasticsearchEnabled}, defaultBranchName: '${
- propsData.defaultBranchName
- }'`, () => {
+ it(`renders correctly drawer when branch name is ${state.query.repository_ref}`, () => {
expect(findSyntaxOptionDrawer().exists()).toBe(hasSyntaxOptions);
});
- },
- );
- });
+ });
+ },
+ );
});
describe('actions', () => {
diff --git a/spec/frontend/sentry/init_sentry_spec.js b/spec/frontend/sentry/init_sentry_spec.js
index e31068b935b..fb0dba35759 100644
--- a/spec/frontend/sentry/init_sentry_spec.js
+++ b/spec/frontend/sentry/init_sentry_spec.js
@@ -3,11 +3,10 @@ import {
defaultStackParser,
makeFetchTransport,
defaultIntegrations,
+ BrowserTracing,
// exports
captureException,
- captureMessage,
- withScope,
SDK_VERSION,
} from 'sentrybrowser';
import * as Sentry from 'sentrybrowser';
@@ -96,11 +95,17 @@ describe('SentryConfig', () => {
transport: makeFetchTransport,
stackParser: defaultStackParser,
- integrations: defaultIntegrations,
+ integrations: [...defaultIntegrations, expect.any(BrowserTracing)],
}),
);
});
+ it('Uses data-page to set BrowserTracing transaction name', () => {
+ const context = BrowserTracing.mock.calls[0][0].beforeNavigate();
+
+ expect(context).toMatchObject({ name: mockPage });
+ });
+
it('binds the BrowserClient to the hub', () => {
expect(mockBindClient).toHaveBeenCalledTimes(1);
expect(mockBindClient).toHaveBeenCalledWith(expect.any(BrowserClient));
@@ -126,8 +131,6 @@ describe('SentryConfig', () => {
// eslint-disable-next-line no-underscore-dangle
expect(window._Sentry).toEqual({
captureException,
- captureMessage,
- withScope,
SDK_VERSION,
});
});
@@ -173,5 +176,27 @@ describe('SentryConfig', () => {
expect(window._Sentry).toBe(undefined);
});
});
+
+ describe('when data-page is not defined in the body', () => {
+ beforeEach(() => {
+ delete document.body.dataset.page;
+ initSentry();
+ });
+
+ it('calls Sentry.setTags with gon values', () => {
+ expect(mockSetTags).toHaveBeenCalledTimes(1);
+ expect(mockSetTags).toHaveBeenCalledWith(
+ expect.objectContaining({
+ page: undefined,
+ }),
+ );
+ });
+
+ it('Uses location.path to set BrowserTracing transaction name', () => {
+ const context = BrowserTracing.mock.calls[0][0].beforeNavigate({ op: 'pageload' });
+
+ expect(context).toEqual({ op: 'pageload', name: window.location.pathname });
+ });
+ });
});
});
diff --git a/spec/frontend/sentry/sentry_browser_wrapper_spec.js b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
index 55354eceb8d..d98286e1371 100644
--- a/spec/frontend/sentry/sentry_browser_wrapper_spec.js
+++ b/spec/frontend/sentry/sentry_browser_wrapper_spec.js
@@ -1,8 +1,6 @@
import * as Sentry from '~/sentry/sentry_browser_wrapper';
const mockError = new Error('error!');
-const mockMsg = 'msg!';
-const mockFn = () => {};
describe('SentryBrowserWrapper', () => {
afterEach(() => {
@@ -14,27 +12,19 @@ describe('SentryBrowserWrapper', () => {
it('methods fail silently', () => {
expect(() => {
Sentry.captureException(mockError);
- Sentry.captureMessage(mockMsg);
- Sentry.withScope(mockFn);
}).not.toThrow();
});
});
describe('when _Sentry is defined', () => {
let mockCaptureException;
- let mockCaptureMessage;
- let mockWithScope;
beforeEach(() => {
mockCaptureException = jest.fn();
- mockCaptureMessage = jest.fn();
- mockWithScope = jest.fn();
// eslint-disable-next-line no-underscore-dangle
window._Sentry = {
captureException: mockCaptureException,
- captureMessage: mockCaptureMessage,
- withScope: mockWithScope,
};
});
@@ -43,17 +33,5 @@ describe('SentryBrowserWrapper', () => {
expect(mockCaptureException).toHaveBeenCalledWith(mockError);
});
-
- it('captureMessage is called', () => {
- Sentry.captureMessage(mockMsg);
-
- expect(mockCaptureMessage).toHaveBeenCalledWith(mockMsg);
- });
-
- it('withScope is called', () => {
- Sentry.withScope(mockFn);
-
- expect(mockWithScope).toHaveBeenCalledWith(mockFn);
- });
});
});
diff --git a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
index 39b480b295c..b2477e9b41c 100644
--- a/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
+++ b/spec/frontend/sidebar/components/todo_toggle/sidebar_todo_widget_spec.js
@@ -22,6 +22,7 @@ describe('Sidebar Todo Widget', () => {
const createComponent = ({
todosQueryHandler = jest.fn().mockResolvedValue(noTodosResponse),
+ provide = {},
} = {}) => {
fakeApollo = createMockApollo([[epicTodoQuery, todosQueryHandler]]);
@@ -30,6 +31,7 @@ describe('Sidebar Todo Widget', () => {
provide: {
canUpdate: true,
isClassicSidebar: true,
+ ...provide,
},
propsData: {
fullPath: 'group',
@@ -122,4 +124,23 @@ describe('Sidebar Todo Widget', () => {
expect(wrapper.emitted('todoUpdated')).toEqual([[false]]);
});
});
+
+ describe('when the query is pending', () => {
+ it('is in the loading state', () => {
+ createComponent();
+
+ expect(findTodoButton().attributes('loading')).toBe('true');
+ });
+
+ it('is not in the loading state if notificationsTodosButtons and movedMrSidebar feature flags are enabled', () => {
+ createComponent({
+ provide: {
+ glFeatures: { notificationsTodosButtons: true, movedMrSidebar: true },
+ },
+ });
+
+ expect(findTodoButton().attributes('loading')).toBeUndefined();
+ expect(findTodoButton().attributes('disabled')).toBe('true');
+ });
+ });
});
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
index 1c60c3af310..6414ab6dfba 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_blob_edit_spec.js.snap
@@ -3,11 +3,11 @@
exports[`Snippet Blob Edit component with loaded blob matches snapshot 1`] = `
<div
class="file-holder snippet"
- data-qa-selector="file_holder_container"
+ data-testid="file-holder-container"
>
<blob-header-edit-stub
candelete="true"
- data-qa-selector="file_name_field"
+ data-testid="file-name-field"
id="reference-0"
showdelete="true"
value="foo/bar/test.md"
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
index 5ed3b520b70..92511acc4f8 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_edit_spec.js.snap
@@ -17,7 +17,7 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
>
<gl-form-input-stub
class="form-control"
- data-qa-selector="description_placeholder"
+ data-testid="description-placeholder"
placeholder="Describe what your snippet does or how to use it…"
/>
</div>
@@ -46,8 +46,8 @@ exports[`Snippet Description Edit component rendering matches the snapshot 1`] =
<textarea
aria-label="Description"
class="js-autosize js-gfm-input js-gfm-input-initialized markdown-area note-textarea"
- data-qa-selector="snippet_description_field"
data-supports-quick-actions="false"
+ data-testid="snippet-description-field"
dir="auto"
id="reference-0"
placeholder="Write a comment or drag your files here…"
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap
index 2b2335036f6..7c5fbf4cfb7 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_description_view_spec.js.snap
@@ -3,7 +3,7 @@
exports[`Snippet Description component matches the snapshot 1`] = `
<markdown-field-view-stub
class="snippet-description"
- data-qa-selector="snippet_description_content"
+ data-testid="snippet-description-content"
>
<div
class="js-snippet-description md"
diff --git a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
index 3274f41e4af..ab96d1a3653 100644
--- a/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
+++ b/spec/frontend/snippets/components/__snapshots__/snippet_visibility_edit_spec.js.snap
@@ -44,8 +44,8 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
/>
<span
class="font-weight-bold js-visibility-option ml-1"
- data-qa-selector="visibility_content"
data-qa-visibility="Private"
+ data-testid="visibility-content"
>
Private
</span>
@@ -64,8 +64,8 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
/>
<span
class="font-weight-bold js-visibility-option ml-1"
- data-qa-selector="visibility_content"
data-qa-visibility="Internal"
+ data-testid="visibility-content"
>
Internal
</span>
@@ -84,8 +84,8 @@ exports[`Snippet Visibility Edit component rendering matches the snapshot 1`] =
/>
<span
class="font-weight-bold js-visibility-option ml-1"
- data-qa-selector="visibility_content"
data-qa-visibility="Public"
+ data-testid="visibility-content"
>
Public
</span>
diff --git a/spec/frontend/snippets/components/edit_spec.js b/spec/frontend/snippets/components/edit_spec.js
index 17862953920..5fbc16ff430 100644
--- a/spec/frontend/snippets/components/edit_spec.js
+++ b/spec/frontend/snippets/components/edit_spec.js
@@ -117,7 +117,8 @@ describe('Snippet Edit app', () => {
.map((path) => `<input name="files[]" value="${path}">`)
.join('');
};
- const setTitle = (val) => wrapper.findByTestId('snippet-title-input').vm.$emit('input', val);
+ const setTitle = (val) =>
+ wrapper.findByTestId('snippet-title-input-field').vm.$emit('input', val);
const setDescription = (val) =>
wrapper.findComponent(SnippetDescriptionEdit).vm.$emit('input', val);
diff --git a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
index cb11e98cd35..fab65434c3a 100644
--- a/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_actions_edit_spec.js
@@ -40,7 +40,7 @@ describe('snippets/components/snippet_blob_actions_edit', () => {
classes: x.classes(),
}));
const findFirstBlobEdit = () => findBlobEdits().at(0);
- const findAddButton = () => wrapper.find('[data-testid="add_button"]');
+ const findAddButton = () => wrapper.find('[data-testid="add-button"]');
const findLimitationsText = () => wrapper.find('[data-testid="limitations_text"]');
const getLastActions = () => {
const events = wrapper.emitted().actions;
diff --git a/spec/frontend/snippets/components/snippet_header_spec.js b/spec/frontend/snippets/components/snippet_header_spec.js
index 4bf64bfd3cd..3932675aa52 100644
--- a/spec/frontend/snippets/components/snippet_header_spec.js
+++ b/spec/frontend/snippets/components/snippet_header_spec.js
@@ -331,7 +331,7 @@ describe('Snippet header component', () => {
expect(findDeleteModal().props().visible).toBe(true);
// Click delete button in delete modal
- document.querySelector('[data-testid="delete-snippet"').click();
+ document.querySelector('[data-testid="delete-snippet-button"').click();
await waitForPromises();
};
diff --git a/spec/frontend/super_sidebar/components/create_menu_spec.js b/spec/frontend/super_sidebar/components/create_menu_spec.js
index b967fb18a39..ffbc789d220 100644
--- a/spec/frontend/super_sidebar/components/create_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/create_menu_spec.js
@@ -47,7 +47,7 @@ describe('CreateMenu component', () => {
createWrapper();
expect(findGlDisclosureDropdown().props('dropdownOffset')).toEqual({
- crossAxis: -179,
+ crossAxis: -177,
mainAxis: 4,
});
});
@@ -98,7 +98,7 @@ describe('CreateMenu component', () => {
createWrapper({ provide: { isImpersonating: true } });
expect(findGlDisclosureDropdown().props('dropdownOffset')).toEqual({
- crossAxis: -147,
+ crossAxis: -143,
mainAxis: 4,
});
});
diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js
index c92f8a68678..39537b65fa5 100644
--- a/spec/frontend/super_sidebar/components/help_center_spec.js
+++ b/spec/frontend/super_sidebar/components/help_center_spec.js
@@ -104,7 +104,7 @@ describe('HelpCenter component', () => {
createWrapper({ ...sidebarData, show_tanuki_bot: true });
});
- it('shows Ask GitLab Duo with the help items', () => {
+ it('shows GitLab Duo Chat with the help items', () => {
expect(findDropdownGroup(0).props('group').items).toEqual([
expect.objectContaining({
icon: 'tanuki-ai',
@@ -115,9 +115,9 @@ describe('HelpCenter component', () => {
]);
});
- describe('when Ask GitLab Duo button is clicked', () => {
+ describe('when GitLab Duo Chat button is clicked', () => {
beforeEach(() => {
- findButton('Ask GitLab Duo').click();
+ findButton('GitLab Duo Chat').click();
});
it('sets helpCenterState.showTanukiBotChatDrawer to true', () => {
diff --git a/spec/frontend/super_sidebar/components/nav_item_spec.js b/spec/frontend/super_sidebar/components/nav_item_spec.js
index 89d774c4b43..e6de9b1de22 100644
--- a/spec/frontend/super_sidebar/components/nav_item_spec.js
+++ b/spec/frontend/super_sidebar/components/nav_item_spec.js
@@ -10,6 +10,7 @@ import {
TRACKING_UNKNOWN_ID,
TRACKING_UNKNOWN_PANEL,
} from '~/super_sidebar/constants';
+import eventHub from '~/super_sidebar/event_hub';
describe('NavItem component', () => {
let wrapper;
@@ -49,7 +50,7 @@ describe('NavItem component', () => {
it.each([0, 5, 3.4, 'foo', '10%'])('item with pill_data `%p` renders a pill', (pillCount) => {
createWrapper({ item: { title: 'Foo', pill_count: pillCount } });
- expect(findPill().text()).toEqual(pillCount.toString());
+ expect(findPill().text()).toBe(pillCount.toString());
});
it.each([null, undefined, false, true, '', NaN, Number.POSITIVE_INFINITY])(
@@ -57,9 +58,49 @@ describe('NavItem component', () => {
(pillCount) => {
createWrapper({ item: { title: 'Foo', pill_count: pillCount } });
- expect(findPill().exists()).toEqual(false);
+ expect(findPill().exists()).toBe(false);
},
);
+
+ describe('updating pill value', () => {
+ const initialPillValue = '20%';
+ const updatedPillValue = '50%';
+ const itemIdForUpdate = '_some_item_id_';
+ const triggerPillValueUpdate = async ({
+ value = updatedPillValue,
+ itemId = itemIdForUpdate,
+ } = {}) => {
+ eventHub.$emit('updatePillValue', { value, itemId });
+ await nextTick();
+ };
+
+ it('updates the pill count', async () => {
+ createWrapper({ item: { id: itemIdForUpdate, pill_count: initialPillValue } });
+
+ await triggerPillValueUpdate();
+
+ expect(findPill().text()).toBe(updatedPillValue);
+ });
+
+ it('does not update the pill count for non matching item id', async () => {
+ createWrapper({ item: { id: '_non_matching_id_', pill_count: initialPillValue } });
+
+ await triggerPillValueUpdate();
+
+ expect(findPill().text()).toBe(initialPillValue);
+ });
+ });
+ });
+
+ describe('destroyed', () => {
+ it('should unbind event listeners on eventHub', async () => {
+ jest.spyOn(eventHub, '$off');
+
+ createWrapper({ item: {} });
+ await wrapper.destroy();
+
+ expect(eventHub.$off).toHaveBeenCalledWith('updatePillValue', expect.any(Function));
+ });
});
describe('pins', () => {
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index 1371f8f00a7..92736b99e14 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -45,6 +45,7 @@ const peekHintClass = 'super-sidebar-peek-hint';
describe('SuperSidebar component', () => {
let wrapper;
+ const findSkipToLink = () => wrapper.findByTestId('super-sidebar-skip-to');
const findSidebar = () => wrapper.findByTestId('super-sidebar');
const findUserBar = () => wrapper.findComponent(UserBar);
const findNavContainer = () => wrapper.findByTestId('nav-container');
@@ -89,6 +90,24 @@ describe('SuperSidebar component', () => {
});
describe('default', () => {
+ it('renders skip to main content link when logged in', () => {
+ createWrapper();
+ expect(findSkipToLink().attributes('href')).toBe('#content-body');
+ });
+
+ it('does not render skip to main content link when logged out', () => {
+ createWrapper({ sidebarData: { is_logged_in: false } });
+ expect(findSkipToLink().exists()).toBe(false);
+ });
+
+ it('has accessible role and name', () => {
+ createWrapper();
+ const nav = wrapper.findByRole('navigation');
+ const heading = wrapper.findByText('Primary navigation');
+ expect(nav.attributes('aria-labelledby')).toBe('super-sidebar-heading');
+ expect(heading.attributes('id')).toBe('super-sidebar-heading');
+ });
+
it('adds inert attribute when collapsed', () => {
createWrapper({ sidebarState: { isCollapsed: true } });
expect(findSidebar().attributes('inert')).toBe('inert');
@@ -295,11 +314,4 @@ describe('SuperSidebar component', () => {
expect(findTrialStatusPopover().exists()).toBe(true);
});
});
-
- describe('ARIA attributes', () => {
- it('adds aria-label attribute to nav element', () => {
- createWrapper();
- expect(wrapper.find('nav').attributes('aria-label')).toBe('Primary');
- });
- });
});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
index 1f2e5602d10..974eb529113 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_toggle_spec.js
@@ -18,13 +18,8 @@ describe('SuperSidebarToggle component', () => {
const findButton = () => wrapper.findComponent(GlButton);
const getTooltip = () => getBinding(wrapper.element, 'gl-tooltip').value;
- const createWrapper = ({ props = {}, sidebarState = {} } = {}) => {
+ const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(SuperSidebarToggle, {
- data() {
- return {
- ...sidebarState,
- };
- },
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
@@ -40,18 +35,15 @@ describe('SuperSidebarToggle component', () => {
expect(findButton().attributes('aria-controls')).toBe('super-sidebar');
});
- it('has aria-expanded as true when expanded', () => {
- createWrapper();
+ it('has aria-expanded as true when type is collapse', () => {
+ createWrapper({ type: 'collapse' });
expect(findButton().attributes('aria-expanded')).toBe('true');
});
- it.each(['isCollapsed', 'isPeek', 'isHoverPeek'])(
- 'has aria-expanded as false when %s is `true`',
- (stateProp) => {
- createWrapper({ sidebarState: { [stateProp]: true } });
- expect(findButton().attributes('aria-expanded')).toBe('false');
- },
- );
+ it('has aria-expanded as false when type is expand', () => {
+ createWrapper();
+ expect(findButton().attributes('aria-expanded')).toBe('false');
+ });
it('has aria-label attribute', () => {
createWrapper();
@@ -60,13 +52,13 @@ describe('SuperSidebarToggle component', () => {
});
describe('tooltip', () => {
- it('displays collapse when expanded', () => {
- createWrapper();
+ it('displays "Hide sidebar" when type is collapse', () => {
+ createWrapper({ type: 'collapse' });
expect(getTooltip().title).toBe('Hide sidebar');
});
- it('displays expand when collapsed', () => {
- createWrapper({ sidebarState: { isCollapsed: true } });
+ it('displays "Keep sidebar visible" when type is expand', () => {
+ createWrapper();
expect(getTooltip().title).toBe('Keep sidebar visible');
});
});
@@ -88,13 +80,11 @@ describe('SuperSidebarToggle component', () => {
});
it('collapses the sidebar and focuses the other toggle', async () => {
- createWrapper();
+ createWrapper({ type: 'collapse' });
findButton().vm.$emit('click');
await nextTick();
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(true, true);
- expect(document.activeElement).toEqual(
- document.querySelector(`.${JS_TOGGLE_COLLAPSE_CLASS}`),
- );
+ expect(document.activeElement).toEqual(document.querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`));
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_hide', {
label: 'nav_toggle',
property: 'nav_sidebar',
@@ -102,11 +92,13 @@ describe('SuperSidebarToggle component', () => {
});
it('expands the sidebar and focuses the other toggle', async () => {
- createWrapper({ sidebarState: { isCollapsed: true } });
+ createWrapper();
findButton().vm.$emit('click');
await nextTick();
expect(toggleSuperSidebarCollapsed).toHaveBeenCalledWith(false, true);
- expect(document.activeElement).toEqual(document.querySelector(`.${JS_TOGGLE_EXPAND_CLASS}`));
+ expect(document.activeElement).toEqual(
+ document.querySelector(`.${JS_TOGGLE_COLLAPSE_CLASS}`),
+ );
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'nav_show', {
label: 'nav_toggle',
property: 'nav_sidebar',
diff --git a/spec/frontend/super_sidebar/components/user_name_group_spec.js b/spec/frontend/super_sidebar/components/user_menu_profile_item_spec.js
index a31ad93d143..9cf55154a59 100644
--- a/spec/frontend/super_sidebar/components/user_name_group_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_profile_item_spec.js
@@ -1,12 +1,11 @@
-import { GlDisclosureDropdownGroup, GlDisclosureDropdownItem, GlTooltip } from '@gitlab/ui';
+import { GlDisclosureDropdownItem, GlTooltip } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import UserNameGroup from '~/super_sidebar/components/user_name_group.vue';
+import UserMenuProfileItem from '~/super_sidebar/components/user_menu_profile_item.vue';
import { userMenuMockData, userMenuMockStatus } from '../mock_data';
-describe('UserNameGroup component', () => {
+describe('UserMenuProfileItem component', () => {
let wrapper;
- const findGlDisclosureDropdownGroup = () => wrapper.findComponent(GlDisclosureDropdownGroup);
const findGlDisclosureDropdownItem = () => wrapper.findComponent(GlDisclosureDropdownItem);
const findGlTooltip = () => wrapper.findComponent(GlTooltip);
const findUserStatus = () => wrapper.findByTestId('user-menu-status');
@@ -14,7 +13,7 @@ describe('UserNameGroup component', () => {
const GlEmoji = { template: '<img/>' };
const createWrapper = (userDataChanges = {}) => {
- wrapper = shallowMountExtended(UserNameGroup, {
+ wrapper = shallowMountExtended(UserMenuProfileItem, {
propsData: {
user: {
...userMenuMockData,
@@ -32,10 +31,6 @@ describe('UserNameGroup component', () => {
createWrapper();
});
- it('renders the menu item in a separate group', () => {
- expect(findGlDisclosureDropdownGroup().exists()).toBe(true);
- });
-
it('renders menu item', () => {
expect(findGlDisclosureDropdownItem().exists()).toBe(true);
});
diff --git a/spec/frontend/super_sidebar/components/user_menu_spec.js b/spec/frontend/super_sidebar/components/user_menu_spec.js
index bcc3383bcd4..79a31492f3f 100644
--- a/spec/frontend/super_sidebar/components/user_menu_spec.js
+++ b/spec/frontend/super_sidebar/components/user_menu_spec.js
@@ -2,7 +2,7 @@ import { GlAvatar, GlDisclosureDropdown } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { stubComponent } from 'helpers/stub_component';
import UserMenu from '~/super_sidebar/components/user_menu.vue';
-import UserNameGroup from '~/super_sidebar/components/user_name_group.vue';
+import UserMenuProfileItem from '~/super_sidebar/components/user_menu_profile_item.vue';
import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import invalidUrl from '~/lib/utils/invalid_url';
import { mockTracking } from 'helpers/tracking_helper';
@@ -56,7 +56,7 @@ describe('UserMenu component', () => {
createWrapper(null, null, { isImpersonating: true });
expect(findDropdown().props('dropdownOffset')).toEqual({
- crossAxis: -179,
+ crossAxis: -177,
mainAxis: 4,
});
});
@@ -86,9 +86,9 @@ describe('UserMenu component', () => {
describe('User Menu Group', () => {
it('renders and passes data to it', () => {
createWrapper();
- const userNameGroup = wrapper.findComponent(UserNameGroup);
- expect(userNameGroup.exists()).toBe(true);
- expect(userNameGroup.props('user')).toEqual(userMenuMockData);
+ const userMenuProfileItem = wrapper.findComponent(UserMenuProfileItem);
+ expect(userMenuProfileItem.exists()).toBe(true);
+ expect(userMenuProfileItem.props('user')).toEqual(userMenuMockData);
});
});
diff --git a/spec/frontend/super_sidebar/utils_spec.js b/spec/frontend/super_sidebar/utils_spec.js
index 85f45de06ba..85c13a4c892 100644
--- a/spec/frontend/super_sidebar/utils_spec.js
+++ b/spec/frontend/super_sidebar/utils_spec.js
@@ -11,7 +11,7 @@ import axios from '~/lib/utils/axios_utils';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import AccessorUtilities from '~/lib/utils/accessor';
import { FREQUENT_ITEMS, FIFTEEN_MINUTES_IN_MS } from '~/frequent_items/constants';
-import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import { unsortedFrequentItems, sortedFrequentItems } from '../frequent_items/mock_data';
import { cachedFrequentProjects } from './mock_data';
@@ -58,7 +58,6 @@ describe('Super sidebar utils spec', () => {
const storageKey = `${username}/frequent-${context.namespace}`;
beforeEach(() => {
- gon.features = { serverSideFrecentNamespaces: true };
axiosMock = new MockAdapter(axios);
axiosMock.onPost(trackVisitsPath).reply(HTTP_STATUS_OK);
});
@@ -99,12 +98,12 @@ describe('Super sidebar utils spec', () => {
expect(axiosMock.history.post[0].url).toBe(trackVisitsPath);
});
- it('does not send a POST request when the serverSideFrecentNamespaces feature flag is disabled', async () => {
- gon.features = { serverSideFrecentNamespaces: false };
+ it('logs an error to Sentry when the request fails', async () => {
+ axiosMock.onPost(trackVisitsPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
trackContextAccess(username, context, trackVisitsPath);
await waitForPromises();
- expect(axiosMock.history.post).toHaveLength(0);
+ expect(Sentry.captureException).toHaveBeenCalled();
});
it('updates existing item frequency/access time if it was persisted to the local storage over 15 minutes ago', () => {
diff --git a/spec/frontend/tags/components/sort_dropdown_spec.js b/spec/frontend/tags/components/sort_dropdown_spec.js
index ebf79c93f9b..a0ba263e832 100644
--- a/spec/frontend/tags/components/sort_dropdown_spec.js
+++ b/spec/frontend/tags/components/sort_dropdown_spec.js
@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import * as urlUtils from '~/lib/utils/url_utility';
import SortDropdown from '~/tags/components/sort_dropdown.vue';
+import setWindowLocation from 'helpers/set_window_location_helper';
describe('Tags sort dropdown', () => {
let wrapper;
@@ -45,20 +46,33 @@ describe('Tags sort dropdown', () => {
});
});
+ describe('when url contains a search param', () => {
+ const branchName = 'branch-1';
+
+ beforeEach(() => {
+ setWindowLocation(`/root/ci-cd-project-demo/-/branches?search=${branchName}`);
+ wrapper = createWrapper();
+ });
+
+ it('should set the default the input value to search param', () => {
+ expect(findSearchBox().props('value')).toBe(branchName);
+ });
+ });
+
describe('when submitting a search term', () => {
beforeEach(() => {
urlUtils.visitUrl = jest.fn();
-
wrapper = createWrapper();
});
it('should call visitUrl', () => {
+ const searchTerm = 'branch-1';
const searchBox = findSearchBox();
-
+ searchBox.vm.$emit('input', searchTerm);
searchBox.vm.$emit('submit');
expect(urlUtils.visitUrl).toHaveBeenCalledWith(
- '/root/ci-cd-project-demo/-/tags?sort=updated_desc',
+ '/root/ci-cd-project-demo/-/tags?search=branch-1&sort=updated_desc',
);
});
diff --git a/spec/frontend/tracking/internal_events_spec.js b/spec/frontend/tracking/internal_events_spec.js
index 6e773fde4db..44a048a4b5f 100644
--- a/spec/frontend/tracking/internal_events_spec.js
+++ b/spec/frontend/tracking/internal_events_spec.js
@@ -6,7 +6,6 @@ import {
GITLAB_INTERNAL_EVENT_CATEGORY,
SERVICE_PING_SCHEMA,
LOAD_INTERNAL_EVENTS_SELECTOR,
- USER_CONTEXT_SCHEMA,
} from '~/tracking/constants';
import * as utils from '~/tracking/utils';
import { Tracker } from '~/tracking/tracker';
@@ -26,18 +25,27 @@ Tracker.enabled = jest.fn();
const event = 'TestEvent';
describe('InternalEvents', () => {
- describe('track_event', () => {
- it('track_event calls API.trackInternalEvent with correct arguments', () => {
- InternalEvents.track_event(event);
+ describe('trackEvent', () => {
+ it('trackEvent calls API.trackInternalEvent with correct arguments', () => {
+ InternalEvents.trackEvent(event);
expect(API.trackInternalEvent).toHaveBeenCalledTimes(1);
expect(API.trackInternalEvent).toHaveBeenCalledWith(event);
});
- it('track_event calls tracking.event functions with correct arguments', () => {
+ it('trackEvent calls trackBrowserSDK with correct arguments', () => {
+ jest.spyOn(InternalEvents, 'trackBrowserSDK').mockImplementation(() => {});
+
+ InternalEvents.trackEvent(event);
+
+ expect(InternalEvents.trackBrowserSDK).toHaveBeenCalledTimes(1);
+ expect(InternalEvents.trackBrowserSDK).toHaveBeenCalledWith(event);
+ });
+
+ it('trackEvent calls tracking.event functions with correct arguments', () => {
const trackingSpy = mockTracking(GITLAB_INTERNAL_EVENT_CATEGORY, undefined, jest.spyOn);
- InternalEvents.track_event(event, { context: extraContext });
+ InternalEvents.trackEvent(event, { context: extraContext });
expect(trackingSpy).toHaveBeenCalledTimes(1);
expect(trackingSpy).toHaveBeenCalledWith(GITLAB_INTERNAL_EVENT_CATEGORY, event, {
@@ -66,10 +74,10 @@ describe('InternalEvents', () => {
`,
methods: {
handleButton1Click() {
- this.track_event(event);
+ this.trackEvent(event);
},
handleButton2Click() {
- this.track_event(event, extraContext);
+ this.trackEvent(event, extraContext);
},
},
mixins: [InternalEvents.mixin()],
@@ -79,8 +87,8 @@ describe('InternalEvents', () => {
wrapper = shallowMountExtended(Component);
});
- it('this.track_event function calls InternalEvent`s track function with an event', async () => {
- const trackEventSpy = jest.spyOn(InternalEvents, 'track_event');
+ it('this.trackEvent function calls InternalEvent`s track function with an event', async () => {
+ const trackEventSpy = jest.spyOn(InternalEvents, 'trackEvent');
await wrapper.findByTestId('button1').trigger('click');
@@ -88,9 +96,9 @@ describe('InternalEvents', () => {
expect(trackEventSpy).toHaveBeenCalledWith(event, {});
});
- it("this.track_event function calls InternalEvent's track function with an event and data", async () => {
+ it("this.trackEvent function calls InternalEvent's track function with an event and data", async () => {
const data = extraContext;
- const trackEventSpy = jest.spyOn(InternalEvents, 'track_event');
+ const trackEventSpy = jest.spyOn(InternalEvents, 'trackEvent');
await wrapper.findByTestId('button2').trigger('click');
@@ -147,7 +155,7 @@ describe('InternalEvents', () => {
describe('tracking', () => {
let trackEventSpy;
beforeEach(() => {
- trackEventSpy = jest.spyOn(InternalEvents, 'track_event');
+ trackEventSpy = jest.spyOn(InternalEvents, 'trackEvent');
});
it('should track event if action exists', () => {
@@ -181,16 +189,6 @@ describe('InternalEvents', () => {
environment: 'testing',
key: 'value',
};
- window.gl.snowplowStandardContext = {
- schema: 'iglu:com.gitlab/gitlab_standard',
- data: {
- environment: 'testing',
- key: 'value',
- google_analytics_id: '',
- source: 'gitlab-javascript',
- extra: {},
- },
- };
});
it('should not call setDocumentTitle or page methods when window.glClient is undefined', () => {
@@ -203,55 +201,48 @@ describe('InternalEvents', () => {
});
it('should call setDocumentTitle and page methods on window.glClient when it is defined', () => {
- const mockStandardContext = window.gl.snowplowStandardContext;
- const userContext = {
- schema: USER_CONTEXT_SCHEMA,
- data: mockStandardContext?.data,
- };
-
InternalEvents.initBrowserSDK();
expect(window.glClient.setDocumentTitle).toHaveBeenCalledWith('GitLab');
expect(window.glClient.page).toHaveBeenCalledWith({
title: 'GitLab',
- context: [userContext],
});
});
- it('should call page method with combined standard and experiment contexts', () => {
- const mockStandardContext = window.gl.snowplowStandardContext;
- const userContext = {
- schema: USER_CONTEXT_SCHEMA,
- data: mockStandardContext?.data,
- };
+ it('should call setDocumentTitle and page methods with default data when window.gl is undefined', () => {
+ window.gl = undefined;
InternalEvents.initBrowserSDK();
+ expect(window.glClient.setDocumentTitle).toHaveBeenCalledWith('GitLab');
expect(window.glClient.page).toHaveBeenCalledWith({
title: 'GitLab',
- context: [userContext],
});
});
+ });
- it('should call setDocumentTitle and page methods with default data when window.gl is undefined', () => {
- window.gl = undefined;
+ describe('trackBrowserSDK', () => {
+ beforeEach(() => {
+ window.glClient = {
+ track: jest.fn(),
+ };
+ });
- InternalEvents.initBrowserSDK();
+ it('should not call glClient.track if Tracker is not enabled', () => {
+ Tracker.enabled.mockReturnValue(false);
- expect(window.glClient.setDocumentTitle).toHaveBeenCalledWith('GitLab');
- expect(window.glClient.page).toHaveBeenCalledWith({
- title: 'GitLab',
- context: [
- {
- schema: USER_CONTEXT_SCHEMA,
- data: {
- google_analytics_id: '',
- source: 'gitlab-javascript',
- extra: {},
- },
- },
- ],
- });
+ InternalEvents.trackBrowserSDK(event);
+
+ expect(window.glClient.track).not.toHaveBeenCalled();
+ });
+
+ it('should call glClient.track with correct arguments if Tracker is enabled', () => {
+ Tracker.enabled.mockReturnValue(true);
+
+ InternalEvents.trackBrowserSDK(event);
+
+ expect(window.glClient.track).toHaveBeenCalledTimes(1);
+ expect(window.glClient.track).toHaveBeenCalledWith(event);
});
});
});
diff --git a/spec/frontend/users_select/test_helper.js b/spec/frontend/users_select/test_helper.js
index 5aae922fec2..0d8e3275aa5 100644
--- a/spec/frontend/users_select/test_helper.js
+++ b/spec/frontend/users_select/test_helper.js
@@ -147,6 +147,7 @@ export const createInputsModelExpectation = (users) =>
name: user.name,
show_status: user.show_status.toString(),
state: user.state,
+ locked: user.locked.toString(),
username: user.username,
web_url: user.web_url,
},
diff --git a/spec/frontend/vue_alerts_spec.js b/spec/frontend/vue_alerts_spec.js
index de2faa09438..be4a45639cf 100644
--- a/spec/frontend/vue_alerts_spec.js
+++ b/spec/frontend/vue_alerts_spec.js
@@ -1,4 +1,5 @@
import { nextTick } from 'vue';
+import { alertVariantOptions } from '@gitlab/ui/dist/utils/constants';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import initVueAlerts from '~/vue_alerts';
@@ -55,7 +56,11 @@ describe('VueAlerts', () => {
primaryButtonText: alert.querySelector('.gl-alert-action').textContent.trim(),
primaryButtonLink: alert.querySelector('.gl-alert-action').href,
variant: [...alert.classList]
- .find((x) => x.match(/gl-alert-(?!not-dismissible)/))
+ .find((cssClass) => {
+ return Object.values(alertVariantOptions).some(
+ (variant) => cssClass === `gl-alert-${variant}`,
+ );
+ })
.replace('gl-alert-', ''),
});
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js
new file mode 100644
index 00000000000..57dcd2fd819
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/checks/conflicts_spec.js
@@ -0,0 +1,90 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import ConflictsComponent from '~/vue_merge_request_widget/components/checks/conflicts.vue';
+import conflictsStateQuery from '~/vue_merge_request_widget/queries/states/conflicts.query.graphql';
+
+Vue.use(VueApollo);
+
+let wrapper;
+let apolloProvider;
+
+function factory({
+ result = 'passed',
+ canMerge = true,
+ pushToSourceBranch = true,
+ shouldBeRebased = false,
+ sourceBranchProtected = false,
+ mr = {},
+} = {}) {
+ apolloProvider = createMockApollo([
+ [
+ conflictsStateQuery,
+ jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: 1,
+ mergeRequest: {
+ id: 1,
+ shouldBeRebased,
+ sourceBranchProtected,
+ userPermissions: { canMerge, pushToSourceBranch },
+ },
+ },
+ },
+ }),
+ ],
+ ]);
+
+ wrapper = mountExtended(ConflictsComponent, {
+ apolloProvider,
+ propsData: {
+ mr,
+ check: { result, failureReason: 'Conflicts message' },
+ },
+ });
+}
+
+describe('Merge request merge checks conflicts component', () => {
+ afterEach(() => {
+ apolloProvider = null;
+ });
+
+ it('renders failure reason text', () => {
+ factory();
+
+ expect(wrapper.text()).toEqual('Conflicts message');
+ });
+
+ it.each`
+ conflictResolutionPath | pushToSourceBranch | sourceBranchProtected | rendersConflictButton | rendersConflictButtonText
+ ${'https://gitlab.com'} | ${true} | ${false} | ${true} | ${'renders'}
+ ${undefined} | ${true} | ${false} | ${false} | ${'does not render'}
+ ${'https://gitlab.com'} | ${false} | ${false} | ${false} | ${'does not render'}
+ ${'https://gitlab.com'} | ${true} | ${true} | ${false} | ${'does not render'}
+ ${'https://gitlab.com'} | ${false} | ${false} | ${false} | ${'does not render'}
+ ${undefined} | ${false} | ${false} | ${false} | ${'does not render'}
+ `(
+ '$rendersConflictButtonText the conflict button for $conflictResolutionPath $pushToSourceBranch $sourceBranchProtected $rendersConflictButton',
+ async ({
+ conflictResolutionPath,
+ pushToSourceBranch,
+ sourceBranchProtected,
+ rendersConflictButton,
+ }) => {
+ factory({ mr: { conflictResolutionPath }, pushToSourceBranch, sourceBranchProtected });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllByTestId('extension-actions-button').length).toBe(
+ rendersConflictButton ? 2 : 1,
+ );
+
+ expect(wrapper.findAllByTestId('extension-actions-button').at(-1).text()).toBe(
+ rendersConflictButton ? 'Resolve conflicts' : 'Resolve locally',
+ );
+ },
+ );
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/checks/message_spec.js b/spec/frontend/vue_merge_request_widget/components/checks/message_spec.js
new file mode 100644
index 00000000000..4446eb7324b
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/checks/message_spec.js
@@ -0,0 +1,30 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import MessageComponent from '~/vue_merge_request_widget/components/checks/message.vue';
+import StatusIcon from '~/vue_merge_request_widget/components/widget/status_icon.vue';
+
+let wrapper;
+
+function factory(propsData = {}) {
+ wrapper = mountExtended(MessageComponent, {
+ propsData,
+ });
+}
+
+describe('Merge request merge checks message component', () => {
+ it('renders failure reason text', () => {
+ factory({ check: { result: 'passed', failureReason: 'Failed message' } });
+
+ expect(wrapper.text()).toEqual('Failed message');
+ });
+
+ it.each`
+ result | icon
+ ${'passed'} | ${'success'}
+ ${'failed'} | ${'failed'}
+ ${'allowed_to_fail'} | ${'neutral'}
+ `('renders $icon icon for $result result', ({ result, icon }) => {
+ factory({ check: { result, failureReason: 'Failed message' } });
+
+ expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe(icon);
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
new file mode 100644
index 00000000000..c86fe6d0a10
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/merge_checks_spec.js
@@ -0,0 +1,92 @@
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import MergeChecksComponent from '~/vue_merge_request_widget/components/merge_checks.vue';
+import mergeChecksQuery from '~/vue_merge_request_widget/queries/merge_checks.query.graphql';
+import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
+
+Vue.use(VueApollo);
+
+let wrapper;
+let apolloProvider;
+
+function factory({ canMerge = true, mergeChecks = [] } = {}) {
+ apolloProvider = createMockApollo([
+ [
+ mergeChecksQuery,
+ jest.fn().mockResolvedValue({
+ data: {
+ project: {
+ id: 1,
+ mergeRequest: { id: 1, userPermissions: { canMerge }, mergeChecks },
+ },
+ },
+ }),
+ ],
+ ]);
+
+ wrapper = mountExtended(MergeChecksComponent, {
+ apolloProvider,
+ propsData: {
+ mr: {},
+ },
+ });
+}
+
+describe('Merge request merge checks component', () => {
+ afterEach(() => {
+ apolloProvider = null;
+ });
+
+ it('renders ready to merge text if user can merge', async () => {
+ factory({ canMerge: true });
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toBe('Ready to merge!');
+ });
+
+ it('renders ready to merge by members text if user can not merge', async () => {
+ factory({ canMerge: false });
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toBe('Ready to merge by members who can write to the target branch.');
+ });
+
+ it.each`
+ mergeChecks | text
+ ${[{ identifier: 'discussions', result: 'failed' }]} | ${'Merge blocked: 1 check failed'}
+ ${[{ identifier: 'discussions', result: 'failed' }, { identifier: 'rebase', result: 'failed' }]} | ${'Merge blocked: 2 checks failed'}
+ `('renders $text for $mergeChecks', async ({ mergeChecks, text }) => {
+ factory({ mergeChecks });
+
+ await waitForPromises();
+
+ expect(wrapper.text()).toBe(text);
+ });
+
+ it.each`
+ result | statusIcon
+ ${'failed'} | ${'failed'}
+ ${'passed'} | ${'success'}
+ `('renders $statusIcon for $result result', async ({ result, statusIcon }) => {
+ factory({ mergeChecks: [{ result, identifier: 'discussions' }] });
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent(StatusIcon).props('iconName')).toBe(statusIcon);
+ });
+
+ it('expands collapsed area', async () => {
+ factory();
+
+ await waitForPromises();
+
+ await wrapper.findByTestId('widget-toggle').trigger('click');
+
+ expect(wrapper.findByTestId('merge-checks-full').exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js
index adefce9060c..86e3922ec8b 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/action_buttons_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlDropdownItem } from '@gitlab/ui';
+import { GlButton, GlDisclosureDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Actions from '~/vue_merge_request_widget/components/widget/action_buttons.vue';
@@ -37,7 +37,7 @@ describe('~/vue_merge_request_widget/components/widget/action_buttons.vue', () =
tertiaryButtons: [{ text: 'hello world', href: 'https://gitlab.com', target: '_blank' }],
});
- expect(wrapper.findAllComponents(GlDropdownItem)).toHaveLength(1);
+ expect(wrapper.findAllComponents(GlDisclosureDropdown)).toHaveLength(1);
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index 09f58f17fd9..eb3d624dc04 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -21,7 +21,7 @@ import {
registerExtension,
registeredExtensions,
} from '~/vue_merge_request_widget/components/extensions';
-import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
+import { STATUS_CLOSED, STATUS_OPEN, STATUS_MERGED } from '~/issues/constants';
import { STATE_QUERY_POLLING_INTERVAL_BACKOFF } from '~/vue_merge_request_widget/constants';
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
import eventHub from '~/vue_merge_request_widget/event_hub';
@@ -30,6 +30,7 @@ import Approvals from '~/vue_merge_request_widget/components/approvals/approvals
import ConflictsState from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
import Preparing from '~/vue_merge_request_widget/components/states/mr_widget_preparing.vue';
import ShaMismatch from '~/vue_merge_request_widget/components/states/sha_mismatch.vue';
+import MergedState from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue';
import WidgetContainer from '~/vue_merge_request_widget/components/widget/app.vue';
import WidgetSuggestPipeline from '~/vue_merge_request_widget/components/mr_widget_suggest_pipeline.vue';
import MrWidgetAlertMessage from '~/vue_merge_request_widget/components/mr_widget_alert_message.vue';
@@ -78,23 +79,13 @@ describe('MrWidgetOptions', () => {
const COLLABORATION_MESSAGE = 'Members who can merge are allowed to add commits';
- const setInitialData = (data) => {
- gl.mrWidgetData = { ...mockData, ...data };
- mock
- .onGet(mockData.merge_request_widget_path)
- .reply(() => [HTTP_STATUS_OK, { ...mockData, ...data }]);
- mock
- .onGet(mockData.merge_request_cached_widget_path)
- .reply(() => [HTTP_STATUS_OK, { ...mockData, ...data }]);
- };
-
const createComponent = ({
updatedMrData = {},
options = {},
data = {},
mountFn = shallowMountExtended,
} = {}) => {
- setInitialData(updatedMrData);
+ gl.mrWidgetData = { ...mockData, ...updatedMrData };
const mrData = { ...mockData, ...updatedMrData };
const mockedApprovalsSubscription = createMockApolloSubscription();
queryResponse = {
@@ -172,8 +163,10 @@ describe('MrWidgetOptions', () => {
const findWidgetContainer = () => wrapper.findComponent(WidgetContainer);
beforeEach(() => {
- gon.features = { asyncMrWidget: true };
+ gon.features = {};
mock = new MockAdapter(axios);
+ mock.onGet(mockData.merge_request_widget_path).reply(HTTP_STATUS_OK, {});
+ mock.onGet(mockData.merge_request_cached_widget_path).reply(HTTP_STATUS_OK, {});
});
afterEach(() => {
@@ -186,25 +179,13 @@ describe('MrWidgetOptions', () => {
describe('default', () => {
describe('computed', () => {
describe('componentName', () => {
- beforeEach(async () => {
- await createComponent();
- });
-
- // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/409365
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip.each`
- ${'merged'} | ${'mr-widget-merged'}
- `('should translate $state into $componentName', ({ state, componentName }) => {
- wrapper.vm.mr.state = state;
-
- expect(wrapper.vm.componentName).toEqual(componentName);
- });
-
it.each`
state | componentName | component
+ ${STATUS_MERGED} | ${'MergedState'} | ${MergedState}
${'conflicts'} | ${'ConflictsState'} | ${ConflictsState}
${'shaMismatch'} | ${'ShaMismatch'} | ${ShaMismatch}
`('should translate $state into $componentName component', async ({ state, component }) => {
+ await createComponent();
Vue.set(wrapper.vm.mr, 'state', state);
await nextTick();
expect(wrapper.findComponent(component).exists()).toBe(true);
@@ -336,13 +317,23 @@ describe('MrWidgetOptions', () => {
describe('methods', () => {
describe('checkStatus', () => {
+ const updatedMrData = { foo: 1 };
+ beforeEach(() => {
+ mock
+ .onGet(mockData.merge_request_widget_path)
+ .reply(HTTP_STATUS_OK, { ...mockData, ...updatedMrData });
+ mock
+ .onGet(mockData.merge_request_cached_widget_path)
+ .reply(HTTP_STATUS_OK, { ...mockData, ...updatedMrData });
+ });
+
it('checks the status of the pipelines', async () => {
const callback = jest.fn();
- await createComponent({ updatedMrData: { foo: 1 } });
+ await createComponent({ updatedMrData });
await waitForPromises();
eventHub.$emit('MRWidgetUpdateRequested', callback);
await waitForPromises();
- expect(callback).toHaveBeenCalledWith(expect.objectContaining({ foo: 1 }));
+ expect(callback).toHaveBeenCalledWith(expect.objectContaining(updatedMrData));
});
it('notifies the user of the pipeline status', async () => {
@@ -515,29 +506,42 @@ describe('MrWidgetOptions', () => {
});
describe('handleNotification', () => {
+ const updatedMrData = { gitlabLogo: 'logo.png' };
beforeEach(() => {
jest.spyOn(notify, 'notifyMe').mockImplementation(() => {});
});
- it('should call notifyMe', async () => {
- const logoFilename = 'logo.png';
- await createComponent({ updatedMrData: { gitlabLogo: logoFilename } });
- expect(notify.notifyMe).toHaveBeenCalledWith(
- `Pipeline passed`,
- `Pipeline passed for "${mockData.title}"`,
- logoFilename,
- );
- });
+ describe('when pipeline has passed', () => {
+ beforeEach(() => {
+ mock
+ .onGet(mockData.merge_request_widget_path)
+ .reply(HTTP_STATUS_OK, { ...mockData, ...updatedMrData });
+ mock
+ .onGet(mockData.merge_request_cached_widget_path)
+ .reply(HTTP_STATUS_OK, { ...mockData, ...updatedMrData });
+ });
- it('should not call notifyMe if the status has not changed', async () => {
- await createComponent({ updatedMrData: { ci_status: undefined } });
- await eventHub.$emit('MRWidgetUpdateRequested');
- expect(notify.notifyMe).not.toHaveBeenCalled();
+ it('should call notifyMe', async () => {
+ await createComponent({ updatedMrData });
+ expect(notify.notifyMe).toHaveBeenCalledWith(
+ `Pipeline passed`,
+ `Pipeline passed for "${mockData.title}"`,
+ updatedMrData.gitlabLogo,
+ );
+ });
});
- it('should not notify if no pipeline provided', async () => {
- await createComponent({ updatedMrData: { pipeline: undefined } });
- expect(notify.notifyMe).not.toHaveBeenCalled();
+ describe('when pipeline has not passed', () => {
+ it('should not call notifyMe if the status has not changed', async () => {
+ await createComponent({ updatedMrData: { ci_status: undefined } });
+ await eventHub.$emit('MRWidgetUpdateRequested');
+ expect(notify.notifyMe).not.toHaveBeenCalled();
+ });
+
+ it('should not notify if no pipeline provided', async () => {
+ await createComponent({ updatedMrData: { pipeline: undefined } });
+ expect(notify.notifyMe).not.toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/vue_shared/alert_details/alert_status_spec.js b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
index 90d29f0bfd4..478df81a966 100644
--- a/spec/frontend/vue_shared/alert_details/alert_status_spec.js
+++ b/spec/frontend/vue_shared/alert_details/alert_status_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import updateAlertStatusMutation from '~/graphql_shared//mutations/alert_status_update.mutation.graphql';
@@ -34,13 +34,13 @@ describe('AlertManagementStatus', () => {
},
});
- const findStatusDropdown = () => wrapper.findComponent(GlDropdown);
- const findFirstStatusOption = () => findStatusDropdown().findComponent(GlDropdownItem);
- const findAllStatusOptions = () => findStatusDropdown().findAllComponents(GlDropdownItem);
- const findStatusDropdownHeader = () => wrapper.findByTestId('dropdown-header');
+ const findStatusDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findFirstStatusOption = () => findStatusDropdown().findComponent(GlListboxItem);
+ const findAllStatusOptions = () => findStatusDropdown().findAllComponents(GlListboxItem);
+ const findStatusDropdownHeader = () => wrapper.findByTestId('listbox-header-text');
const selectFirstStatusOption = () => {
- findFirstStatusOption().vm.$emit('click');
+ findFirstStatusOption().vm.$emit('select', new Event('click'));
return waitForPromises();
};
@@ -57,7 +57,7 @@ describe('AlertManagementStatus', () => {
provide = {},
handler = mockUpdatedMutationResult(),
} = {}) {
- wrapper = shallowMountExtended(AlertManagementStatus, {
+ wrapper = mountExtended(AlertManagementStatus, {
apolloProvider: createMockApolloProvider(handler),
propsData: {
alert: { ...mockAlert },
@@ -82,7 +82,7 @@ describe('AlertManagementStatus', () => {
it('shows the dropdown', () => {
mountComponent({ props: { isSidebar: true, isDropdownShowing: true } });
- expect(wrapper.classes()).toContain('show');
+ expect(wrapper.classes()).not.toContain('gl-display-none');
});
});
@@ -92,8 +92,7 @@ describe('AlertManagementStatus', () => {
});
it('calls `$apollo.mutate` with `updateAlertStatus` mutation and variables containing `iid`, `status`, & `projectPath`', async () => {
- findFirstStatusOption().vm.$emit('click');
- await waitForPromises();
+ await selectFirstStatusOption();
expect(requestHandler).toHaveBeenCalledWith({
iid,
@@ -194,9 +193,7 @@ describe('AlertManagementStatus', () => {
handler: mockUpdatedMutationResult({ nodes: mockAlerts }),
});
Tracking.event.mockClear();
- findFirstStatusOption().vm.$emit('click');
-
- await waitForPromises();
+ await selectFirstStatusOption();
const status = findFirstStatusOption().text();
const { category, action, label } = trackAlertStatusUpdateOptions;
diff --git a/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap b/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap
index 359aaacde0b..499a971d791 100644
--- a/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap
+++ b/spec/frontend/vue_shared/components/badges/__snapshots__/beta_badge_spec.js.snap
@@ -2,22 +2,15 @@
exports[`Beta badge component renders the badge 1`] = `
<div>
- <gl-badge-stub
- class="gl-cursor-pointer"
+ <a
+ class="badge badge-neutral badge-pill gl-badge gl-cursor-pointer md"
href="#"
- iconsize="md"
- size="md"
- variant="neutral"
+ target="_self"
>
Beta
- </gl-badge-stub>
- <gl-popover-stub
- cssclasses=""
- data-testid="beta-badge"
- showclosebutton="true"
- target="[Function]"
- title="What's Beta?"
- triggers="hover focus click"
+ </a>
+ <div
+ class="gl-popover"
>
<p>
A Beta feature is not production-ready, but is unlikely to change drastically before it's released. We encourage users to try Beta features and provide feedback.
@@ -43,6 +36,6 @@ exports[`Beta badge component renders the badge 1`] = `
Is complete or near completion.
</li>
</ul>
- </gl-popover-stub>
+ </div>
</div>
`;
diff --git a/spec/frontend/vue_shared/components/badges/__snapshots__/experiment_badge_spec.js.snap b/spec/frontend/vue_shared/components/badges/__snapshots__/experiment_badge_spec.js.snap
new file mode 100644
index 00000000000..4ad70338f3c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/badges/__snapshots__/experiment_badge_spec.js.snap
@@ -0,0 +1,41 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Experiment badge component renders the badge 1`] = `
+<div>
+ <a
+ class="badge badge-neutral badge-pill gl-badge gl-cursor-pointer md"
+ href="#"
+ target="_self"
+ >
+ Experiment
+ </a>
+ <div
+ class="gl-popover"
+ >
+ <p>
+ An Experiment is a feature that's in the process of being developed. It's not production-ready. We encourage users to try Experimental features and provide feedback.
+ </p>
+ <p
+ class="gl-mb-0"
+ >
+ An Experiment:
+ </p>
+ <ul
+ class="gl-pl-4"
+ >
+ <li>
+ May be unstable.
+ </li>
+ <li>
+ Can cause data loss.
+ </li>
+ <li>
+ Has no support and might not be documented.
+ </li>
+ <li>
+ Can be removed at any time.
+ </li>
+ </ul>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/vue_shared/components/badges/beta_badge_spec.js b/spec/frontend/vue_shared/components/badges/beta_badge_spec.js
index c930c6d5708..d826ca5c7c0 100644
--- a/spec/frontend/vue_shared/components/badges/beta_badge_spec.js
+++ b/spec/frontend/vue_shared/components/badges/beta_badge_spec.js
@@ -1,4 +1,4 @@
-import { shallowMount } from '@vue/test-utils';
+import { mount } from '@vue/test-utils';
import { GlBadge } from '@gitlab/ui';
import BetaBadge from '~/vue_shared/components/badges/beta_badge.vue';
@@ -7,7 +7,7 @@ describe('Beta badge component', () => {
const findBadge = () => wrapper.findComponent(GlBadge);
const createWrapper = (props = {}) => {
- wrapper = shallowMount(BetaBadge, {
+ wrapper = mount(BetaBadge, {
propsData: { ...props },
});
};
diff --git a/spec/frontend/vue_shared/components/badges/experiment_badge_spec.js b/spec/frontend/vue_shared/components/badges/experiment_badge_spec.js
new file mode 100644
index 00000000000..3239578a173
--- /dev/null
+++ b/spec/frontend/vue_shared/components/badges/experiment_badge_spec.js
@@ -0,0 +1,32 @@
+import { mount } from '@vue/test-utils';
+import { GlBadge } from '@gitlab/ui';
+import ExperimentBadge from '~/vue_shared/components/badges/experiment_badge.vue';
+
+describe('Experiment badge component', () => {
+ let wrapper;
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const createWrapper = (props = {}) => {
+ wrapper = mount(ExperimentBadge, {
+ propsData: { ...props },
+ });
+ };
+
+ it('renders the badge', () => {
+ createWrapper();
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
+ it('passes default size to badge', () => {
+ createWrapper();
+
+ expect(findBadge().props('size')).toBe('md');
+ });
+
+ it('passes given size to badge', () => {
+ createWrapper({ size: 'sm' });
+
+ expect(findBadge().props('size')).toBe('sm');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/badges/hover_badge_spec.js b/spec/frontend/vue_shared/components/badges/hover_badge_spec.js
new file mode 100644
index 00000000000..68f368215c0
--- /dev/null
+++ b/spec/frontend/vue_shared/components/badges/hover_badge_spec.js
@@ -0,0 +1,50 @@
+import { mount } from '@vue/test-utils';
+import { GlBadge, GlPopover } from '@gitlab/ui';
+import HoverBadge from '~/vue_shared/components/badges/hover_badge.vue';
+
+describe('Hover badge component', () => {
+ let wrapper;
+
+ const findBadge = () => wrapper.findComponent(GlBadge);
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const createWrapper = ({ props = {}, slots } = {}) => {
+ wrapper = mount(HoverBadge, {
+ propsData: {
+ label: 'Label',
+ title: 'Title',
+ ...props,
+ },
+ slots,
+ });
+ };
+
+ it('passes label to popover', () => {
+ createWrapper();
+
+ expect(findBadge().text()).toBe('Label');
+ });
+
+ it('passes title to popover', () => {
+ createWrapper();
+
+ expect(findPopover().props('title')).toBe('Title');
+ });
+
+ it('renders the default slot', () => {
+ createWrapper({ slots: { default: '<p>This is an awesome content</p>' } });
+
+ expect(findPopover().text()).toContain('This is an awesome content');
+ });
+
+ it('passes default size to badge', () => {
+ createWrapper();
+
+ expect(findBadge().props('size')).toBe('md');
+ });
+
+ it('passes given size to badge', () => {
+ createWrapper({ props: { size: 'sm' } });
+
+ expect(findBadge().props('size')).toBe('sm');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
index eadcd452929..c1109f21b47 100644
--- a/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/blob_viewers/rich_viewer_spec.js
@@ -60,6 +60,7 @@ describe('Blob Rich Viewer component', () => {
expect(wrapper.text()).toContain('Line: 10');
expect(wrapper.text()).toContain('Line: 50');
expect(wrapper.emitted(CONTENT_LOADED_EVENT)).toHaveLength(1);
+ expect(handleLocationHash).toHaveBeenCalled();
expect(findMarkdownFieldView().props('isLoading')).toBe(false);
});
diff --git a/spec/frontend/vue_shared/components/ci_badge_link_spec.js b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
index c74964c13f5..e1660225a5c 100644
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -149,4 +149,10 @@ describe('CI Badge Link Component', () => {
expect(findBadge().props('size')).toBe('lg');
});
+
+ it('should have class `gl-px-2` when `showText` is false', () => {
+ createComponent({ status: statuses.success, size: 'md', showText: false });
+
+ expect(findBadge().classes()).toContain('gl-px-2');
+ });
});
diff --git a/spec/frontend/vue_shared/components/clone_dropdown/clone_dropdown_item_spec.js b/spec/frontend/vue_shared/components/clone_dropdown/clone_dropdown_item_spec.js
index e0dfa084f3e..341afa03f80 100644
--- a/spec/frontend/vue_shared/components/clone_dropdown/clone_dropdown_item_spec.js
+++ b/spec/frontend/vue_shared/components/clone_dropdown/clone_dropdown_item_spec.js
@@ -6,11 +6,11 @@ describe('Clone Dropdown Button', () => {
let wrapper;
const link = 'ssh://foo.bar';
const label = 'SSH';
- const qaSelector = 'some-selector';
+ const testId = 'some-selector';
const defaultPropsData = {
link,
label,
- qaSelector,
+ testId,
};
const findCopyButton = () => wrapper.findComponent(GlButton);
@@ -46,7 +46,7 @@ describe('Clone Dropdown Button', () => {
});
it('sets the qa selector', () => {
- expect(findCopyButton().attributes('data-qa-selector')).toBe(qaSelector);
+ expect(findCopyButton().attributes('data-testid')).toBe(testId);
});
});
});
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
index a22ad4c450e..7c9f3a3546a 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/mock_data.js
@@ -97,6 +97,19 @@ export const projectMilestonesResponse = {
},
};
+export const projectUsersResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/1',
+ attributes: {
+ nodes: mockUsers,
+ __typename: 'UserConnection',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
export const mockCrmContacts = [
{
__typename: 'CustomerRelationsContact',
@@ -247,8 +260,8 @@ export const mockAuthorToken = {
symbol: '@',
token: UserToken,
operators: OPERATORS_IS,
- fetchPath: 'gitlab-org/gitlab-test',
- fetchUsers: Api.projectUsers.bind(Api),
+ fullPath: 'gitlab-org/gitlab-test',
+ isProject: true,
};
export const mockLabelToken = {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
index 63eacaabd0c..72e3475df75 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/base_token_spec.js
@@ -420,6 +420,12 @@ describe('BaseToken', () => {
expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
});
+ it('renders `footer` slot when present', () => {
+ wrapper = createComponent({ slots: { footer: "<div class='custom-footer' />" } });
+
+ expect(wrapper.find('.custom-footer').exists()).toBe(true);
+ });
+
describe('events', () => {
describe('when activeToken has been selected', () => {
beforeEach(() => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
index e4ca7dcb19a..0229d00eb91 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/tokens/user_token_spec.js
@@ -6,16 +6,21 @@ import {
} from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
-import { nextTick } from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/alert';
import axios from '~/lib/utils/axios_utils';
+import usersAutocompleteQuery from '~/graphql_shared/queries/users_autocomplete.query.graphql';
import { OPTIONS_NONE_ANY } from '~/vue_shared/components/filtered_search_bar/constants';
import UserToken from '~/vue_shared/components/filtered_search_bar/tokens/user_token.vue';
import BaseToken from '~/vue_shared/components/filtered_search_bar/tokens/base_token.vue';
-import { mockAuthorToken, mockUsers } from '../mock_data';
+import { mockAuthorToken, mockUsers, projectUsersResponse } from '../mock_data';
+
+Vue.use(VueApollo);
jest.mock('~/alert');
const defaultStubs = {
@@ -37,6 +42,9 @@ const mockPreloadedUsers = [
},
];
+const usersQueryHandler = jest.fn().mockResolvedValue(projectUsersResponse);
+const mockApollo = createMockApollo([[usersAutocompleteQuery, usersQueryHandler]]);
+
function createComponent(options = {}) {
const {
config = mockAuthorToken,
@@ -47,6 +55,7 @@ function createComponent(options = {}) {
listeners = {},
} = options;
return mount(UserToken, {
+ apolloProvider: mockApollo,
propsData: {
config,
value,
@@ -145,6 +154,33 @@ describe('UserToken', () => {
expect(findBaseToken().props('suggestionsLoading')).toBe(false);
});
});
+
+ describe('default - when fetchMilestones function is not provided in config', () => {
+ beforeEach(() => {
+ wrapper = createComponent({});
+ return triggerFetchUsers();
+ });
+
+ it('calls searchMilestonesQuery to fetch milestones', () => {
+ expect(usersQueryHandler).toHaveBeenCalledWith({
+ fullPath: mockAuthorToken.fullPath,
+ isProject: mockAuthorToken.isProject,
+ search: null,
+ });
+ });
+
+ it('calls searchMilestonesQuery with search parameter when provided', async () => {
+ const searchTerm = 'foo';
+
+ await triggerFetchUsers(searchTerm);
+
+ expect(usersQueryHandler).toHaveBeenCalledWith({
+ fullPath: mockAuthorToken.fullPath,
+ isProject: mockAuthorToken.isProject,
+ search: searchTerm,
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
index eee85ce4fd3..72a0eb98a07 100644
--- a/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
+++ b/spec/frontend/vue_shared/components/form/input_copy_toggle_visibility_spec.js
@@ -363,13 +363,13 @@ describe('InputCopyToggleVisibility', () => {
it('passes no `size` prop', () => {
createComponent();
- expect(findFormInput().props('size')).toBe(null);
+ expect(findFormInput().props('width')).toBe(null);
});
it('passes `size` prop to the input', () => {
createComponent({ props: { size: 'md' } });
- expect(findFormInput().props('size')).toBe('md');
+ expect(findFormInput().props('width')).toBe('md');
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
index 712e78458c6..57f54f7e7d3 100644
--- a/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/editor_mode_switcher_spec.js
@@ -1,41 +1,22 @@
import { nextTick } from 'vue';
-import { GlButton, GlLink, GlPopover } from '@gitlab/ui';
+import { GlButton } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
-import { counter } from '~/vue_shared/components/markdown/utils';
-import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
-import { stubComponent } from 'helpers/stub_component';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
-jest.mock('~/vue_shared/components/markdown/utils', () => ({
- counter: jest.fn().mockReturnValue(0),
-}));
-
describe('vue_shared/component/markdown/editor_mode_switcher', () => {
let wrapper;
useLocalStorageSpy();
- const createComponent = ({
- value,
- userCalloutDismisserSlotProps = { dismiss: jest.fn() },
- } = {}) => {
+ const createComponent = ({ value } = {}) => {
wrapper = mount(EditorModeSwitcher, {
propsData: {
value,
},
- stubs: {
- UserCalloutDismisser: stubComponent(UserCalloutDismisser, {
- render() {
- return this.$scopedSlots.default(userCalloutDismisserSlotProps);
- },
- }),
- },
});
};
const findSwitcherButton = () => wrapper.findComponent(GlButton);
- const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser);
- const findCalloutPopover = () => wrapper.findComponent(GlPopover);
describe.each`
value | buttonText
@@ -54,62 +35,7 @@ describe('vue_shared/component/markdown/editor_mode_switcher', () => {
await nextTick();
findSwitcherButton().vm.$emit('click');
- expect(wrapper.emitted().switch).toEqual([[false]]);
- });
- });
-
- describe('rich text editor callout', () => {
- let dismiss;
-
- beforeEach(() => {
- dismiss = jest.fn();
- createComponent({ value: 'markdown', userCalloutDismisserSlotProps: { dismiss } });
- });
-
- it('does not skip the user_callout_dismisser query', () => {
- expect(findUserCalloutDismisser().props()).toMatchObject({
- skipQuery: false,
- featureName: 'rich_text_editor',
- });
- });
-
- it('mounts new rich text editor popover', () => {
- expect(findCalloutPopover().props()).toMatchObject({
- showCloseButton: '',
- triggers: 'manual',
- target: 'switch-to-rich-text-editor',
- });
- });
-
- it('dismisses the callout and emits "switch" event when popover close button is clicked', async () => {
- await findCalloutPopover().findComponent(GlLink).vm.$emit('click');
-
- expect(wrapper.emitted().switch).toEqual([[true]]);
- expect(dismiss).toHaveBeenCalled();
- });
-
- it('dismisses the callout when action button is clicked', () => {
- findSwitcherButton().vm.$emit('click');
-
- expect(dismiss).toHaveBeenCalled();
- });
-
- it('does not show the callout if rich text is already enabled', async () => {
- await wrapper.setProps({ value: 'richText' });
-
- expect(findCalloutPopover().props()).toMatchObject({
- show: false,
- });
- });
-
- it('does not show the callout if already displayed once on the page', () => {
- counter.mockReturnValue(1);
-
- createComponent({ value: 'markdown' });
-
- expect(findCalloutPopover().props()).toMatchObject({
- show: false,
- });
+ expect(wrapper.emitted().switch).toEqual([[]]);
});
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
index c69b18bca88..b4c90fe49d1 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -7,6 +7,8 @@ import {
EDITING_MODE_MARKDOWN_FIELD,
EDITING_MODE_CONTENT_EDITOR,
CLEAR_AUTOSAVE_ENTRY_EVENT,
+ CONTENT_EDITOR_READY_EVENT,
+ MARKDOWN_EDITOR_READY_EVENT,
} from '~/vue_shared/constants';
import markdownEditorEventHub from '~/vue_shared/components/markdown/eventhub';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
@@ -83,22 +85,23 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
const findContentEditor = () => {
const result = wrapper.findComponent(ContentEditor);
-
// In Vue.js 3 there are nuances stubbing component with custom stub on mount
// So we try to search for stub also
return result.exists() ? result : wrapper.findComponent(ContentEditorStub);
};
- const enableContentEditor = async () => {
- findMarkdownField().vm.$emit('enableContentEditor');
- await nextTick();
- await waitForPromises();
+ const enableContentEditor = () => {
+ return new Promise((resolve) => {
+ markdownEditorEventHub.$once(CONTENT_EDITOR_READY_EVENT, resolve);
+ findMarkdownField().vm.$emit('enableContentEditor');
+ });
};
- const enableMarkdownEditor = async () => {
- findContentEditor().vm.$emit('enableMarkdownEditor');
- await nextTick();
- await waitForPromises();
+ const enableMarkdownEditor = () => {
+ return new Promise((resolve) => {
+ markdownEditorEventHub.$once(MARKDOWN_EDITOR_READY_EVENT, resolve);
+ findContentEditor().vm.$emit('enableMarkdownEditor');
+ });
};
beforeEach(() => {
@@ -128,9 +131,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
});
- // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/412618
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip('passes render_quick_actions param to renderMarkdownPath if quick actions are enabled', async () => {
+ it('passes render_quick_actions param to renderMarkdownPath if quick actions are enabled', async () => {
buildWrapper({ propsData: { supportsQuickActions: true } });
await enableContentEditor();
@@ -139,9 +140,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(mock.history.post[0].url).toContain(`render_quick_actions=true`);
});
- // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/411565
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip('does not pass render_quick_actions param to renderMarkdownPath if quick actions are disabled', async () => {
+ it('does not pass render_quick_actions param to renderMarkdownPath if quick actions are disabled', async () => {
buildWrapper({ propsData: { supportsQuickActions: false } });
await enableContentEditor();
@@ -213,9 +212,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findMarkdownField().find('textarea').attributes('disabled')).toBe(undefined);
});
- // quarantine: https://gitlab.com/gitlab-org/gitlab/-/issues/404734
- // eslint-disable-next-line jest/no-disabled-tests
- it.skip('disables content editor when disabled prop is true', async () => {
+ it('disables content editor when disabled prop is true', async () => {
buildWrapper({ propsData: { disabled: true } });
await enableContentEditor();
@@ -358,9 +355,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
});
it(`emits ${EDITING_MODE_MARKDOWN_FIELD} event when enableMarkdownEditor emitted from content editor`, async () => {
- buildWrapper({
- stubs: { ContentEditor: ContentEditorStub },
- });
+ buildWrapper();
await enableContentEditor();
await enableMarkdownEditor();
@@ -494,12 +489,62 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findContentEditor().props().autofocus).toBe(false);
});
- it('bubbles up keydown event', () => {
- const event = new Event('keydown');
+ describe('when keydown event is fired', () => {
+ let event;
+ beforeEach(() => {
+ event = new Event('keydown');
+ window.getSelection = jest.fn(() => ({
+ toString: jest.fn(() => 'test'),
+ removeAllRanges: jest.fn(),
+ }));
+ Object.assign(event, { preventDefault: jest.fn() });
+ });
+ it('bubbles up keydown event', () => {
+ findContentEditor().vm.$emit('keydown', event);
+
+ expect(wrapper.emitted('keydown')).toEqual([[event]]);
+ });
+
+ it('bubbles up keydown event for meta key with default behaviour intact', () => {
+ event.metaKey = true;
+ findContentEditor().vm.$emit('keydown', event);
- findContentEditor().vm.$emit('keydown', event);
+ expect(wrapper.emitted('keydown')).toEqual([[event]]);
+ expect(event.preventDefault).toHaveBeenCalledTimes(0);
+ });
+
+ it('bubbles up keydown event for meta + k key on selected text with default behaviour prevented', () => {
+ event.metaKey = true;
+ event.key = 'k';
+ findContentEditor().vm.$emit('keydown', event);
+
+ expect(wrapper.emitted('keydown')).toEqual([[event]]);
+ expect(event.preventDefault).toHaveBeenCalledTimes(1);
+ });
+
+ it('bubbles up keydown event for meta + k key without text selection with default behaviour prevented', () => {
+ event.metaKey = true;
+ event.key = 'k';
+ window.getSelection = jest.fn(() => ({
+ toString: jest.fn(() => ''),
+ removeAllRanges: jest.fn(),
+ }));
+
+ findContentEditor().vm.$emit('keydown', event);
- expect(wrapper.emitted('keydown')).toEqual([[event]]);
+ expect(wrapper.emitted('keydown')).toEqual([[event]]);
+ expect(event.preventDefault).toHaveBeenCalledTimes(1);
+ });
+
+ it('bubbles up keydown event for meta + non-k key with default behaviour intact', () => {
+ event.metaKey = true;
+ event.key = 'l';
+
+ findContentEditor().vm.$emit('keydown', event);
+
+ expect(wrapper.emitted('keydown')).toEqual([[event]]);
+ expect(event.preventDefault).toHaveBeenCalledTimes(0);
+ });
});
describe(`when richText editor triggers enableMarkdownEditor event`, () => {
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
index 90d8ce3b500..59f01b7ff7f 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -3,7 +3,6 @@ import Toolbar from '~/vue_shared/components/markdown/toolbar.vue';
import EditorModeSwitcher from '~/vue_shared/components/markdown/editor_mode_switcher.vue';
import { updateText } from '~/lib/utils/text_markdown';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
jest.mock('~/lib/utils/text_markdown');
@@ -83,28 +82,5 @@ describe('toolbar', () => {
expect(wrapper.emitted('enableContentEditor')).toEqual([[]]);
expect(updateText).not.toHaveBeenCalled();
});
-
- it('does not insert a template text if textarea has some value', () => {
- wrapper.findComponent(EditorModeSwitcher).vm.$emit('switch', true);
-
- expect(updateText).not.toHaveBeenCalled();
- });
-
- it('inserts a "getting started with rich text" template when switched for the first time', () => {
- document.querySelector('textarea').value = '';
-
- wrapper.findComponent(EditorModeSwitcher).vm.$emit('switch', true);
-
- expect(updateText).toHaveBeenCalledWith(
- expect.objectContaining({
- tag: `### Rich text editor
-
-Try out **styling** _your_ content right here or read the [direction](${PROMO_URL}/direction/plan/knowledge/content_editor/).`,
- textArea: document.querySelector('textarea'),
- cursorOffset: 0,
- wrap: false,
- }),
- );
- });
});
});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js
index c6cd963fc33..67aa57a019b 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js
@@ -1,5 +1,5 @@
-import { GlAlert, GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlAlert, GlListboxItem, GlLoadingIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -32,7 +32,7 @@ describe('RunnerCliInstructions component', () => {
const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert);
- const findArchitectureDropdownItems = () => wrapper.findAllByTestId('architecture-dropdown-item');
+ const findArchitectureDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
const findBinaryDownloadButton = () => wrapper.findByTestId('binary-download-button');
const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions');
const findRegisterCommand = () => wrapper.findByTestId('register-command');
@@ -43,7 +43,7 @@ describe('RunnerCliInstructions component', () => {
fakeApollo = createMockApollo(requestHandlers);
wrapper = extendedWrapper(
- shallowMount(RunnerCliInstructions, {
+ mount(RunnerCliInstructions, {
propsData: {
platform: mockPlatform,
registrationToken: 'MY_TOKEN',
diff --git a/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
index c1feb64dacb..623a8739907 100644
--- a/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
+++ b/spec/frontend/vue_shared/components/segmented_control_button_group_spec.js
@@ -10,6 +10,7 @@ const DEFAULT_OPTIONS = [
];
describe('~/vue_shared/components/segmented_control_button_group.vue', () => {
+ let consoleSpy;
let wrapper;
const createComponent = (props = {}, scopedSlots = {}) => {
@@ -97,4 +98,34 @@ describe('~/vue_shared/components/segmented_control_button_group.vue', () => {
);
});
});
+
+ describe('options prop validation', () => {
+ beforeEach(() => {
+ consoleSpy = jest.spyOn(console, 'error').mockImplementation();
+ });
+
+ it.each([
+ [[{ disabled: true }]],
+ [[{ value: '1', disabled: 'false' }]],
+ [[{ value: null, disabled: 'true' }]],
+ [[[{ value: true }, null]]],
+ ])('with options=%j, fails validation', (options) => {
+ createComponent({ options });
+
+ expect(consoleSpy).toHaveBeenCalledTimes(1);
+ expect(consoleSpy).toHaveBeenCalledWith(
+ expect.stringContaining('Invalid prop: custom validator check failed for prop "options"'),
+ );
+ });
+
+ it.each([
+ [[{ value: '1' }]],
+ [[{ value: 1, disabled: true }]],
+ [[{ value: true, disabled: false }]],
+ ])('with options=%j, passes validation', (options) => {
+ createComponent({ options });
+
+ expect(consoleSpy).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/__snapshots__/utils_spec.js.snap b/spec/frontend/vue_shared/components/source_viewer/__snapshots__/utils_spec.js.snap
new file mode 100644
index 00000000000..e75b07dcf71
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/__snapshots__/utils_spec.js.snap
@@ -0,0 +1,88 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`SourceViewer utils toggleBlameClasses adds classes 1`] = `
+<div
+ class="content"
+>
+ <div
+ class="gl-border-gray-500 gl-border-t gl-pt-3!"
+ >
+ <div
+ id="reference-0"
+ >
+ 1
+ </div>
+ <div
+ id="reference-1"
+ >
+ 2
+ </div>
+ <div
+ id="reference-2"
+ >
+ 3
+ </div>
+ </div>
+ <div>
+ <div
+ class="gl-border-gray-500 gl-border-t gl-pt-3!"
+ id="reference-3"
+ >
+ Content 1
+ </div>
+ <div
+ class="gl-border-gray-500 gl-border-t gl-pt-3!"
+ id="reference-4"
+ >
+ Content 2
+ </div>
+ <div
+ class="gl-border-gray-500 gl-border-t gl-pt-3!"
+ id="reference-5"
+ >
+ Content 3
+ </div>
+ </div>
+</div>
+`;
+
+exports[`SourceViewer utils toggleBlameClasses removes classes 1`] = `
+<div
+ class="content"
+>
+ <div>
+ <div
+ id="reference-0"
+ >
+ 1
+ </div>
+ <div
+ id="reference-1"
+ >
+ 2
+ </div>
+ <div
+ id="reference-2"
+ >
+ 3
+ </div>
+ </div>
+ <div>
+ <div
+ id="reference-3"
+ >
+ Content 1
+ </div>
+ <div
+ id="reference-4"
+ >
+ Content 2
+ </div>
+ <div
+ id="reference-5"
+ >
+ Content 3
+ </div>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js
new file mode 100644
index 00000000000..ff8b2be9634
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/components/blame_info_spec.js
@@ -0,0 +1,63 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { setHTMLFixture } from 'helpers/fixtures';
+import CommitInfo from '~/repository/components/commit_info.vue';
+import BlameInfo from '~/vue_shared/components/source_viewer/components/blame_info.vue';
+import * as utils from '~/vue_shared/components/source_viewer/utils';
+import { SOURCE_CODE_CONTENT_MOCK, BLAME_DATA_MOCK } from '../mock_data';
+
+describe('BlameInfo component', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(BlameInfo, {
+ propsData: { blameData: BLAME_DATA_MOCK },
+ });
+ };
+
+ beforeEach(() => {
+ setHTMLFixture(SOURCE_CODE_CONTENT_MOCK);
+ jest.spyOn(utils, 'toggleBlameClasses');
+ createComponent();
+ });
+
+ const findCommitInfoComponents = () => wrapper.findAllComponents(CommitInfo);
+
+ it('adds the necessary classes to the DOM', () => {
+ expect(utils.toggleBlameClasses).toHaveBeenCalledWith(BLAME_DATA_MOCK, true);
+ });
+
+ it('renders a CommitInfo component for each blame entry', () => {
+ expect(findCommitInfoComponents().length).toBe(BLAME_DATA_MOCK.length);
+ });
+
+ it.each(BLAME_DATA_MOCK)(
+ 'sets the correct data and positioning for the commitInfo',
+ ({ lineno, commit, index }) => {
+ const commitInfoComponent = findCommitInfoComponents().at(index);
+
+ expect(commitInfoComponent.props('commit')).toEqual(commit);
+ expect(commitInfoComponent.element.style.top).toBe(utils.calculateBlameOffset(lineno));
+ },
+ );
+
+ describe('commitInfo component styling', () => {
+ const borderTopClassName = 'gl-border-t';
+
+ it('does not add a top border for the first entry', () => {
+ expect(findCommitInfoComponents().at(0).element.classList).not.toContain(borderTopClassName);
+ });
+
+ it('add a top border for the rest of the entries', () => {
+ expect(findCommitInfoComponents().at(1).element.classList).toContain(borderTopClassName);
+ expect(findCommitInfoComponents().at(2).element.classList).toContain(borderTopClassName);
+ });
+ });
+
+ describe('when component is destroyed', () => {
+ beforeEach(() => wrapper.destroy());
+
+ it('resets the DOM to its original state', () => {
+ expect(utils.toggleBlameClasses).toHaveBeenCalledWith(BLAME_DATA_MOCK, false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
index f35e9607d5c..b3516f7ed72 100644
--- a/spec/frontend/vue_shared/components/source_viewer/mock_data.js
+++ b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
@@ -22,3 +22,24 @@ export const CHUNK_2 = {
startingFrom: 70,
blamePath,
};
+
+export const SOURCE_CODE_CONTENT_MOCK = `
+<div class="content">
+ <div>
+ <div id="L1">1</div>
+ <div id="L2">2</div>
+ <div id="L3">3</div>
+ </div>
+
+ <div>
+ <div id="LC1">Content 1</div>
+ <div id="LC2">Content 2</div>
+ <div id="LC3">Content 3</div>
+ </div>
+</div>`;
+
+export const BLAME_DATA_MOCK = [
+ { lineno: 1, commit: { author: 'Peter' }, index: 0 },
+ { lineno: 2, commit: { author: 'Sarah' }, index: 1 },
+ { lineno: 3, commit: { author: 'Peter' }, index: 2 },
+];
diff --git a/spec/frontend/vue_shared/components/source_viewer/utils_spec.js b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
new file mode 100644
index 00000000000..0ac72aa9afb
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/utils_spec.js
@@ -0,0 +1,35 @@
+import { setHTMLFixture } from 'helpers/fixtures';
+import {
+ calculateBlameOffset,
+ toggleBlameClasses,
+} from '~/vue_shared/components/source_viewer/utils';
+import { SOURCE_CODE_CONTENT_MOCK, BLAME_DATA_MOCK } from './mock_data';
+
+describe('SourceViewer utils', () => {
+ beforeEach(() => setHTMLFixture(SOURCE_CODE_CONTENT_MOCK));
+
+ const findContent = () => document.querySelector('.content');
+
+ describe('calculateBlameOffset', () => {
+ it('returns an offset of zero if line number === 1', () => {
+ expect(calculateBlameOffset(1)).toBe('0px');
+ });
+
+ it('calculates an offset for the blame component', () => {
+ const { offsetTop } = document.querySelector('#LC3');
+ expect(calculateBlameOffset(3)).toBe(`${offsetTop}px`);
+ });
+ });
+
+ describe('toggleBlameClasses', () => {
+ it('adds classes', () => {
+ toggleBlameClasses(BLAME_DATA_MOCK, true);
+ expect(findContent()).toMatchSnapshot();
+ });
+
+ it('removes classes', () => {
+ toggleBlameClasses(BLAME_DATA_MOCK, false);
+ expect(findContent()).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
index 17a363ad8b1..41cf1d2b2e8 100644
--- a/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
+++ b/spec/frontend/vue_shared/components/time_ago_tooltip_spec.js
@@ -1,4 +1,5 @@
import { shallowMount } from '@vue/test-utils';
+import { GlTruncate } from '@gitlab/ui';
import timezoneMock from 'timezone-mock';
import { formatDate, getTimeago } from '~/lib/utils/datetime_utility';
@@ -36,6 +37,14 @@ describe('Time ago with tooltip component', () => {
expect(vm.text()).toEqual(timeAgoTimestamp);
});
+ it('should render truncated value with gl-truncate as true', () => {
+ buildVm({
+ enableTruncation: true,
+ });
+
+ expect(vm.findComponent(GlTruncate).exists()).toBe(true);
+ });
+
it('should render provided html class', () => {
buildVm({
cssClass: 'foo',
diff --git a/spec/frontend/vue_shared/components/toggle_labels_spec.js b/spec/frontend/vue_shared/components/toggle_labels_spec.js
new file mode 100644
index 00000000000..e4b4b7f9e0c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/toggle_labels_spec.js
@@ -0,0 +1,56 @@
+import { GlToggle } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+import ToggleLabels from '~/vue_shared/components/toggle_labels.vue';
+import isShowingLabelsQuery from '~/graphql_shared/client/is_showing_labels.query.graphql';
+
+Vue.use(VueApollo);
+
+describe('ToggleLabels', () => {
+ let wrapper;
+
+ const findToggle = () => wrapper.findComponent(GlToggle);
+
+ const mockSetIsShowingLabelsResolver = jest.fn();
+ const mockApollo = createMockApollo([], {
+ Mutation: {
+ setIsShowingLabels: mockSetIsShowingLabelsResolver,
+ },
+ });
+
+ const createComponent = () => {
+ mockApollo.clients.defaultClient.cache.writeQuery({
+ query: isShowingLabelsQuery,
+ data: {
+ isShowingLabels: true,
+ },
+ });
+ wrapper = shallowMountExtended(ToggleLabels, {
+ apolloProvider: mockApollo,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('calls setIsShowingLabelsMutation on toggle', async () => {
+ expect(findToggle().props('value')).toBe(true);
+ findToggle().vm.$emit('change', false);
+
+ await waitForPromises();
+
+ expect(mockSetIsShowingLabelsResolver).toHaveBeenCalledWith(
+ {},
+ {
+ isShowingLabels: false,
+ },
+ expect.anything(),
+ expect.anything(),
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js b/spec/frontend/vue_shared/components/vuex_module_provider_spec.js
index e24c5a4609d..95f557b10c1 100644
--- a/spec/frontend/vue_shared/components/vuex_module_provider_spec.js
+++ b/spec/frontend/vue_shared/components/vuex_module_provider_spec.js
@@ -1,6 +1,4 @@
import { mount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
import VuexModuleProvider from '~/vue_shared/components/vuex_module_provider.vue';
const TestComponent = {
@@ -38,12 +36,4 @@ describe('~/vue_shared/components/vuex_module_provider', () => {
});
expect(findProvidedVuexModule()).toBe(TEST_VUEX_MODULE);
});
-
- it('does not blow up when used with vue-apollo', () => {
- // See https://github.com/vuejs/vue-apollo/pull/1153 for details
- Vue.use(VueApollo);
-
- createComponent();
- expect(findProvidedVuexModule()).toBe(TEST_VUEX_MODULE);
- });
});
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
index 03f509a3fa3..35e3564c599 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_create_root_spec.js
@@ -5,6 +5,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import IssuableCreateRoot from '~/vue_shared/issuable/create/components/issuable_create_root.vue';
import IssuableForm from '~/vue_shared/issuable/create/components/issuable_form.vue';
+import { TYPE_TEST_CASE } from '~/issues/constants';
Vue.use(VueApollo);
@@ -13,6 +14,7 @@ const createComponent = ({
descriptionHelpPath = '/help/user/markdown',
labelsFetchPath = '/gitlab-org/gitlab-shell/-/labels.json',
labelsManagePath = '/gitlab-org/gitlab-shell/-/labels',
+ issuableType = TYPE_TEST_CASE,
} = {}) => {
return mount(IssuableCreateRoot, {
propsData: {
@@ -20,6 +22,7 @@ const createComponent = ({
descriptionHelpPath,
labelsFetchPath,
labelsManagePath,
+ issuableType,
},
apolloProvider: createMockApollo(),
slots: {
diff --git a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
index 62361705843..61185f913d9 100644
--- a/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/create/components/issuable_form_spec.js
@@ -1,9 +1,10 @@
-import { GlFormInput } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlFormInput, GlFormGroup, GlFormCheckbox } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import IssuableForm from '~/vue_shared/issuable/create/components/issuable_form.vue';
import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
import LabelsSelect from '~/sidebar/components/labels/labels_select_vue/labels_select_root.vue';
+import { TYPE_TEST_CASE } from '~/issues/constants';
import { __ } from '~/locale';
const createComponent = ({
@@ -11,13 +12,15 @@ const createComponent = ({
descriptionHelpPath = '/help/user/markdown',
labelsFetchPath = '/gitlab-org/gitlab-shell/-/labels.json',
labelsManagePath = '/gitlab-org/gitlab-shell/-/labels',
+ issuableType = TYPE_TEST_CASE,
} = {}) => {
- return shallowMount(IssuableForm, {
+ return shallowMountExtended(IssuableForm, {
propsData: {
descriptionPreviewPath,
descriptionHelpPath,
labelsFetchPath,
labelsManagePath,
+ issuableType,
},
slots: {
actions: `
@@ -58,7 +61,7 @@ describe('IssuableForm', () => {
describe('template', () => {
it('renders issuable title input field', () => {
- const titleFieldEl = wrapper.find('[data-testid="issuable-title"]');
+ const titleFieldEl = wrapper.findByTestId('issuable-title');
expect(titleFieldEl.exists()).toBe(true);
expect(titleFieldEl.find('label').text()).toBe('Title');
@@ -68,7 +71,7 @@ describe('IssuableForm', () => {
});
it('renders issuable description input field', () => {
- const descriptionFieldEl = wrapper.find('[data-testid="issuable-description"]');
+ const descriptionFieldEl = wrapper.findByTestId('issuable-description');
expect(descriptionFieldEl.exists()).toBe(true);
expect(descriptionFieldEl.find('label').text()).toBe('Description');
@@ -88,8 +91,23 @@ describe('IssuableForm', () => {
});
});
+ it('renders issuable confidential checkbox', () => {
+ const confidentialCheckboxEl = wrapper.findByTestId('issuable-confidential');
+ expect(confidentialCheckboxEl.exists()).toBe(true);
+
+ expect(confidentialCheckboxEl.findComponent(GlFormGroup).exists()).toBe(true);
+ expect(confidentialCheckboxEl.findComponent(GlFormGroup).attributes('label')).toBe(
+ 'Confidentiality',
+ );
+
+ expect(confidentialCheckboxEl.findComponent(GlFormCheckbox).exists()).toBe(true);
+ expect(confidentialCheckboxEl.findComponent(GlFormCheckbox).text()).toBe(
+ 'This test case is confidential and should only be visible to team members with at least Reporter access.',
+ );
+ });
+
it('renders labels select field', () => {
- const labelsSelectEl = wrapper.find('[data-testid="issuable-labels"]');
+ const labelsSelectEl = wrapper.findByTestId('issuable-labels');
expect(labelsSelectEl.exists()).toBe(true);
expect(labelsSelectEl.find('label').text()).toBe('Labels');
@@ -111,7 +129,7 @@ describe('IssuableForm', () => {
it('renders contents for slot "actions"', () => {
const buttonEl = wrapper
- .find('[data-testid="issuable-create-actions"]')
+ .findByTestId('issuable-create-actions')
.find('button.js-issuable-save');
expect(buttonEl.exists()).toBe(true);
diff --git a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
index 9f7254ba0e6..47da111b604 100644
--- a/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
+++ b/spec/frontend/vue_shared/issuable/list/components/issuable_item_spec.js
@@ -1,6 +1,5 @@
import { GlLink, GlLabel, GlIcon, GlFormCheckbox, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
-import { escape } from 'lodash';
import { useFakeDate } from 'helpers/fake_date';
import { shallowMountExtended as shallowMount } from 'helpers/vue_test_utils_helper';
import IssuableItem from '~/vue_shared/issuable/list/components/issuable_item.vue';
@@ -288,23 +287,10 @@ describe('IssuableItem', () => {
expect(titleEl.exists()).toBe(true);
expect(titleEl.findComponent(GlLink).attributes('href')).toBe(expectedHref);
expect(titleEl.findComponent(GlLink).attributes('target')).toBe(expectedTarget);
- expect(titleEl.findComponent(GlLink).html()).toContain(mockIssuable.titleHtml);
+ expect(titleEl.findComponent(GlLink).text()).toBe(mockIssuable.title);
},
);
- it('renders issuable title with escaped markup when issue tracker is external', () => {
- const mockTitle = '<script>foobar</script>';
- wrapper = createComponent({
- issuable: {
- ...mockIssuable,
- title: mockTitle,
- externalTracker: 'jira',
- },
- });
-
- expect(wrapper.findByTestId('issuable-title').html()).toContain(escape(mockTitle));
- });
-
it('renders checkbox when `showCheckbox` prop is true', async () => {
wrapper = createComponent({
showCheckbox: true,
@@ -366,7 +352,7 @@ describe('IssuableItem', () => {
expect(hiddenIcon.props('name')).toBe('spam');
expect(hiddenIcon.attributes()).toMatchObject({
- title: 'This issue is hidden because its author has been banned',
+ title: 'This issue is hidden because its author has been banned.',
arialabel: 'Hidden',
});
});
diff --git a/spec/frontend/vue_shared/issuable/list/mock_data.js b/spec/frontend/vue_shared/issuable/list/mock_data.js
index b39d177f292..f8cf3ba5271 100644
--- a/spec/frontend/vue_shared/issuable/list/mock_data.js
+++ b/spec/frontend/vue_shared/issuable/list/mock_data.js
@@ -42,7 +42,7 @@ export const mockCurrentUserTodo = {
export const mockIssuable = {
iid: '30',
title: 'Dismiss Cipher with no integrity',
- titleHtml: '<gl-emoji title="party-parrot"></gl-emoji>Dismiss Cipher with no integrity',
+ titleHtml: 'Dismiss Cipher with no integrity',
description: 'fortitudinis _fomentis_ dolor mitigari solet.',
descriptionHtml: 'fortitudinis <i>fomentis</i> dolor mitigari solet.',
state: 'opened',
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
index 3b6f06d835b..03395e5dfc0 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_header_spec.js
@@ -2,6 +2,8 @@ import { GlBadge, GlButton, GlIcon, GlLink, GlSprintf } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import HiddenBadge from '~/issuable/components/hidden_badge.vue';
+import LockedBadge from '~/issuable/components/locked_badge.vue';
import { STATUS_CLOSED, STATUS_OPEN, STATUS_REOPENED, TYPE_ISSUE } from '~/issues/constants';
import { __ } from '~/locale';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
@@ -23,8 +25,8 @@ describe('IssuableHeader component', () => {
wrapper.findAllComponents(GlIcon).filter((component) => component.props('name') === name);
const findIcon = (name) =>
findGlIconWithName(name).exists() ? findGlIconWithName(name).at(0) : undefined;
- const findBlockedIcon = () => findIcon('lock');
- const findHiddenIcon = () => findIcon('spam');
+ const findBlockedBadge = () => wrapper.findComponent(LockedBadge);
+ const findHiddenBadge = () => wrapper.findComponent(HiddenBadge);
const findExternalLinkIcon = () => findIcon('external-link');
const findFirstContributionIcon = () => findIcon('first-contribution');
const findComponentTooltip = (component) => getBinding(component.element, 'gl-tooltip');
@@ -111,49 +113,31 @@ describe('IssuableHeader component', () => {
});
});
- describe('blocked icon', () => {
+ describe('blocked badge', () => {
it('renders when issuable is blocked', () => {
createComponent({ blocked: true });
- expect(findBlockedIcon().props('ariaLabel')).toBe('Blocked');
- });
-
- it('has tooltip', () => {
- createComponent({ blocked: true });
-
- expect(findComponentTooltip(findBlockedIcon())).toBeDefined();
- expect(findBlockedIcon().attributes('title')).toBe(
- 'This issue is locked. Only project members can comment.',
- );
+ expect(findBlockedBadge().props('issuableType')).toBe('issue');
});
it('does not render when issuable is not blocked', () => {
createComponent({ blocked: false });
- expect(findBlockedIcon()).toBeUndefined();
+ expect(findBlockedBadge().exists()).toBe(false);
});
});
- describe('hidden icon', () => {
+ describe('hidden badge', () => {
it('renders when issuable is hidden', () => {
createComponent({ isHidden: true });
- expect(findHiddenIcon().props('ariaLabel')).toBe('Hidden');
- });
-
- it('has tooltip', () => {
- createComponent({ isHidden: true });
-
- expect(findComponentTooltip(findHiddenIcon())).toBeDefined();
- expect(findHiddenIcon().attributes('title')).toBe(
- 'This issue is hidden because its author has been banned',
- );
+ expect(findHiddenBadge().props('issuableType')).toBe('issue');
});
it('does not render when issuable is not hidden', () => {
createComponent({ isHidden: false });
- expect(findHiddenIcon()).toBeUndefined();
+ expect(findHiddenBadge().exists()).toBe(false);
});
});
diff --git a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
index 826fc2b2230..b2b372d9d0d 100644
--- a/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
@@ -10,9 +10,11 @@ import WorkItemCommentLocked from '~/work_items/components/notes/work_item_comme
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
import createNoteMutation from '~/work_items/graphql/notes/create_work_item_note.mutation.graphql';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
createWorkItemNoteResponse,
+ groupWorkItemByIidResponseFactory,
workItemByIidResponseFactory,
workItemQueryResponse,
} from '../../mock_data';
@@ -29,6 +31,7 @@ describe('Work item add note', () => {
const mutationSuccessHandler = jest.fn().mockResolvedValue(createWorkItemNoteResponse);
let workItemResponseHandler;
+ let groupWorkItemResponseHandler;
const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
const findTextarea = () => wrapper.findByTestId('note-reply-textarea');
@@ -40,29 +43,32 @@ describe('Work item add note', () => {
canCreateNote = true,
workItemIid = '1',
workItemResponse = workItemByIidResponseFactory({ canUpdate, canCreateNote }),
+ groupWorkItemResponse = groupWorkItemByIidResponseFactory({ canUpdate, canCreateNote }),
signedIn = true,
isEditing = true,
+ isGroup = false,
workItemType = 'Task',
isInternalThread = false,
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
+ groupWorkItemResponseHandler = jest.fn().mockResolvedValue(groupWorkItemResponse);
if (signedIn) {
window.gon.current_user_id = '1';
window.gon.current_user_avatar_url = 'avatar.png';
}
- const apolloProvider = createMockApollo([
- [workItemByIidQuery, workItemResponseHandler],
- [createNoteMutation, mutationHandler],
- ]);
-
const { id } = workItemQueryResponse.data.workItem;
wrapper = shallowMountExtended(WorkItemAddNote, {
- apolloProvider,
+ apolloProvider: createMockApollo([
+ [workItemByIidQuery, workItemResponseHandler],
+ [groupWorkItemByIidQuery, groupWorkItemResponseHandler],
+ [createNoteMutation, mutationHandler],
+ ]),
provide: {
- fullPath: 'test-project-path',
+ isGroup,
},
propsData: {
+ fullPath: 'test-project-path',
workItemId: id,
workItemIid,
workItemType,
@@ -272,16 +278,44 @@ describe('Work item add note', () => {
});
});
- it('calls the work item query', async () => {
- await createComponent();
+ describe('when project context', () => {
+ it('calls the project work item query', async () => {
+ await createComponent();
+
+ expect(workItemResponseHandler).toHaveBeenCalled();
+ });
+
+ it('skips calling the group work item query', async () => {
+ await createComponent();
+
+ expect(groupWorkItemResponseHandler).not.toHaveBeenCalled();
+ });
+
+ it('skips calling the project work item query when missing workItemIid', async () => {
+ await createComponent({ workItemIid: '', isEditing: false });
- expect(workItemResponseHandler).toHaveBeenCalled();
+ expect(workItemResponseHandler).not.toHaveBeenCalled();
+ });
});
- it('skips calling the work item query when missing workItemIid', async () => {
- await createComponent({ workItemIid: '', isEditing: false });
+ describe('when group context', () => {
+ it('skips calling the project work item query', async () => {
+ await createComponent({ isGroup: true });
+
+ expect(workItemResponseHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the group work item query', async () => {
+ await createComponent({ isGroup: true });
- expect(workItemResponseHandler).not.toHaveBeenCalled();
+ expect(groupWorkItemResponseHandler).toHaveBeenCalled();
+ });
+
+ it('skips calling the group work item query when missing workItemIid', async () => {
+ await createComponent({ isGroup: true, workItemIid: '', isEditing: false });
+
+ expect(groupWorkItemResponseHandler).not.toHaveBeenCalled();
+ });
});
it('wrapper adds `internal-note` class when internal thread', async () => {
diff --git a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
index dd88f34ae4f..ee2b434bd75 100644
--- a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
@@ -48,6 +48,7 @@ describe('Work item comment form component', () => {
} = {}) => {
wrapper = shallowMount(WorkItemCommentForm, {
propsData: {
+ fullPath: 'test-project-path',
workItemState,
workItemId,
workItemType,
@@ -59,9 +60,6 @@ describe('Work item comment form component', () => {
autocompleteDataSources: {},
isNewDiscussion,
},
- provide: {
- fullPath: 'test-project-path',
- },
directives: {
GlTooltip: createMockDirective('gl-tooltip'),
},
diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
index 9d22a64f2cb..fa53ba54faa 100644
--- a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
@@ -31,10 +31,8 @@ describe('Work Item Discussion', () => {
workItemType = 'Task',
} = {}) => {
wrapper = shallowMount(WorkItemDiscussion, {
- provide: {
- fullPath: 'gitlab-org',
- },
propsData: {
+ fullPath: 'gitlab-org',
discussion,
workItemId,
workItemIid: '1',
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
index e4180b2d178..6a24987b737 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -48,6 +48,7 @@ describe('Work Item Note Actions', () => {
} = {}) => {
wrapper = shallowMountExtended(WorkItemNoteActions, {
propsData: {
+ fullPath: 'gitlab-org',
showReply,
showEdit,
workItemIid: '1',
@@ -63,7 +64,6 @@ describe('Work Item Note Actions', () => {
projectName,
},
provide: {
- fullPath: 'gitlab-org',
glFeatures: {
workItemsMvc2: true,
},
diff --git a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
index d425f1e50dc..ce915635946 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_awards_list_spec.js
@@ -61,10 +61,8 @@ describe('Work Item Note Awards List', () => {
});
wrapper = shallowMount(WorkItemNoteAwardsList, {
- provide: {
- fullPath,
- },
propsData: {
+ fullPath,
workItemIid,
note,
isModal: false,
diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js
index 9049a69656a..2b4c9604382 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -15,8 +15,10 @@ import NoteActions from '~/work_items/components/notes/work_item_note_actions.vu
import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
import updateWorkItemNoteMutation from '~/work_items/graphql/notes/update_work_item_note.mutation.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
+ groupWorkItemByIidResponseFactory,
mockAssignees,
mockWorkItemCommentNote,
updateWorkItemMutationResponse,
@@ -68,6 +70,9 @@ describe('Work Item Note', () => {
});
const workItemResponseHandler = jest.fn().mockResolvedValue(workItemByIidResponseFactory());
+ const groupWorkItemResponseHandler = jest
+ .fn()
+ .mockResolvedValue(groupWorkItemByIidResponseFactory());
const workItemByAuthoredByDifferentUser = jest
.fn()
.mockResolvedValue(mockWorkItemByDifferentUser);
@@ -90,6 +95,7 @@ describe('Work Item Note', () => {
const createComponent = ({
note = mockWorkItemCommentNote,
isFirstNote = false,
+ isGroup = false,
updateNoteMutationHandler = successHandler,
workItemId = mockWorkItemId,
updateWorkItemMutationHandler = updateWorkItemMutationSuccessHandler,
@@ -98,9 +104,10 @@ describe('Work Item Note', () => {
} = {}) => {
wrapper = shallowMount(WorkItemNote, {
provide: {
- fullPath: 'test-project-path',
+ isGroup,
},
propsData: {
+ fullPath: 'test-project-path',
workItemId,
workItemIid: '1',
note,
@@ -112,6 +119,7 @@ describe('Work Item Note', () => {
},
apolloProvider: mockApollo([
[workItemByIidQuery, workItemByIidResponseHandler],
+ [groupWorkItemByIidQuery, groupWorkItemResponseHandler],
[updateWorkItemNoteMutation, updateNoteMutationHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
]),
@@ -442,4 +450,32 @@ describe('Work Item Note', () => {
expect(findAwardsList().props('workItemIid')).toBe('1');
});
});
+
+ describe('when project context', () => {
+ it('calls the project work item query', () => {
+ createComponent();
+
+ expect(workItemResponseHandler).toHaveBeenCalled();
+ });
+
+ it('skips calling the group work item query', () => {
+ createComponent();
+
+ expect(groupWorkItemResponseHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when group context', () => {
+ it('skips calling the project work item query', () => {
+ createComponent({ isGroup: true });
+
+ expect(workItemResponseHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the group work item query', () => {
+ createComponent({ isGroup: true });
+
+ expect(groupWorkItemResponseHandler).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
index b86f9ff34ae..2e1a7983dec 100644
--- a/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
+++ b/spec/frontend/work_items/components/shared/work_item_link_child_contents_spec.js
@@ -1,4 +1,4 @@
-import { GlLabel, GlIcon } from '@gitlab/ui';
+import { GlLabel, GlIcon, GlLink } from '@gitlab/ui';
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -33,7 +33,7 @@ describe('WorkItemLinkChildContents', () => {
const findStatusIconComponent = () =>
wrapper.findByTestId('item-status-icon').findComponent(GlIcon);
const findConfidentialIconComponent = () => wrapper.findByTestId('confidential-icon');
- const findTitleEl = () => wrapper.findByTestId('item-title');
+ const findTitleEl = () => wrapper.findComponent(GlLink);
const findStatusTooltipComponent = () => wrapper.findComponent(RichTimestampTooltip);
const findMetadataComponent = () => wrapper.findComponent(WorkItemLinkChildMetadata);
const findAllLabels = () => wrapper.findAllComponents(GlLabel);
@@ -46,7 +46,6 @@ describe('WorkItemLinkChildContents', () => {
propsData: {
canUpdate,
childItem,
- childPath: '/gitlab-org/gitlab-test/-/work_items/4',
},
});
};
diff --git a/spec/frontend/work_items/components/work_item_actions_spec.js b/spec/frontend/work_items/components/work_item_actions_spec.js
index 0098a2e0864..15c33bf5b1e 100644
--- a/spec/frontend/work_items/components/work_item_actions_spec.js
+++ b/spec/frontend/work_items/components/work_item_actions_spec.js
@@ -22,13 +22,12 @@ import {
import updateWorkItemNotificationsMutation from '~/work_items/graphql/update_work_item_notifications.mutation.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import convertWorkItemMutation from '~/work_items/graphql/work_item_convert.mutation.graphql';
-import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
convertWorkItemMutationResponse,
projectWorkItemTypesQueryResponse,
convertWorkItemMutationErrorResponse,
- workItemByIidResponseFactory,
+ updateWorkItemNotificationsMutationResponse,
} from '../mock_data';
jest.mock('~/lib/utils/common_utils');
@@ -38,10 +37,7 @@ describe('WorkItemActions component', () => {
Vue.use(VueApollo);
let wrapper;
- let mockApollo;
const mockWorkItemReference = 'gitlab-org/gitlab-test#1';
- const mockWorkItemIid = '1';
- const mockFullPath = 'gitlab-org/gitlab-test';
const mockWorkItemCreateNoteEmail =
'gitlab-incoming+gitlab-org-gitlab-test-2-ddpzuq0zd2wefzofcpcdr3dg7-issue-1@gmail.com';
@@ -75,14 +71,22 @@ describe('WorkItemActions component', () => {
hide: jest.fn(),
};
+ const typesQuerySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
const convertWorkItemMutationSuccessHandler = jest
.fn()
.mockResolvedValue(convertWorkItemMutationResponse);
-
const convertWorkItemMutationErrorHandler = jest
.fn()
.mockResolvedValue(convertWorkItemMutationErrorResponse);
- const typesQuerySuccessHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
+ const toggleNotificationsOffHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemNotificationsMutationResponse(false));
+ const toggleNotificationsOnHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemNotificationsMutationResponse(true));
+ const toggleNotificationsFailureHandler = jest
+ .fn()
+ .mockRejectedValue(new Error('Failed to subscribe'));
const createComponent = ({
canUpdate = true,
@@ -90,35 +94,21 @@ describe('WorkItemActions component', () => {
isConfidential = false,
subscribed = false,
isParentConfidential = false,
- notificationsMock = [updateWorkItemNotificationsMutation, jest.fn()],
convertWorkItemMutationHandler = convertWorkItemMutationSuccessHandler,
+ notificationsMutationHandler,
workItemType = 'Task',
workItemReference = mockWorkItemReference,
workItemCreateNoteEmail = mockWorkItemCreateNoteEmail,
- writeQueryCache = false,
} = {}) => {
- const handlers = [notificationsMock];
- mockApollo = createMockApollo([
- ...handlers,
- [convertWorkItemMutation, convertWorkItemMutationHandler],
- [projectWorkItemTypesQuery, typesQuerySuccessHandler],
- ]);
-
- // Write the query cache only when required e.g., notification widget mutation is called
- if (writeQueryCache) {
- const workItemQueryResponse = workItemByIidResponseFactory({ canUpdate: true });
-
- mockApollo.clients.defaultClient.cache.writeQuery({
- query: workItemByIidQuery,
- variables: { fullPath: mockFullPath, iid: mockWorkItemIid },
- data: workItemQueryResponse.data,
- });
- }
-
wrapper = shallowMountExtended(WorkItemActions, {
isLoggedIn: isLoggedIn(),
- apolloProvider: mockApollo,
+ apolloProvider: createMockApollo([
+ [projectWorkItemTypesQuery, typesQuerySuccessHandler],
+ [convertWorkItemMutation, convertWorkItemMutationHandler],
+ [updateWorkItemNotificationsMutation, notificationsMutationHandler],
+ ]),
propsData: {
+ fullPath: 'gitlab-org/gitlab-test',
workItemId: 'gid://gitlab/WorkItem/1',
canUpdate,
canDelete,
@@ -128,10 +118,9 @@ describe('WorkItemActions component', () => {
workItemType,
workItemReference,
workItemCreateNoteEmail,
- workItemIid: '1',
},
provide: {
- fullPath: mockFullPath,
+ isGroup: false,
glFeatures: { workItemsMvc2: true },
},
mocks: {
@@ -159,7 +148,6 @@ describe('WorkItemActions component', () => {
it('renders modal', () => {
createComponent();
- expect(findModal().exists()).toBe(true);
expect(findModal().props('visible')).toBe(false);
});
@@ -247,59 +235,15 @@ describe('WorkItemActions component', () => {
});
it('does not render when canDelete is false', () => {
- createComponent({
- canDelete: false,
- });
+ createComponent({ canDelete: false });
expect(findDeleteButton().exists()).toBe(false);
});
});
describe('notifications action', () => {
- const errorMessage = 'Failed to subscribe';
- const notificationToggledOffMessage = 'Notifications turned off.';
- const notificationToggledOnMessage = 'Notifications turned on.';
-
- const toggleNotificationsOffHandler = jest.fn().mockResolvedValue({
- data: {
- updateWorkItemNotificationsSubscription: {
- issue: {
- id: 'gid://gitlab/WorkItem/1',
- subscribed: false,
- },
- errors: [],
- },
- },
- });
-
- const toggleNotificationsOnHandler = jest.fn().mockResolvedValue({
- data: {
- updateWorkItemNotificationsSubscription: {
- issue: {
- id: 'gid://gitlab/WorkItem/1',
- subscribed: true,
- },
- errors: [],
- },
- },
- });
-
- const toggleNotificationsFailureHandler = jest.fn().mockRejectedValue(new Error(errorMessage));
-
- const notificationsOffMock = [
- updateWorkItemNotificationsMutation,
- toggleNotificationsOffHandler,
- ];
-
- const notificationsOnMock = [updateWorkItemNotificationsMutation, toggleNotificationsOnHandler];
-
- const notificationsFailureMock = [
- updateWorkItemNotificationsMutation,
- toggleNotificationsFailureHandler,
- ];
-
beforeEach(() => {
- createComponent({ writeQueryCache: true });
+ createComponent();
isLoggedIn.mockReturnValue(true);
});
@@ -308,25 +252,26 @@ describe('WorkItemActions component', () => {
});
it.each`
- scenario | subscribedToNotifications | notificationsMock | subscribedState | toastMessage
- ${'turned off'} | ${false} | ${notificationsOffMock} | ${false} | ${notificationToggledOffMessage}
- ${'turned on'} | ${true} | ${notificationsOnMock} | ${true} | ${notificationToggledOnMessage}
+ scenario | subscribedToNotifications | notificationsMutationHandler | subscribed | toastMessage
+ ${'turned off'} | ${false} | ${toggleNotificationsOffHandler} | ${false} | ${'Notifications turned off.'}
+ ${'turned on'} | ${true} | ${toggleNotificationsOnHandler} | ${true} | ${'Notifications turned on.'}
`(
'calls mutation and displays toast when notification toggle is $scenario',
- async ({ subscribedToNotifications, notificationsMock, subscribedState, toastMessage }) => {
- createComponent({ notificationsMock, writeQueryCache: true });
-
- await waitForPromises();
+ async ({
+ subscribedToNotifications,
+ notificationsMutationHandler,
+ subscribed,
+ toastMessage,
+ }) => {
+ createComponent({ notificationsMutationHandler });
findNotificationsToggle().vm.$emit('change', subscribedToNotifications);
-
await waitForPromises();
- expect(notificationsMock[1]).toHaveBeenCalledWith({
+ expect(notificationsMutationHandler).toHaveBeenCalledWith({
input: {
- projectPath: mockFullPath,
- iid: mockWorkItemIid,
- subscribedState,
+ id: 'gid://gitlab/WorkItem/1',
+ subscribed,
},
});
expect(toast).toHaveBeenCalledWith(toastMessage);
@@ -334,15 +279,12 @@ describe('WorkItemActions component', () => {
);
it('emits error when the update notification mutation fails', async () => {
- createComponent({ notificationsMock: notificationsFailureMock, writeQueryCache: true });
-
- await waitForPromises();
+ createComponent({ notificationsMutationHandler: toggleNotificationsFailureHandler });
findNotificationsToggle().vm.$emit('change', false);
-
await waitForPromises();
- expect(wrapper.emitted('error')).toEqual([[errorMessage]]);
+ expect(wrapper.emitted('error')).toEqual([['Failed to subscribe']]);
});
});
@@ -359,13 +301,11 @@ describe('WorkItemActions component', () => {
it('promote key result to objective', async () => {
createComponent({ workItemType: 'Key Result' });
-
- // wait for work item types
await waitForPromises();
expect(findPromoteButton().exists()).toBe(true);
- findPromoteButton().vm.$emit('action');
+ findPromoteButton().vm.$emit('action');
await waitForPromises();
expect(convertWorkItemMutationSuccessHandler).toHaveBeenCalled();
@@ -378,13 +318,11 @@ describe('WorkItemActions component', () => {
workItemType: 'Key Result',
convertWorkItemMutationHandler: convertWorkItemMutationErrorHandler,
});
-
- // wait for work item types
await waitForPromises();
expect(findPromoteButton().exists()).toBe(true);
- findPromoteButton().vm.$emit('action');
+ findPromoteButton().vm.$emit('action');
await waitForPromises();
expect(convertWorkItemMutationErrorHandler).toHaveBeenCalled();
@@ -399,6 +337,7 @@ describe('WorkItemActions component', () => {
createComponent();
expect(findCopyReferenceButton().exists()).toBe(true);
+
findCopyReferenceButton().vm.$emit('action');
expect(toast).toHaveBeenCalledWith('Reference copied');
@@ -421,6 +360,7 @@ describe('WorkItemActions component', () => {
createComponent();
expect(findCopyCreateNoteEmailButton().exists()).toBe(true);
+
findCopyCreateNoteEmailButton().vm.$emit('action');
expect(toast).toHaveBeenCalledWith('Email address copied');
diff --git a/spec/frontend/work_items/components/work_item_assignees_spec.js b/spec/frontend/work_items/components/work_item_assignees_spec.js
index 50a8847032e..196e19791df 100644
--- a/spec/frontend/work_items/components/work_item_assignees_spec.js
+++ b/spec/frontend/work_items/components/work_item_assignees_spec.js
@@ -6,7 +6,8 @@ import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { mockTracking } from 'helpers/tracking_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
-import userSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
+import groupUsersSearchQuery from '~/graphql_shared/queries/group_users_search.query.graphql';
+import usersSearchQuery from '~/graphql_shared/queries/users_search.query.graphql';
import currentUserQuery from '~/graphql_shared/queries/current_user.query.graphql';
import InviteMembersTrigger from '~/invite_members/components/invite_members_trigger.vue';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
@@ -53,6 +54,9 @@ describe('WorkItemAssignees component', () => {
const successSearchQueryHandler = jest
.fn()
.mockResolvedValue(projectMembersResponseWithCurrentUser);
+ const successGroupSearchQueryHandler = jest
+ .fn()
+ .mockResolvedValue(projectMembersResponseWithCurrentUser);
const successSearchQueryHandlerWithMoreAssignees = jest
.fn()
.mockResolvedValue(projectMembersResponseWithCurrentUserWithNextPage);
@@ -75,19 +79,22 @@ describe('WorkItemAssignees component', () => {
allowsMultipleAssignees = true,
canInviteMembers = false,
canUpdate = true,
+ isGroup = false,
} = {}) => {
const apolloProvider = createMockApollo([
- [userSearchQuery, searchQueryHandler],
+ [usersSearchQuery, searchQueryHandler],
+ [groupUsersSearchQuery, successGroupSearchQueryHandler],
[currentUserQuery, currentUserQueryHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
]);
wrapper = mountExtended(WorkItemAssignees, {
provide: {
- fullPath: 'test-project-path',
+ isGroup,
},
propsData: {
assignees,
+ fullPath: 'test-project-path',
workItemId,
allowsMultipleAssignees,
workItemType: TASK_TYPE_NAME,
@@ -540,4 +547,36 @@ describe('WorkItemAssignees component', () => {
expect(findTokenSelector().props('dropdownItems')).toHaveLength(2);
});
+
+ describe('when project context', () => {
+ beforeEach(() => {
+ createComponent();
+ findTokenSelector().vm.$emit('focus');
+ findTokenSelector().vm.$emit('text-input', 'jane');
+ });
+
+ it('calls the project users search query', () => {
+ expect(successSearchQueryHandler).toHaveBeenCalled();
+ });
+
+ it('does not call the group users search query', () => {
+ expect(successGroupSearchQueryHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when group context', () => {
+ beforeEach(() => {
+ createComponent({ isGroup: true });
+ findTokenSelector().vm.$emit('focus');
+ findTokenSelector().vm.$emit('text-input', 'jane');
+ });
+
+ it('does not call the project users search query', () => {
+ expect(successSearchQueryHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the group users search query', () => {
+ expect(successGroupSearchQueryHandler).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
index 8b7e04854af..123cf647674 100644
--- a/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
+++ b/spec/frontend/work_items/components/work_item_attributes_wrapper_spec.js
@@ -20,6 +20,7 @@ describe('WorkItemAttributesWrapper component', () => {
const createComponent = ({ workItem = workItemQueryResponse.data.workItem } = {}) => {
wrapper = shallowMount(WorkItemAttributesWrapper, {
propsData: {
+ fullPath: 'group/project',
workItem,
},
provide: {
@@ -28,7 +29,6 @@ describe('WorkItemAttributesWrapper component', () => {
hasOkrsFeature: true,
hasIssuableHealthStatusFeature: true,
projectNamespace: 'namespace',
- fullPath: 'group/project',
},
stubs: {
WorkItemWeight: true,
diff --git a/spec/frontend/work_items/components/work_item_created_updated_spec.js b/spec/frontend/work_items/components/work_item_created_updated_spec.js
index f77c5481906..3f14615e173 100644
--- a/spec/frontend/work_items/components/work_item_created_updated_spec.js
+++ b/spec/frontend/work_items/components/work_item_created_updated_spec.js
@@ -7,12 +7,18 @@ import waitForPromises from 'helpers/wait_for_promises';
import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
+import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
-import { workItemByIidResponseFactory, mockAssignees } from '../mock_data';
+import {
+ groupWorkItemByIidResponseFactory,
+ mockAssignees,
+ workItemByIidResponseFactory,
+} from '../mock_data';
describe('WorkItemCreatedUpdated component', () => {
let wrapper;
let successHandler;
+ let groupSuccessHandler;
Vue.use(VueApollo);
@@ -30,21 +36,31 @@ describe('WorkItemCreatedUpdated component', () => {
updatedAt,
confidential = false,
updateInProgress = false,
+ isGroup = false,
} = {}) => {
- const workItemQueryResponse = workItemByIidResponseFactory({
+ const workItemQueryResponse = workItemByIidResponseFactory({ author, updatedAt, confidential });
+ const groupWorkItemQueryResponse = groupWorkItemByIidResponseFactory({
author,
updatedAt,
confidential,
});
successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ groupSuccessHandler = jest.fn().mockResolvedValue(groupWorkItemQueryResponse);
wrapper = shallowMount(WorkItemCreatedUpdated, {
- apolloProvider: createMockApollo([[workItemByIidQuery, successHandler]]),
+ apolloProvider: createMockApollo([
+ [workItemByIidQuery, successHandler],
+ [groupWorkItemByIidQuery, groupSuccessHandler],
+ ]),
provide: {
+ isGroup,
+ },
+ propsData: {
fullPath: '/some/project',
+ workItemIid,
+ updateInProgress,
},
- propsData: { workItemIid, updateInProgress },
stubs: {
GlAvatarLink,
GlSprintf,
@@ -54,10 +70,44 @@ describe('WorkItemCreatedUpdated component', () => {
await waitForPromises();
};
- it('skips the work item query when workItemIid is not defined', async () => {
- await createComponent({ workItemIid: null });
+ describe('when project context', () => {
+ it('calls the project work item query', async () => {
+ await createComponent();
+
+ expect(successHandler).toHaveBeenCalled();
+ });
+
+ it('skips calling the group work item query', async () => {
+ await createComponent();
+
+ expect(groupSuccessHandler).not.toHaveBeenCalled();
+ });
+
+ it('skips calling the project work item query when workItemIid is not defined', async () => {
+ await createComponent({ workItemIid: null });
+
+ expect(successHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when group context', () => {
+ it('skips calling the project work item query', async () => {
+ await createComponent({ isGroup: true });
+
+ expect(successHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the group work item query', async () => {
+ await createComponent({ isGroup: true });
- expect(successHandler).not.toHaveBeenCalled();
+ expect(groupSuccessHandler).toHaveBeenCalled();
+ });
+
+ it('skips calling the group work item query when workItemIid is not defined', async () => {
+ await createComponent({ isGroup: true, workItemIid: null });
+
+ expect(groupSuccessHandler).not.toHaveBeenCalled();
+ });
});
it('shows work item type metadata with type and icon', async () => {
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index 8b9963b2476..de2895591dd 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -13,9 +13,11 @@ import WorkItemDescription from '~/work_items/components/work_item_description.v
import WorkItemDescriptionRendered from '~/work_items/components/work_item_description_rendered.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
import {
+ groupWorkItemByIidResponseFactory,
updateWorkItemMutationResponse,
workItemByIidResponseFactory,
workItemQueryResponse,
@@ -33,6 +35,7 @@ describe('WorkItemDescription', () => {
const mutationSuccessHandler = jest.fn().mockResolvedValue(updateWorkItemMutationResponse);
let workItemResponseHandler;
+ let groupWorkItemResponseHandler;
const findForm = () => wrapper.findComponent(GlForm);
const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
@@ -51,22 +54,28 @@ describe('WorkItemDescription', () => {
canUpdate = true,
workItemResponse = workItemByIidResponseFactory({ canUpdate }),
isEditing = false,
+ isGroup = false,
workItemIid = '1',
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
+ groupWorkItemResponseHandler = jest
+ .fn()
+ .mockResolvedValue(groupWorkItemByIidResponseFactory({ canUpdate }));
const { id } = workItemQueryResponse.data.workItem;
wrapper = shallowMount(WorkItemDescription, {
apolloProvider: createMockApollo([
[workItemByIidQuery, workItemResponseHandler],
+ [groupWorkItemByIidQuery, groupWorkItemResponseHandler],
[updateWorkItemMutation, mutationHandler],
]),
propsData: {
+ fullPath: 'test-project-path',
workItemId: id,
workItemIid,
},
provide: {
- fullPath: 'test-project-path',
+ isGroup,
},
});
@@ -247,9 +256,31 @@ describe('WorkItemDescription', () => {
});
});
- it('calls the work item query', async () => {
- await createComponent();
+ describe('when project context', () => {
+ it('calls the project work item query', () => {
+ createComponent();
+
+ expect(workItemResponseHandler).toHaveBeenCalled();
+ });
- expect(workItemResponseHandler).toHaveBeenCalled();
+ it('skips calling the group work item query', () => {
+ createComponent();
+
+ expect(groupWorkItemResponseHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when group context', () => {
+ it('skips calling the project work item query', () => {
+ createComponent({ isGroup: true });
+
+ expect(workItemResponseHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the group work item query', () => {
+ createComponent({ isGroup: true });
+
+ expect(groupWorkItemResponseHandler).toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_detail_spec.js b/spec/frontend/work_items/components/work_item_detail_spec.js
index fec6d0673c6..28826748cb0 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -28,12 +28,14 @@ import WorkItemStateToggleButton from '~/work_items/components/work_item_state_t
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import WorkItemTodos from '~/work_items/components/work_item_todos.vue';
import { i18n } from '~/work_items/constants';
+import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import updateWorkItemTaskMutation from '~/work_items/graphql/update_work_item_task.mutation.graphql';
import workItemUpdatedSubscription from '~/work_items/graphql/work_item_updated.subscription.graphql';
import {
+ groupWorkItemByIidResponseFactory,
mockParent,
workItemByIidResponseFactory,
objectiveType,
@@ -49,6 +51,10 @@ describe('WorkItemDetail component', () => {
Vue.use(VueApollo);
const workItemQueryResponse = workItemByIidResponseFactory({ canUpdate: true, canDelete: true });
+ const groupWorkItemQueryResponse = groupWorkItemByIidResponseFactory({
+ canUpdate: true,
+ canDelete: true,
+ });
const workItemQueryResponseWithCannotUpdate = workItemByIidResponseFactory({
canUpdate: false,
canDelete: false,
@@ -59,6 +65,7 @@ describe('WorkItemDetail component', () => {
canDelete: true,
});
const successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ const groupSuccessHandler = jest.fn().mockResolvedValue(groupWorkItemQueryResponse);
const showModalHandler = jest.fn();
const { id } = workItemQueryResponse.data.workspace.workItems.nodes[0];
const workItemUpdatedSubscriptionHandler = jest
@@ -92,6 +99,7 @@ describe('WorkItemDetail component', () => {
const findWorkItemTypeIcon = () => wrapper.findComponent(WorkItemTypeIcon);
const createComponent = ({
+ isGroup = false,
isModal = false,
updateInProgress = false,
workItemIid = '1',
@@ -101,14 +109,13 @@ describe('WorkItemDetail component', () => {
workItemsMvc2Enabled = false,
linkedWorkItemsEnabled = false,
} = {}) => {
- const handlers = [
- [workItemByIidQuery, handler],
- [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler],
- confidentialityMock,
- ];
-
wrapper = shallowMountExtended(WorkItemDetail, {
- apolloProvider: createMockApollo(handlers),
+ apolloProvider: createMockApollo([
+ [workItemByIidQuery, handler],
+ [groupWorkItemByIidQuery, groupSuccessHandler],
+ [workItemUpdatedSubscription, workItemUpdatedSubscriptionHandler],
+ confidentialityMock,
+ ]),
isLoggedIn: isLoggedIn(),
propsData: {
isModal,
@@ -131,6 +138,7 @@ describe('WorkItemDetail component', () => {
hasIssuableHealthStatusFeature: true,
projectNamespace: 'namespace',
fullPath: 'group/project',
+ isGroup,
reportAbusePath: '/report/abuse/path',
},
stubs: {
@@ -484,25 +492,64 @@ describe('WorkItemDetail component', () => {
expect(findAlert().text()).toBe(updateError);
});
- it('calls the work item query', async () => {
- createComponent();
- await waitForPromises();
+ describe('when project context', () => {
+ it('calls the project work item query', async () => {
+ createComponent();
+ await waitForPromises();
- expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
- });
+ expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
+ });
- it('skips the work item query when there is no workItemIid', async () => {
- createComponent({ workItemIid: null });
- await waitForPromises();
+ it('skips calling the group work item query', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(groupSuccessHandler).not.toHaveBeenCalled();
+ });
- expect(successHandler).not.toHaveBeenCalled();
+ it('skips calling the project work item query when there is no workItemIid', async () => {
+ createComponent({ workItemIid: null });
+ await waitForPromises();
+
+ expect(successHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the project work item query when isModal=true', async () => {
+ createComponent({ isModal: true });
+ await waitForPromises();
+
+ expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
+ });
});
- it('calls the work item query when isModal=true', async () => {
- createComponent({ isModal: true });
- await waitForPromises();
+ describe('when group context', () => {
+ it('skips calling the project work item query', async () => {
+ createComponent({ isGroup: true });
+ await waitForPromises();
+
+ expect(successHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the group work item query', async () => {
+ createComponent({ isGroup: true });
+ await waitForPromises();
+
+ expect(groupSuccessHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
+ });
+
+ it('skips calling the group work item query when there is no workItemIid', async () => {
+ createComponent({ isGroup: true, workItemIid: null });
+ await waitForPromises();
- expect(successHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
+ expect(groupSuccessHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the group work item query when isModal=true', async () => {
+ createComponent({ isGroup: true, isModal: true });
+ await waitForPromises();
+
+ expect(groupSuccessHandler).toHaveBeenCalledWith({ fullPath: 'group/project', iid: '1' });
+ });
});
describe('hierarchy widget', () => {
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 4a20e654060..28aa7ffa1be 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -7,10 +7,12 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import labelSearchQuery from '~/sidebar/components/labels/labels_select_widget/graphql/project_labels.query.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
import { i18n, I18N_WORK_ITEM_ERROR_FETCHING_LABELS } from '~/work_items/constants';
import {
+ groupWorkItemByIidResponseFactory,
projectLabelsResponse,
mockLabels,
workItemByIidResponseFactory,
@@ -32,6 +34,9 @@ describe('WorkItemLabels component', () => {
const workItemQuerySuccess = jest
.fn()
.mockResolvedValue(workItemByIidResponseFactory({ labels: null }));
+ const groupWorkItemQuerySuccess = jest
+ .fn()
+ .mockResolvedValue(groupWorkItemByIidResponseFactory({ labels: null }));
const successSearchQueryHandler = jest.fn().mockResolvedValue(projectLabelsResponse);
const successUpdateWorkItemMutationHandler = jest
.fn()
@@ -40,6 +45,7 @@ describe('WorkItemLabels component', () => {
const createComponent = ({
canUpdate = true,
+ isGroup = false,
workItemQueryHandler = workItemQuerySuccess,
searchQueryHandler = successSearchQueryHandler,
updateWorkItemMutationHandler = successUpdateWorkItemMutationHandler,
@@ -48,13 +54,15 @@ describe('WorkItemLabels component', () => {
wrapper = mountExtended(WorkItemLabels, {
apolloProvider: createMockApollo([
[workItemByIidQuery, workItemQueryHandler],
+ [groupWorkItemByIidQuery, groupWorkItemQuerySuccess],
[labelSearchQuery, searchQueryHandler],
[updateWorkItemMutation, updateWorkItemMutationHandler],
]),
provide: {
- fullPath: 'test-project-path',
+ isGroup,
},
propsData: {
+ fullPath: 'test-project-path',
workItemId,
workItemIid,
canUpdate,
@@ -244,17 +252,49 @@ describe('WorkItemLabels component', () => {
});
});
- it('calls the work item query', async () => {
- createComponent();
- await waitForPromises();
+ describe('when project context', () => {
+ it('calls the project work item query', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(workItemQuerySuccess).toHaveBeenCalled();
+ });
+
+ it('skips calling the group work item query', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(groupWorkItemQuerySuccess).not.toHaveBeenCalled();
+ });
- expect(workItemQuerySuccess).toHaveBeenCalled();
+ it('skips calling the project work item query when missing workItemIid', async () => {
+ createComponent({ workItemIid: '' });
+ await waitForPromises();
+
+ expect(workItemQuerySuccess).not.toHaveBeenCalled();
+ });
});
- it('skips calling the work item query when missing workItemIid', async () => {
- createComponent({ workItemIid: '' });
- await waitForPromises();
+ describe('when group context', () => {
+ it('skips calling the project work item query', async () => {
+ createComponent({ isGroup: true });
+ await waitForPromises();
+
+ expect(workItemQuerySuccess).not.toHaveBeenCalled();
+ });
- expect(workItemQuerySuccess).not.toHaveBeenCalled();
+ it('calls the group work item query', async () => {
+ createComponent({ isGroup: true });
+ await waitForPromises();
+
+ expect(groupWorkItemQuerySuccess).toHaveBeenCalled();
+ });
+
+ it('skips calling the group work item query when missing workItemIid', async () => {
+ createComponent({ isGroup: true, workItemIid: '' });
+ await waitForPromises();
+
+ expect(groupWorkItemQuerySuccess).not.toHaveBeenCalled();
+ });
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
index cd077fbf705..0147b199040 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_children_wrapper_spec.js
@@ -53,9 +53,10 @@ describe('WorkItemChildrenWrapper', () => {
wrapper = shallowMountExtended(WorkItemChildrenWrapper, {
apolloProvider: mockApollo,
provide: {
- fullPath: 'test/project',
+ isGroup: false,
},
propsData: {
+ fullPath: 'test/project',
workItemType,
workItemId: 'gid://gitlab/WorkItem/515',
workItemIid: '1',
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
index a624bbe8567..9addf6c3450 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_spec.js
@@ -62,9 +62,6 @@ describe('WorkItemLinkChild', () => {
[getWorkItemTreeQuery, getWorkItemTreeQueryHandler],
[updateWorkItemMutation, mutationChangeParentHandler],
]),
- provide: {
- fullPath: 'gitlab-org/gitlab-test',
- },
propsData: {
canUpdate,
issuableGid,
@@ -93,23 +90,7 @@ describe('WorkItemLinkChild', () => {
expect(findWorkItemLinkChildContents().props()).toEqual({
childItem: workItemObjectiveWithChild,
canUpdate: true,
- childPath: '/gitlab-org/gitlab-test/-/work_items/12',
- });
- });
-
- describe('with relative instance', () => {
- beforeEach(() => {
- window.gon = { relative_url_root: '/test' };
- createComponent({
- childItem: workItemObjectiveWithChild,
- workItemType: WORK_ITEM_TYPE_VALUE_OBJECTIVE,
- });
- });
-
- it('adds the relative url to child path value', () => {
- expect(findWorkItemLinkChildContents().props('childPath')).toBe(
- '/test/gitlab-org/gitlab-test/-/work_items/12',
- );
+ showTaskIcon: false,
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
index aaab22fd18d..0a9da17d284 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js
@@ -54,6 +54,7 @@ describe('WorkItemLinksForm', () => {
[createWorkItemMutation, createMutationResolver],
]),
propsData: {
+ fullPath: 'project/path',
issuableGid: 'gid://gitlab/WorkItem/1',
parentConfidential,
parentIteration,
@@ -62,8 +63,8 @@ describe('WorkItemLinksForm', () => {
formType,
},
provide: {
- fullPath: 'project/path',
hasIterationsFeature,
+ isGroup: false,
},
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index e24cfe27616..0b88b3ff5b4 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -13,9 +13,11 @@ import WorkItemChildrenWrapper from '~/work_items/components/work_item_links/wor
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import { FORM_TYPES } from '~/work_items/constants';
+import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
getIssueDetailsResponse,
+ groupWorkItemByIidResponseFactory,
workItemHierarchyResponse,
workItemHierarchyEmptyResponse,
workItemHierarchyNoUpdatePermissionResponse,
@@ -32,6 +34,9 @@ describe('WorkItemLinks', () => {
let mockApollo;
const responseWithAddChildPermission = jest.fn().mockResolvedValue(workItemHierarchyResponse);
+ const groupResponseWithAddChildPermission = jest
+ .fn()
+ .mockResolvedValue(groupWorkItemByIidResponseFactory());
const responseWithoutAddChildPermission = jest
.fn()
.mockResolvedValue(workItemByIidResponseFactory({ adminParentLink: false }));
@@ -40,20 +45,22 @@ describe('WorkItemLinks', () => {
fetchHandler = responseWithAddChildPermission,
issueDetailsQueryHandler = jest.fn().mockResolvedValue(getIssueDetailsResponse()),
hasIterationsFeature = false,
+ isGroup = false,
} = {}) => {
mockApollo = createMockApollo(
[
[workItemByIidQuery, fetchHandler],
+ [groupWorkItemByIidQuery, groupResponseWithAddChildPermission],
[issueDetailsQuery, issueDetailsQueryHandler],
],
resolvers,
- { addTypename: true },
);
wrapper = shallowMountExtended(WorkItemLinks, {
provide: {
fullPath: 'project/path',
hasIterationsFeature,
+ isGroup,
reportAbusePath: '/report/abuse/path',
},
propsData: {
@@ -243,4 +250,32 @@ describe('WorkItemLinks', () => {
expect(findAbuseCategorySelector().exists()).toBe(false);
});
});
+
+ describe('when project context', () => {
+ it('calls the project work item query', () => {
+ createComponent();
+
+ expect(responseWithAddChildPermission).toHaveBeenCalled();
+ });
+
+ it('skips calling the group work item query', () => {
+ createComponent();
+
+ expect(groupResponseWithAddChildPermission).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when group context', () => {
+ it('skips calling the project work item query', () => {
+ createComponent({ isGroup: true });
+
+ expect(responseWithAddChildPermission).not.toHaveBeenCalled();
+ });
+
+ it('calls the group work item query', () => {
+ createComponent({ isGroup: true });
+
+ expect(groupResponseWithAddChildPermission).toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
index 01fa4591cde..f30fded0b45 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js
@@ -29,10 +29,8 @@ describe('WorkItemTree', () => {
canUpdate = true,
} = {}) => {
wrapper = shallowMountExtended(WorkItemTree, {
- provide: {
- fullPath: 'test/project',
- },
propsData: {
+ fullPath: 'test/project',
workItemType,
parentWorkItemType,
workItemId: 'gid://gitlab/WorkItem/515',
diff --git a/spec/frontend/work_items/components/work_item_milestone_spec.js b/spec/frontend/work_items/components/work_item_milestone_spec.js
index c42c9a573e5..e303ad4b481 100644
--- a/spec/frontend/work_items/components/work_item_milestone_spec.js
+++ b/spec/frontend/work_items/components/work_item_milestone_spec.js
@@ -66,10 +66,8 @@ describe('WorkItemMilestone component', () => {
[projectMilestonesQuery, searchQueryHandler],
[updateWorkItemMutation, mutationHandler],
]),
- provide: {
- fullPath: 'full-path',
- },
propsData: {
+ fullPath: 'full-path',
canUpdate,
workItemMilestone: milestone,
workItemId,
diff --git a/spec/frontend/work_items/components/work_item_notes_spec.js b/spec/frontend/work_items/components/work_item_notes_spec.js
index 35f01c85ec8..9e02e0708d4 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -98,10 +98,8 @@ describe('WorkItemNotes component', () => {
[workItemNoteUpdatedSubscription, notesUpdateSubscriptionHandler],
[workItemNoteDeletedSubscription, notesDeleteSubscriptionHandler],
]),
- provide: {
- fullPath: 'test-path',
- },
propsData: {
+ fullPath: 'test-path',
workItemId,
workItemIid,
workItemType: 'task',
diff --git a/spec/frontend/work_items/components/work_item_parent_spec.js b/spec/frontend/work_items/components/work_item_parent_spec.js
new file mode 100644
index 00000000000..a72eeabc43c
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_parent_spec.js
@@ -0,0 +1,236 @@
+import * as Sentry from '@sentry/browser';
+import { GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui';
+
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import WorkItemParent from '~/work_items/components/work_item_parent.vue';
+import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
+import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
+import { WORK_ITEM_TYPE_ENUM_OBJECTIVE } from '~/work_items/constants';
+
+import {
+ availableObjectivesResponse,
+ mockParentWidgetResponse,
+ updateWorkItemMutationResponseFactory,
+ searchedObjectiveResponse,
+ updateWorkItemMutationErrorResponse,
+} from '../mock_data';
+
+jest.mock('@sentry/browser');
+
+describe('WorkItemParent component', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+
+ const workItemId = 'gid://gitlab/WorkItem/1';
+ const workItemType = 'Objective';
+
+ const availableWorkItemsSuccessHandler = jest.fn().mockResolvedValue(availableObjectivesResponse);
+ const availableWorkItemsFailureHandler = jest.fn().mockRejectedValue(new Error());
+
+ const successUpdateWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponseFactory({ parent: mockParentWidgetResponse }));
+
+ const createComponent = ({
+ canUpdate = true,
+ parent = null,
+ searchQueryHandler = availableWorkItemsSuccessHandler,
+ mutationHandler = successUpdateWorkItemMutationHandler,
+ } = {}) => {
+ wrapper = shallowMountExtended(WorkItemParent, {
+ apolloProvider: createMockApollo([
+ [projectWorkItemsQuery, searchQueryHandler],
+ [updateWorkItemMutation, mutationHandler],
+ ]),
+ provide: {
+ fullPath: 'full-path',
+ },
+ propsData: {
+ canUpdate,
+ parent,
+ workItemId,
+ workItemType,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findInputGroup = () => wrapper.findComponent(GlFormGroup);
+ const findParentText = () => wrapper.findByTestId('disabled-text');
+ const findCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+
+ describe('template', () => {
+ it('shows field label as Parent', () => {
+ expect(findInputGroup().exists()).toBe(true);
+ expect(findInputGroup().attributes('label')).toBe('Parent');
+ });
+
+ it('renders the collapsible listbox with required props', () => {
+ expect(findCollapsibleListbox().exists()).toBe(true);
+ expect(findCollapsibleListbox().props()).toMatchObject({
+ items: [],
+ headerText: 'Assign parent',
+ category: 'tertiary',
+ loading: false,
+ noCaret: true,
+ isCheckCentered: true,
+ searchable: true,
+ searching: false,
+ infiniteScroll: false,
+ noResultsText: 'No matching results',
+ toggleText: 'None',
+ searchPlaceholder: 'Search',
+ resetButtonLabel: 'Unassign',
+ block: true,
+ });
+ });
+
+ it('displays parent text instead of listbox if canUpdate is false', () => {
+ createComponent({ canUpdate: false, parent: mockParentWidgetResponse });
+
+ expect(findCollapsibleListbox().exists()).toBe(false);
+ expect(findParentText().exists()).toBe(true);
+ expect(findParentText().text()).toBe('Objective 101');
+ });
+
+ it('shows loading while searching', async () => {
+ await findCollapsibleListbox().vm.$emit('shown');
+ expect(findCollapsibleListbox().props('searching')).toBe(true);
+ expect(findCollapsibleListbox().props('no-caret')).toBeUndefined();
+ });
+ });
+
+ describe('work items query', () => {
+ it('loads work items in the listbox', async () => {
+ await findCollapsibleListbox().vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(findCollapsibleListbox().props('searching')).toBe(false);
+ expect(findCollapsibleListbox().props('items')).toStrictEqual([
+ { text: 'Objective 101', value: 'gid://gitlab/WorkItem/716' },
+ { text: 'Objective 103', value: 'gid://gitlab/WorkItem/712' },
+ { text: 'Objective 102', value: 'gid://gitlab/WorkItem/711' },
+ ]);
+ expect(availableWorkItemsSuccessHandler).toHaveBeenCalled();
+ });
+
+ it('emits error when the query fails', async () => {
+ createComponent({ searchQueryHandler: availableWorkItemsFailureHandler });
+
+ await findCollapsibleListbox().vm.$emit('shown');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while fetching items. Please try again.'],
+ ]);
+ });
+
+ it('searches item when input data is entered', async () => {
+ const searchedItemQueryHandler = jest.fn().mockResolvedValue(searchedObjectiveResponse);
+ createComponent({
+ searchQueryHandler: searchedItemQueryHandler,
+ });
+
+ await findCollapsibleListbox().vm.$emit('shown');
+ await findCollapsibleListbox().vm.$emit('search', 'Objective 101');
+
+ await waitForPromises();
+
+ expect(searchedItemQueryHandler).toHaveBeenCalledWith({
+ fullPath: 'full-path',
+ searchTerm: 'Objective 101',
+ types: [WORK_ITEM_TYPE_ENUM_OBJECTIVE],
+ in: 'TITLE',
+ });
+
+ await nextTick();
+
+ expect(findCollapsibleListbox().props('items')).toStrictEqual([
+ { text: 'Objective 101', value: 'gid://gitlab/WorkItem/716' },
+ ]);
+ });
+ });
+
+ describe('listbox', () => {
+ const selectWorkItem = async (workItem) => {
+ await findCollapsibleListbox().vm.$emit('shown');
+ await findCollapsibleListbox().vm.$emit('select', workItem);
+ };
+
+ it('calls mutation when item is selected', async () => {
+ selectWorkItem('gid://gitlab/WorkItem/716');
+
+ await waitForPromises();
+
+ expect(successUpdateWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: 'gid://gitlab/WorkItem/1',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/716',
+ },
+ },
+ });
+ });
+
+ it('calls mutation when item is unassigned', async () => {
+ const unAssignParentWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponseFactory({ parent: null }));
+ createComponent({
+ mutationHandler: unAssignParentWorkItemMutationHandler,
+ });
+
+ await findCollapsibleListbox().vm.$emit('reset');
+
+ await waitForPromises();
+
+ expect(unAssignParentWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: 'gid://gitlab/WorkItem/1',
+ hierarchyWidget: {
+ parentId: null,
+ },
+ },
+ });
+ });
+
+ it('emits error when mutation fails', async () => {
+ createComponent({
+ mutationHandler: jest.fn().mockResolvedValue(updateWorkItemMutationErrorResponse),
+ });
+
+ selectWorkItem('gid://gitlab/WorkItem/716');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([['Error!']]);
+ });
+
+ it('emits error and captures exception in sentry when network request fails', async () => {
+ const error = new Error('error');
+ createComponent({
+ mutationHandler: jest.fn().mockRejectedValue(error),
+ });
+
+ selectWorkItem('gid://gitlab/WorkItem/716');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong while updating the objective. Please try again.'],
+ ]);
+ expect(Sentry.captureException).toHaveBeenCalledWith(error);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap b/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap
index 9105e4de5e0..bbc19a011a5 100644
--- a/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap
+++ b/spec/frontend/work_items/components/work_item_relationships/__snapshots__/work_item_relationship_list_spec.js.snap
@@ -1,7 +1,9 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`WorkItemRelationshipList renders linked item list 1`] = `
-<div>
+<div
+ data-testid="work-item-linked-items-list"
+>
<h4
class="gl-font-sm gl-font-weight-semibold gl-mb-2 gl-mt-3 gl-mx-2 gl-text-gray-700"
data-testid="work-items-list-heading"
@@ -20,7 +22,7 @@ exports[`WorkItemRelationshipList renders linked item list 1`] = `
<work-item-link-child-contents-stub
canupdate="true"
childitem="[object Object]"
- childpath="/test-project-path/-/work_items/83"
+ showtaskicon="true"
/>
</li>
</ul>
diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js
new file mode 100644
index 00000000000..d7b3ced2ff9
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_relationships/work_item_add_relationship_form_spec.js
@@ -0,0 +1,156 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlForm, GlFormRadioGroup, GlAlert } from '@gitlab/ui';
+
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import WorkItemAddRelationshipForm from '~/work_items/components/work_item_relationships/work_item_add_relationship_form.vue';
+import WorkItemTokenInput from '~/work_items/components/shared/work_item_token_input.vue';
+import addLinkedItemsMutation from '~/work_items/graphql/add_linked_items.mutation.graphql';
+import { LINKED_ITEM_TYPE_VALUE, MAX_WORK_ITEMS } from '~/work_items/constants';
+
+import { linkedWorkItemResponse, generateWorkItemsListWithId } from '../../mock_data';
+
+describe('WorkItemAddRelationshipForm', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+ const linkedWorkItemsSuccessMutationHandler = jest
+ .fn()
+ .mockResolvedValue(linkedWorkItemResponse());
+
+ const createComponent = async ({
+ workItemId = 'gid://gitlab/WorkItem/1',
+ workItemIid = '1',
+ workItemType = 'Objective',
+ childrenIds = [],
+ linkedWorkItemsMutationHandler = linkedWorkItemsSuccessMutationHandler,
+ } = {}) => {
+ const mockApolloProvider = createMockApollo([
+ [addLinkedItemsMutation, linkedWorkItemsMutationHandler],
+ ]);
+
+ wrapper = shallowMountExtended(WorkItemAddRelationshipForm, {
+ apolloProvider: mockApolloProvider,
+ propsData: {
+ workItemId,
+ workItemIid,
+ workItemFullPath: 'test-project-path',
+ workItemType,
+ childrenIds,
+ },
+ });
+
+ await waitForPromises();
+ };
+
+ const findLinkWorkItemForm = () => wrapper.findComponent(GlForm);
+ const findLinkWorkItemButton = () => wrapper.findByTestId('link-work-item-button');
+ const findMaxWorkItemNote = () => wrapper.findByTestId('max-work-item-note');
+ const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
+ const findWorkItemTokenInput = () => wrapper.findComponent(WorkItemTokenInput);
+ const findGlAlert = () => wrapper.findComponent(GlAlert);
+
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ it('renders link work item form with default values', () => {
+ expect(findLinkWorkItemForm().exists()).toBe(true);
+ expect(findRadioGroup().props('options')).toEqual([
+ { text: 'relates to', value: LINKED_ITEM_TYPE_VALUE.RELATED },
+ { text: 'blocks', value: LINKED_ITEM_TYPE_VALUE.BLOCKS },
+ { text: 'is blocked by', value: LINKED_ITEM_TYPE_VALUE.BLOCKED_BY },
+ ]);
+ expect(findLinkWorkItemButton().attributes('disabled')).toBe('true');
+ expect(findMaxWorkItemNote().text()).toBe('Add a maximum of 10 items at a time.');
+ });
+
+ it('renders work item token input with default props', () => {
+ expect(findWorkItemTokenInput().props()).toMatchObject({
+ value: [],
+ fullPath: 'test-project-path',
+ childrenIds: [],
+ parentWorkItemId: 'gid://gitlab/WorkItem/1',
+ areWorkItemsToAddValid: true,
+ });
+ });
+
+ describe('linking a work item', () => {
+ const selectWorkItemTokens = (workItems) => {
+ findWorkItemTokenInput().vm.$emit('input', workItems);
+ };
+
+ it('enables add button when work item is selected', async () => {
+ await selectWorkItemTokens([
+ {
+ id: 'gid://gitlab/WorkItem/644',
+ },
+ ]);
+ expect(findLinkWorkItemButton().attributes('disabled')).toBeUndefined();
+ });
+
+ it('disables button when more than 10 work items are selected', async () => {
+ await selectWorkItemTokens(generateWorkItemsListWithId(MAX_WORK_ITEMS + 1));
+
+ expect(findWorkItemTokenInput().props('areWorkItemsToAddValid')).toBe(false);
+ expect(findLinkWorkItemButton().attributes('disabled')).toBe('true');
+ });
+
+ it.each`
+ assertionName | linkTypeInput
+ ${'related'} | ${LINKED_ITEM_TYPE_VALUE.RELATED}
+ ${'blocking'} | ${LINKED_ITEM_TYPE_VALUE.BLOCKED_BY}
+ `('selects and links $assertionName work item', async ({ linkTypeInput }) => {
+ findRadioGroup().vm.$emit('input', linkTypeInput);
+ await selectWorkItemTokens([
+ {
+ id: 'gid://gitlab/WorkItem/641',
+ },
+ {
+ id: 'gid://gitlab/WorkItem/642',
+ },
+ ]);
+
+ expect(findWorkItemTokenInput().props('areWorkItemsToAddValid')).toBe(true);
+
+ findLinkWorkItemForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn(),
+ });
+ await waitForPromises();
+
+ expect(linkedWorkItemsSuccessMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: 'gid://gitlab/WorkItem/1',
+ linkType: linkTypeInput,
+ workItemsIds: ['gid://gitlab/WorkItem/641', 'gid://gitlab/WorkItem/642'],
+ },
+ });
+ });
+
+ it.each`
+ errorType | mutationMock | errorMessage
+ ${'an error in the mutation response'} | ${jest.fn().mockResolvedValue(linkedWorkItemResponse({}, ['Linked Item failed']))} | ${'Linked Item failed'}
+ ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('Network Error'))} | ${'Something went wrong when trying to link a item. Please try again.'}
+ `('shows an error message when there is $errorType', async ({ mutationMock, errorMessage }) => {
+ createComponent({ linkedWorkItemsMutationHandler: mutationMock });
+ await selectWorkItemTokens([
+ {
+ id: 'gid://gitlab/WorkItem/641',
+ },
+ ]);
+
+ findLinkWorkItemForm().vm.$emit('submit', {
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn(),
+ });
+ await waitForPromises();
+
+ expect(findGlAlert().exists()).toBe(true);
+ expect(findGlAlert().text()).toBe(errorMessage);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js
index 759ab7e14da..e26bea46ab1 100644
--- a/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js
+++ b/spec/frontend/work_items/components/work_item_relationships/work_item_relationship_list_spec.js
@@ -14,7 +14,6 @@ describe('WorkItemRelationshipList', () => {
linkedItems,
heading,
canUpdate,
- workItemFullPath: 'test-project-path',
},
});
};
@@ -35,7 +34,7 @@ describe('WorkItemRelationshipList', () => {
expect(findWorkItemLinkChildContents().props()).toMatchObject({
childItem: mockLinkedItems[0].workItem,
canUpdate: true,
- childPath: '/test-project-path/-/work_items/83',
+ showTaskIcon: true,
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js
index c9a2499b127..7178fa1aae7 100644
--- a/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js
+++ b/spec/frontend/work_items/components/work_item_relationships/work_item_relationships_spec.js
@@ -9,12 +9,17 @@ import waitForPromises from 'helpers/wait_for_promises';
import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemRelationships from '~/work_items/components/work_item_relationships/work_item_relationships.vue';
import WorkItemRelationshipList from '~/work_items/components/work_item_relationships/work_item_relationship_list.vue';
+import WorkItemAddRelationshipForm from '~/work_items/components/work_item_relationships/work_item_add_relationship_form.vue';
+import groupWorkItemByIidQuery from '~/work_items/graphql/group_work_item_by_iid.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import removeLinkedItemsMutation from '~/work_items/graphql/remove_linked_items.mutation.graphql';
import {
+ groupWorkItemByIidResponseFactory,
workItemByIidResponseFactory,
mockLinkedItems,
mockBlockingLinkedItem,
+ removeLinkedWorkItemResponse,
} from '../../mock_data';
describe('WorkItemRelationships', () => {
@@ -24,23 +29,44 @@ describe('WorkItemRelationships', () => {
const emptyLinkedWorkItemsQueryHandler = jest
.fn()
.mockResolvedValue(workItemByIidResponseFactory());
- const linkedWorkItemsQueryHandler = jest
+ const groupWorkItemsQueryHandler = jest
.fn()
- .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems }));
- const blockingLinkedWorkItemQueryHandler = jest
+ .mockResolvedValue(groupWorkItemByIidResponseFactory());
+ const removeLinkedWorkItemSuccessMutationHandler = jest
.fn()
- .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockBlockingLinkedItem }));
+ .mockResolvedValue(removeLinkedWorkItemResponse('Successfully unlinked IDs: 2.'));
+ const removeLinkedWorkItemErrorMutationHandler = jest
+ .fn()
+ .mockResolvedValue(removeLinkedWorkItemResponse(null, ['Linked item removal failed']));
+ const $toast = {
+ show: jest.fn(),
+ };
const createComponent = async ({
workItemQueryHandler = emptyLinkedWorkItemsQueryHandler,
+ workItemType = 'Task',
+ isGroup = false,
+ removeLinkedWorkItemMutationHandler = removeLinkedWorkItemSuccessMutationHandler,
} = {}) => {
- const mockApollo = createMockApollo([[workItemByIidQuery, workItemQueryHandler]]);
+ const mockApollo = createMockApollo([
+ [workItemByIidQuery, workItemQueryHandler],
+ [removeLinkedItemsMutation, removeLinkedWorkItemMutationHandler],
+ [groupWorkItemByIidQuery, groupWorkItemsQueryHandler],
+ ]);
wrapper = shallowMountExtended(WorkItemRelationships, {
apolloProvider: mockApollo,
propsData: {
+ workItemId: 'gid://gitlab/WorkItem/1',
workItemIid: '1',
workItemFullPath: 'test-project-path',
+ workItemType,
+ },
+ provide: {
+ isGroup,
+ },
+ mocks: {
+ $toast,
},
});
@@ -51,8 +77,11 @@ describe('WorkItemRelationships', () => {
const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper);
const findEmptyRelatedMessageContainer = () => wrapper.findByTestId('links-empty');
const findLinkedItemsCountContainer = () => wrapper.findByTestId('linked-items-count');
+ const findLinkedItemsHelpLink = () => wrapper.findByTestId('help-link');
const findAllWorkItemRelationshipListComponents = () =>
wrapper.findAllComponents(WorkItemRelationshipList);
+ const findAddButton = () => wrapper.findByTestId('link-item-add-button');
+ const findWorkItemRelationshipForm = () => wrapper.findComponent(WorkItemAddRelationshipForm);
it('shows loading icon when query is not processed', () => {
createComponent();
@@ -60,22 +89,35 @@ describe('WorkItemRelationships', () => {
expect(findLoadingIcon().exists()).toBe(true);
});
- it('renders the component with empty message when there are no items', async () => {
+ it('renders the component with with defaults', async () => {
await createComponent();
expect(wrapper.find('.work-item-relationships').exists()).toBe(true);
expect(findEmptyRelatedMessageContainer().exists()).toBe(true);
+ expect(findAddButton().exists()).toBe(true);
+ expect(findWorkItemRelationshipForm().exists()).toBe(false);
+ expect(findLinkedItemsHelpLink().attributes('href')).toBe(
+ '/help/user/okrs.md#linked-items-in-okrs',
+ );
});
it('renders blocking linked item lists', async () => {
- await createComponent({ workItemQueryHandler: blockingLinkedWorkItemQueryHandler });
+ await createComponent({
+ workItemQueryHandler: jest
+ .fn()
+ .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockBlockingLinkedItem })),
+ });
expect(findAllWorkItemRelationshipListComponents().length).toBe(1);
expect(findLinkedItemsCountContainer().text()).toBe('1');
});
it('renders blocking, blocked by and related to linked item lists with proper count', async () => {
- await createComponent({ workItemQueryHandler: linkedWorkItemsQueryHandler });
+ await createComponent({
+ workItemQueryHandler: jest
+ .fn()
+ .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems })),
+ });
// renders all 3 lists: blocking, blocked by and related to
expect(findAllWorkItemRelationshipListComponents().length).toBe(3);
@@ -90,4 +132,103 @@ describe('WorkItemRelationships', () => {
expect(findWidgetWrapper().props('error')).toBe(errorMessage);
});
+
+ it('does not render add button when there is no permission', async () => {
+ await createComponent({
+ workItemQueryHandler: jest
+ .fn()
+ .mockResolvedValue(workItemByIidResponseFactory({ canAdminWorkItemLink: false })),
+ });
+
+ expect(findAddButton().exists()).toBe(false);
+ });
+
+ it('shows form on add button and hides when cancel button is clicked', async () => {
+ await createComponent();
+
+ await findAddButton().vm.$emit('click');
+ expect(findWorkItemRelationshipForm().exists()).toBe(true);
+
+ await findWorkItemRelationshipForm().vm.$emit('cancel');
+ expect(findWorkItemRelationshipForm().exists()).toBe(false);
+ });
+
+ describe('when project context', () => {
+ it('calls the project work item query', () => {
+ createComponent();
+
+ expect(emptyLinkedWorkItemsQueryHandler).toHaveBeenCalled();
+ });
+
+ it('skips calling the group work item query', () => {
+ createComponent();
+
+ expect(groupWorkItemsQueryHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when group context', () => {
+ it('skips calling the project work item query', () => {
+ createComponent({ isGroup: true });
+
+ expect(emptyLinkedWorkItemsQueryHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the group work item query', () => {
+ createComponent({ isGroup: true });
+
+ expect(groupWorkItemsQueryHandler).toHaveBeenCalled();
+ });
+ });
+
+ it('removes linked item and shows toast message when removeLinkedItem event is emitted', async () => {
+ await createComponent({
+ workItemQueryHandler: jest
+ .fn()
+ .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems })),
+ });
+
+ expect(findLinkedItemsCountContainer().text()).toBe('3');
+
+ await findAllWorkItemRelationshipListComponents()
+ .at(0)
+ .vm.$emit('removeLinkedItem', { id: 'gid://gitlab/WorkItem/2' });
+
+ await waitForPromises();
+
+ expect(removeLinkedWorkItemSuccessMutationHandler).toHaveBeenCalledWith({
+ input: {
+ id: 'gid://gitlab/WorkItem/1',
+ workItemsIds: ['gid://gitlab/WorkItem/2'],
+ },
+ });
+
+ expect($toast.show).toHaveBeenCalledWith('Linked item removed');
+
+ expect(findLinkedItemsCountContainer().text()).toBe('2');
+ });
+
+ it.each`
+ errorType | mutationMock | errorMessage
+ ${'an error in the mutation response'} | ${removeLinkedWorkItemErrorMutationHandler} | ${'Linked item removal failed'}
+ ${'a network error'} | ${jest.fn().mockRejectedValue(new Error('Network Error'))} | ${'Something went wrong when removing item. Please refresh this page.'}
+ `(
+ 'shows an error message when there is $errorType while removing items',
+ async ({ mutationMock, errorMessage }) => {
+ await createComponent({
+ workItemQueryHandler: jest
+ .fn()
+ .mockResolvedValue(workItemByIidResponseFactory({ linkedItems: mockLinkedItems })),
+ removeLinkedWorkItemMutationHandler: mutationMock,
+ });
+
+ await findAllWorkItemRelationshipListComponents()
+ .at(0)
+ .vm.$emit('removeLinkedItem', { id: 'gid://gitlab/WorkItem/2' });
+
+ await waitForPromises();
+
+ expect(findWidgetWrapper().props('error')).toBe(errorMessage);
+ },
+ );
});
diff --git a/spec/frontend/work_items/components/work_item_todos_spec.js b/spec/frontend/work_items/components/work_item_todos_spec.js
index 454bd97bbee..c76cdbcee53 100644
--- a/spec/frontend/work_items/components/work_item_todos_spec.js
+++ b/spec/frontend/work_items/components/work_item_todos_spec.js
@@ -86,6 +86,9 @@ describe('WorkItemTodo component', () => {
workItemFullpath: mockWorkItemFullpath,
currentUserTodos,
},
+ provide: {
+ isGroup: false,
+ },
});
};
diff --git a/spec/frontend/work_items/graphql/cache_utils_spec.js b/spec/frontend/work_items/graphql/cache_utils_spec.js
index 6d0083790d1..64ef1bdbb88 100644
--- a/spec/frontend/work_items/graphql/cache_utils_spec.js
+++ b/spec/frontend/work_items/graphql/cache_utils_spec.js
@@ -43,7 +43,7 @@ describe('work items graphql cache utils', () => {
title: 'New child',
};
- addHierarchyChild(mockCache, fullPath, iid, child);
+ addHierarchyChild({ cache: mockCache, fullPath, iid, workItem: child });
expect(mockCache.writeQuery).toHaveBeenCalledWith({
query: workItemByIidQuery,
@@ -88,7 +88,7 @@ describe('work items graphql cache utils', () => {
title: 'New child',
};
- addHierarchyChild(mockCache, fullPath, iid, child);
+ addHierarchyChild({ cache: mockCache, fullPath, iid, workItem: child });
expect(mockCache.writeQuery).not.toHaveBeenCalled();
});
@@ -106,7 +106,7 @@ describe('work items graphql cache utils', () => {
title: 'Child',
};
- removeHierarchyChild(mockCache, fullPath, iid, childToRemove);
+ removeHierarchyChild({ cache: mockCache, fullPath, iid, workItem: childToRemove });
expect(mockCache.writeQuery).toHaveBeenCalledWith({
query: workItemByIidQuery,
@@ -145,7 +145,7 @@ describe('work items graphql cache utils', () => {
title: 'Child',
};
- removeHierarchyChild(mockCache, fullPath, iid, childToRemove);
+ removeHierarchyChild({ cache: mockCache, fullPath, iid, workItem: childToRemove });
expect(mockCache.writeQuery).not.toHaveBeenCalled();
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index ba244b19eb5..9eb604c81cb 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -146,6 +146,7 @@ export const workItemQueryResponse = {
setWorkItemMetadata: false,
adminParentLink: false,
createNote: false,
+ adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
widgets: [
@@ -193,6 +194,7 @@ export const workItemQueryResponse = {
confidential: false,
title: '123',
state: 'OPEN',
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/4',
workItemType: {
id: '1',
name: 'Task',
@@ -251,6 +253,7 @@ export const updateWorkItemMutationResponse = {
setWorkItemMetadata: false,
adminParentLink: false,
createNote: false,
+ adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
reference: 'test-project-path#1',
@@ -269,6 +272,7 @@ export const updateWorkItemMutationResponse = {
confidential: false,
title: '123',
state: 'OPEN',
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/4',
workItemType: {
id: '1',
name: 'Task',
@@ -360,6 +364,7 @@ export const convertWorkItemMutationResponse = {
setWorkItemMetadata: false,
adminParentLink: false,
createNote: false,
+ adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
reference: 'gitlab-org/gitlab-test#1',
@@ -378,6 +383,7 @@ export const convertWorkItemMutationResponse = {
confidential: false,
title: '123',
state: 'OPEN',
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/4',
workItemType: {
id: '1',
name: 'Task',
@@ -486,6 +492,7 @@ export const mockBlockingLinkedItem = {
state: 'OPEN',
createdAt: '2023-03-28T10:50:16Z',
closedAt: null,
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/83',
widgets: [],
__typename: 'WorkItem',
},
@@ -518,6 +525,7 @@ export const mockLinkedItems = {
state: 'OPEN',
createdAt: '2023-03-28T10:50:16Z',
closedAt: null,
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/83',
widgets: [],
__typename: 'WorkItem',
},
@@ -540,6 +548,7 @@ export const mockLinkedItems = {
state: 'OPEN',
createdAt: '2023-03-28T10:50:16Z',
closedAt: null,
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/55',
widgets: [],
__typename: 'WorkItem',
},
@@ -562,6 +571,7 @@ export const mockLinkedItems = {
state: 'OPEN',
createdAt: '2023-03-28T10:50:16Z',
closedAt: null,
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/56',
widgets: [],
__typename: 'WorkItem',
},
@@ -579,6 +589,7 @@ export const workItemResponseFactory = ({
canDelete = false,
canCreateNote = false,
adminParentLink = false,
+ canAdminWorkItemLink = true,
notificationsWidgetPresent = true,
currentUserTodosWidgetPresent = true,
awardEmojiWidgetPresent = true,
@@ -636,6 +647,7 @@ export const workItemResponseFactory = ({
updateWorkItem: canUpdate,
setWorkItemMetadata: canUpdate,
adminParentLink,
+ adminWorkItemLink: canAdminWorkItemLink,
createNote: canCreateNote,
__typename: 'WorkItemPermissions',
},
@@ -756,6 +768,7 @@ export const workItemResponseFactory = ({
confidential: false,
title: '123',
state: 'OPEN',
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/5',
workItemType: {
id: '1',
name: 'Task',
@@ -828,13 +841,16 @@ export const workItemByIidResponseFactory = (options) => {
};
};
-export const updateWorkItemMutationResponseFactory = (options) => {
+export const groupWorkItemByIidResponseFactory = (options) => {
const response = workItemResponseFactory(options);
return {
data: {
- workItemUpdate: {
- workItem: response.data.workItem,
- errors: [],
+ workspace: {
+ __typename: 'Group',
+ id: 'gid://gitlab/Group/1',
+ workItems: {
+ nodes: [response.data.workItem],
+ },
},
},
};
@@ -914,6 +930,7 @@ export const createWorkItemMutationResponse = {
setWorkItemMetadata: false,
adminParentLink: false,
createNote: false,
+ adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
reference: 'test-project-path#1',
@@ -996,6 +1013,7 @@ export const workItemHierarchyEmptyResponse = {
setWorkItemMetadata: false,
adminParentLink: false,
createNote: false,
+ adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
confidential: false,
@@ -1046,6 +1064,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
setWorkItemMetadata: false,
adminParentLink: false,
createNote: false,
+ adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
project: {
@@ -1077,6 +1096,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/2',
widgets: [
{
type: 'HIERARCHY',
@@ -1110,6 +1130,7 @@ export const workItemTask = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/4',
widgets: [],
__typename: 'WorkItem',
};
@@ -1128,6 +1149,7 @@ export const confidentialWorkItemTask = {
confidential: true,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/2',
widgets: [],
__typename: 'WorkItem',
};
@@ -1146,6 +1168,7 @@ export const closedWorkItemTask = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: '2022-08-12T13:07:52Z',
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/3',
widgets: [],
__typename: 'WorkItem',
};
@@ -1168,6 +1191,7 @@ export const childrenWorkItems = [
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/5',
widgets: [],
__typename: 'WorkItem',
},
@@ -1196,6 +1220,7 @@ export const workItemHierarchyResponse = {
setWorkItemMetadata: true,
adminParentLink: true,
createNote: true,
+ adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
author: {
@@ -1297,6 +1322,7 @@ export const workItemObjectiveWithChild = {
setWorkItemMetadata: true,
adminParentLink: true,
createNote: true,
+ adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
author: {
@@ -1368,6 +1394,7 @@ export const workItemHierarchyTreeResponse = {
setWorkItemMetadata: true,
adminParentLink: true,
createNote: true,
+ adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
confidential: false,
@@ -1403,6 +1430,7 @@ export const workItemHierarchyTreeResponse = {
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
+ webUrl: '/gitlab-org/gitlab-test/-/work_items/13',
widgets: [
{
type: 'HIERARCHY',
@@ -1449,6 +1477,7 @@ export const changeIndirectWorkItemParentMutationResponse = {
setWorkItemMetadata: true,
adminParentLink: true,
createNote: true,
+ adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
description: null,
@@ -1517,6 +1546,7 @@ export const changeWorkItemParentMutationResponse = {
setWorkItemMetadata: true,
adminParentLink: true,
createNote: true,
+ adminWorkItemLink: true,
__typename: 'WorkItemPermissions',
},
description: null,
@@ -1568,6 +1598,7 @@ export const availableWorkItemsResponse = {
nodes: [
{
id: 'gid://gitlab/WorkItem/458',
+ iid: '2',
title: 'Task 1',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
@@ -1576,6 +1607,7 @@ export const availableWorkItemsResponse = {
},
{
id: 'gid://gitlab/WorkItem/459',
+ iid: '3',
title: 'Task 2',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
@@ -1584,6 +1616,7 @@ export const availableWorkItemsResponse = {
},
{
id: 'gid://gitlab/WorkItem/460',
+ iid: '4',
title: 'Task 3',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
@@ -1596,6 +1629,64 @@ export const availableWorkItemsResponse = {
},
};
+export const availableObjectivesResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/2',
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/716',
+ iid: '122',
+ title: 'Objective 101',
+ state: 'OPEN',
+ confidential: false,
+ __typename: 'WorkItem',
+ },
+ {
+ id: 'gid://gitlab/WorkItem/712',
+ iid: '118',
+ title: 'Objective 103',
+ state: 'OPEN',
+ confidential: false,
+ __typename: 'WorkItem',
+ },
+ {
+ id: 'gid://gitlab/WorkItem/711',
+ iid: '117',
+ title: 'Objective 102',
+ state: 'OPEN',
+ confidential: false,
+ __typename: 'WorkItem',
+ },
+ ],
+ },
+ },
+ },
+};
+
+export const searchedObjectiveResponse = {
+ data: {
+ workspace: {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/2',
+ workItems: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WorkItem/716',
+ iid: '122',
+ title: 'Objective 101',
+ state: 'OPEN',
+ confidential: false,
+ __typename: 'WorkItem',
+ },
+ ],
+ },
+ },
+ },
+};
+
export const searchedWorkItemsResponse = {
data: {
workspace: {
@@ -1605,6 +1696,7 @@ export const searchedWorkItemsResponse = {
nodes: [
{
id: 'gid://gitlab/WorkItem/459',
+ iid: '3',
title: 'Task 2',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
@@ -1931,6 +2023,21 @@ export const mockMilestoneWidgetResponse = {
title: 'v4.0',
};
+export const mockParentWidgetResponse = {
+ id: 'gid://gitlab/WorkItem/716',
+ iid: '122',
+ title: 'Objective 101',
+ confidential: false,
+ webUrl: 'http://127.0.0.1:3000/gitlab-org/gitlab-test/-/work_items/122',
+ workItemType: {
+ id: 'gid://gitlab/WorkItems::Type/6',
+ name: 'Objective',
+ iconName: 'issue-type-objective',
+ __typename: 'WorkItemType',
+ },
+ __typename: 'WorkItem',
+};
+
export const projectMilestonesResponse = {
data: {
workspace: {
@@ -3439,6 +3546,31 @@ export const getTodosMutationResponse = (state) => {
};
};
+export const linkedWorkItemResponse = (options, errors = []) => {
+ const response = workItemResponseFactory(options);
+ return {
+ data: {
+ workItemAddLinkedItems: {
+ workItem: response.data.workItem,
+ errors,
+ __typename: 'WorkItemAddLinkedItemsPayload',
+ },
+ },
+ };
+};
+
+export const removeLinkedWorkItemResponse = (message, errors = []) => {
+ return {
+ data: {
+ workItemRemoveLinkedItems: {
+ errors,
+ message,
+ __typename: 'WorkItemRemoveLinkedItemsPayload',
+ },
+ },
+ };
+};
+
export const groupWorkItemsQueryResponse = {
data: {
group: {
@@ -3498,3 +3630,36 @@ export const groupWorkItemsQueryResponse = {
},
},
};
+
+export const updateWorkItemMutationResponseFactory = (options) => {
+ const response = workItemResponseFactory(options);
+ return {
+ data: {
+ workItemUpdate: {
+ workItem: response.data.workItem,
+ errors: [],
+ },
+ },
+ };
+};
+
+export const updateWorkItemNotificationsMutationResponse = (subscribed) => ({
+ data: {
+ workItemSubscribe: {
+ workItem: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetNotifications',
+ type: 'NOTIFICATIONS',
+ subscribed,
+ },
+ ],
+ },
+ errors: [],
+ },
+ },
+});
+
+export const generateWorkItemsListWithId = (count) =>
+ Array.from({ length: count }, (_, i) => ({ id: `gid://gitlab/WorkItem/${i + 1}` }));
diff --git a/spec/frontend/work_items/pages/create_work_item_spec.js b/spec/frontend/work_items/pages/create_work_item_spec.js
index c369a454286..527f5890338 100644
--- a/spec/frontend/work_items/pages/create_work_item_spec.js
+++ b/spec/frontend/work_items/pages/create_work_item_spec.js
@@ -65,6 +65,7 @@ describe('Create work item component', () => {
},
provide: {
fullPath: 'full-path',
+ isGroup: false,
},
});
};
@@ -199,8 +200,6 @@ describe('Create work item component', () => {
wrapper.find('form').trigger('submit');
await waitForPromises();
- expect(findAlert().text()).toBe(
- 'Something went wrong when creating work item. Please try again.',
- );
+ expect(findAlert().text()).toBe('Something went wrong when creating item. Please try again.');
});
});
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index 79ba31e7012..d4efcf78189 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -41,6 +41,7 @@ describe('Work items router', () => {
router,
provide: {
fullPath: 'full-path',
+ isGroup: false,
issuesListPath: 'full-path/-/issues',
hasIssueWeightsFeature: false,
hasIterationsFeature: false,
diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js
index 8a49140119d..aa24b80cf08 100644
--- a/spec/frontend/work_items/utils_spec.js
+++ b/spec/frontend/work_items/utils_spec.js
@@ -1,4 +1,4 @@
-import { autocompleteDataSources, markdownPreviewPath, workItemPath } from '~/work_items/utils';
+import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
describe('autocompleteDataSources', () => {
beforeEach(() => {
@@ -25,14 +25,3 @@ describe('markdownPreviewPath', () => {
);
});
});
-
-describe('workItemPath', () => {
- it('returns corrrect data sources', () => {
- expect(workItemPath('project/group', '2')).toEqual('/project/group/-/work_items/2');
- });
-
- it('returns corrrect data sources with relative url root', () => {
- gon.relative_url_root = '/foobar';
- expect(workItemPath('project/group', '2')).toEqual('/foobar/project/group/-/work_items/2');
- });
-});