summaryrefslogtreecommitdiff
path: root/spec/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__mocks__/@cubejs-client/core.js26
-rw-r--r--spec/frontend/abuse_reports/components/abuse_category_selector_spec.js126
-rw-r--r--spec/frontend/admin/broadcast_messages/components/message_form_spec.js6
-rw-r--r--spec/frontend/admin/users/components/user_date_spec.js2
-rw-r--r--spec/frontend/admin/users/mock_data.js8
-rw-r--r--spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js4
-rw-r--r--spec/frontend/analytics/cycle_analytics/store/actions_spec.js38
-rw-r--r--spec/frontend/api/groups_api_spec.js4
-rw-r--r--spec/frontend/api/harbor_registry_spec.js8
-rw-r--r--spec/frontend/api/packages_api_spec.js4
-rw-r--r--spec/frontend/api/tags_api_spec.js4
-rw-r--r--spec/frontend/api/user_api_spec.js35
-rw-r--r--spec/frontend/api_spec.js157
-rw-r--r--spec/frontend/artifacts/components/artifact_row_spec.js21
-rw-r--r--spec/frontend/artifacts/components/artifacts_table_row_details_spec.js1
-rw-r--r--spec/frontend/artifacts/components/feedback_banner_spec.js63
-rw-r--r--spec/frontend/artifacts/components/job_artifacts_table_spec.js24
-rw-r--r--spec/frontend/autosave_spec.js128
-rw-r--r--spec/frontend/batch_comments/components/submit_dropdown_spec.js2
-rw-r--r--spec/frontend/behaviors/markdown/render_gfm_spec.js9
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js20
-rw-r--r--spec/frontend/boards/board_list_helper.js7
-rw-r--r--spec/frontend/boards/board_list_spec.js2
-rw-r--r--spec/frontend/boards/components/board_app_spec.js3
-rw-r--r--spec/frontend/boards/components/board_card_spec.js8
-rw-r--r--spec/frontend/boards/components/board_column_spec.js1
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js6
-rw-r--r--spec/frontend/boards/components/board_content_spec.js3
-rw-r--r--spec/frontend/boards/components/board_filtered_search_spec.js3
-rw-r--r--spec/frontend/boards/components/board_form_spec.js6
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js2
-rw-r--r--spec/frontend/boards/components/board_new_issue_spec.js11
-rw-r--r--spec/frontend/boards/components/board_top_bar_spec.js1
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js25
-rw-r--r--spec/frontend/boards/components/issue_board_filtered_search_spec.js2
-rw-r--r--spec/frontend/boards/stores/getters_spec.js36
-rw-r--r--spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js10
-rw-r--r--spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js (renamed from spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js)2
-rw-r--r--spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js (renamed from spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js)2
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js (renamed from spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js)4
-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_group_variables_spec.js (renamed from spec/frontend/ci_variable_list/components/ci_group_variables_spec.js)6
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js (renamed from spec/frontend/ci_variable_list/components/ci_project_variables_spec.js)6
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js (renamed from spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js)6
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js (renamed from spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js)10
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js (renamed from spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js)18
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js (renamed from spec/frontend/ci_variable_list/components/ci_variable_table_spec.js)4
-rw-r--r--spec/frontend/ci/ci_variable_list/mocks.js (renamed from spec/frontend/ci_variable_list/mocks.js)32
-rw-r--r--spec/frontend/ci/ci_variable_list/services/mock_data.js (renamed from spec/frontend/ci_variable_list/services/mock_data.js)0
-rw-r--r--spec/frontend/ci/ci_variable_list/stubs.js (renamed from spec/frontend/ci_variable_list/stubs.js)0
-rw-r--r--spec/frontend/ci/ci_variable_list/utils_spec.js (renamed from spec/frontend/ci_variable_list/utils_spec.js)4
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js29
-rw-r--r--spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js4
-rw-r--r--spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js (renamed from spec/frontend/pipeline_new/components/pipeline_new_form_spec.js)28
-rw-r--r--spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js (renamed from spec/frontend/pipeline_new/components/refs_dropdown_spec.js)91
-rw-r--r--spec/frontend/ci/pipeline_new/mock_data.js (renamed from spec/frontend/pipeline_new/mock_data.js)10
-rw-r--r--spec/frontend/ci/pipeline_new/utils/filter_variables_spec.js (renamed from spec/frontend/pipeline_new/utils/filter_variables_spec.js)2
-rw-r--r--spec/frontend/ci/pipeline_new/utils/format_refs_spec.js82
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/pipeline_schedules_spec.js73
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js11
-rw-r--r--spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js12
-rw-r--r--spec/frontend/ci/pipeline_schedules/mock_data.js18
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js12
-rw-r--r--spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js139
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js14
-rw-r--r--spec/frontend/constants_spec.js30
-rw-r--r--spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js31
-rw-r--r--spec/frontend/content_editor/extensions/attachment_spec.js10
-rw-r--r--spec/frontend/content_editor/extensions/link_spec.js2
-rw-r--r--spec/frontend/content_editor/markdown_processing_spec.js16
-rw-r--r--spec/frontend/content_editor/markdown_processing_spec_helper.js92
-rw-r--r--spec/frontend/content_editor/markdown_snapshot_spec.js95
-rw-r--r--spec/frontend/content_editor/markdown_snapshot_spec_helper.js96
-rw-r--r--spec/frontend/content_editor/services/upload_helpers_spec.js4
-rw-r--r--spec/frontend/deploy_freeze/store/mutations_spec.js4
-rw-r--r--spec/frontend/design_management/components/design_notes/design_reply_form_spec.js24
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap14
-rw-r--r--spec/frontend/diff_spec.js72
-rw-r--r--spec/frontend/diffs/components/app_spec.js1
-rw-r--r--spec/frontend/diffs/components/diff_file_spec.js6
-rw-r--r--spec/frontend/diffs/components/diff_line_note_form_spec.js3
-rw-r--r--spec/frontend/dropzone_input_spec.js6
-rw-r--r--spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json2
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml40
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules.yml5
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml20
-rw-r--r--spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml4
-rw-r--r--spec/frontend/environments/environment_details/deployment_job_spec.js49
-rw-r--r--spec/frontend/environments/environment_details/deployment_status_link_spec.js57
-rw-r--r--spec/frontend/environments/environment_details/deployment_triggerer_spec.js51
-rw-r--r--spec/frontend/environments/environment_details/empty_state_spec.js39
-rw-r--r--spec/frontend/environments/environment_details/page_spec.js69
-rw-r--r--spec/frontend/environments/environment_details/pagination_spec.js157
-rw-r--r--spec/frontend/environments/environment_details_page_spec.js50
-rw-r--r--spec/frontend/error_tracking/components/error_tracking_list_spec.js18
-rw-r--r--spec/frontend/error_tracking/store/list/actions_spec.js6
-rw-r--r--spec/frontend/error_tracking_settings/components/project_dropdown_spec.js16
-rw-r--r--spec/frontend/error_tracking_settings/mock.js7
-rw-r--r--spec/frontend/feature_flags/components/environments_dropdown_spec.js10
-rw-r--r--spec/frontend/feature_flags/components/new_environments_dropdown_spec.js4
-rw-r--r--spec/frontend/feature_highlight/feature_highlight_helper_spec.js7
-rw-r--r--spec/frontend/fixtures/environments.rb69
-rw-r--r--spec/frontend/fixtures/issues.rb2
-rw-r--r--spec/frontend/fixtures/projects.rb32
-rw-r--r--spec/frontend/fixtures/runner_instructions.rb2
-rw-r--r--spec/frontend/flash_spec.js204
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js62
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js49
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_spec.js35
-rw-r--r--spec/frontend/frequent_items/store/actions_spec.js87
-rw-r--r--spec/frontend/frequent_items/store/mutations_spec.js35
-rw-r--r--spec/frontend/gfm_auto_complete/mock_data.js24
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js27
-rw-r--r--spec/frontend/group_settings/components/shared_runners_form_spec.js2
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js27
-rw-r--r--spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js18
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js39
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js8
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js4
-rw-r--r--spec/frontend/ide/stores/modules/terminal/messages_spec.js10
-rw-r--r--spec/frontend/import_entities/components/import_status_spec.js19
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js38
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js103
-rw-r--r--spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js12
-rw-r--r--spec/frontend/incidents_settings/components/incidents_settings_service_spec.js8
-rw-r--r--spec/frontend/integrations/edit/components/integration_form_spec.js81
-rw-r--r--spec/frontend/integrations/edit/components/integration_forms/section_spec.js109
-rw-r--r--spec/frontend/integrations/edit/components/trigger_field_spec.js31
-rw-r--r--spec/frontend/integrations/overrides/components/integration_overrides_spec.js10
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js28
-rw-r--r--spec/frontend/invite_members/components/user_limit_notification_spec.js20
-rw-r--r--spec/frontend/issuable/components/issuable_by_email_spec.js6
-rw-r--r--spec/frontend/issuable/components/issuable_header_warnings_spec.js5
-rw-r--r--spec/frontend/issuable/issuable_form_spec.js10
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js173
-rw-r--r--spec/frontend/issues/dashboard/utils_spec.js88
-rw-r--r--spec/frontend/issues/list/mock_data.js19
-rw-r--r--spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js6
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js33
-rw-r--r--spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js9
-rw-r--r--spec/frontend/issues/show/components/incidents/mock_data.js1
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js142
-rw-r--r--spec/frontend/jira_connect/branches/components/project_dropdown_spec.js53
-rw-r--r--spec/frontend/jira_connect/subscriptions/api_spec.js14
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js56
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js21
-rw-r--r--spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap20
-rw-r--r--spec/frontend/jobs/components/job/manual_variables_form_spec.js21
-rw-r--r--spec/frontend/jobs/components/job/sidebar_spec.js4
-rw-r--r--spec/frontend/jobs/components/table/jobs_table_spec.js6
-rw-r--r--spec/frontend/language_switcher/components/app_spec.js10
-rw-r--r--spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js18
-rw-r--r--spec/frontend/lib/utils/datetime/date_format_utility_spec.js22
-rw-r--r--spec/frontend/lib/utils/poll_until_complete_spec.js14
-rw-r--r--spec/frontend/locale/ensure_single_line_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js1
-rw-r--r--spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js2
-rw-r--r--spec/frontend/members/components/action_buttons/leave_button_spec.js59
-rw-r--r--spec/frontend/members/components/action_buttons/remove_member_button_spec.js18
-rw-r--r--spec/frontend/members/components/action_buttons/user_action_buttons_spec.js161
-rw-r--r--spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js54
-rw-r--r--spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js77
-rw-r--r--spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js220
-rw-r--r--spec/frontend/members/components/modals/leave_modal_spec.js122
-rw-r--r--spec/frontend/members/components/modals/remove_member_modal_spec.js44
-rw-r--r--spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap61
-rw-r--r--spec/frontend/members/components/table/created_at_spec.js19
-rw-r--r--spec/frontend/members/components/table/member_action_buttons_spec.js4
-rw-r--r--spec/frontend/members/components/table/member_activity_spec.js40
-rw-r--r--spec/frontend/members/components/table/member_source_spec.js94
-rw-r--r--spec/frontend/members/components/table/members_table_cell_spec.js16
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js45
-rw-r--r--spec/frontend/members/components/table/role_dropdown_spec.js66
-rw-r--r--spec/frontend/members/guest_overage_confirm_action_spec.js7
-rw-r--r--spec/frontend/members/mock_data.js18
-rw-r--r--spec/frontend/members/store/actions_spec.js8
-rw-r--r--spec/frontend/members/utils_spec.js26
-rw-r--r--spec/frontend/merge_request_tabs_spec.js70
-rw-r--r--spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap45
-rw-r--r--spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap511
-rw-r--r--spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js4
-rw-r--r--spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js111
-rw-r--r--spec/frontend/monitoring/requests/index_spec.js20
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js10
-rw-r--r--spec/frontend/monitoring/store/mutations_spec.js7
-rw-r--r--spec/frontend/nav/components/new_nav_toggle_spec.js23
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js9
-rw-r--r--spec/frontend/notes/components/note_body_spec.js12
-rw-r--r--spec/frontend/notes/stores/actions_spec.js6
-rw-r--r--spec/frontend/notifications/components/custom_notifications_modal_spec.js14
-rw-r--r--spec/frontend/notifications/components/notification_email_listbox_input_spec.js81
-rw-r--r--spec/frontend/notifications/components/notifications_dropdown_spec.js10
-rw-r--r--spec/frontend/observability/observability_app_spec.js29
-rw-r--r--spec/frontend/observability/skeleton_spec.js145
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js45
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js16
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js8
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/pypi_installation_spec.js.snap2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js16
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js57
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js2
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js156
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap125
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/list_spec.js103
-rw-r--r--spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js42
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js19
-rw-r--r--spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js42
-rw-r--r--spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap14
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js12
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js12
-rw-r--r--spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js2
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_content_spec.js6
-rw-r--r--spec/frontend/pipeline_new/utils/format_refs_spec.js21
-rw-r--r--spec/frontend/pipeline_wizard/components/wrapper_spec.js7
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js8
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js8
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js115
-rw-r--r--spec/frontend/projects/commit/components/projects_dropdown_spec.js64
-rw-r--r--spec/frontend/projects/merge_requests/components/report_abuse_dropdown_item_spec.js73
-rw-r--r--spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js51
-rw-r--r--spec/frontend/projects/settings/components/default_branch_selector_spec.js1
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js11
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/mock_data.js5
-rw-r--r--spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js8
-rw-r--r--spec/frontend/read_more_spec.js33
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js11
-rw-r--r--spec/frontend/repository/commits_service_spec.js15
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js10
-rw-r--r--spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js40
-rw-r--r--spec/frontend/repository/components/blob_viewers/openapi_viewer_spec.js30
-rw-r--r--spec/frontend/repository/components/fork_info_spec.js122
-rw-r--r--spec/frontend/repository/components/new_directory_modal_spec.js8
-rw-r--r--spec/frontend/repository/components/tree_content_spec.js23
-rw-r--r--spec/frontend/repository/components/upload_blob_modal_spec.js4
-rw-r--r--spec/frontend/repository/mock_data.js18
-rw-r--r--spec/frontend/repository/utils/ref_switcher_utils_spec.js7
-rw-r--r--spec/frontend/search/store/utils_spec.js22
-rw-r--r--spec/frontend/self_monitor/store/actions_spec.js6
-rw-r--r--spec/frontend/set_status_modal/set_status_form_spec.js93
-rw-r--r--spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js49
-rw-r--r--spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js81
-rw-r--r--spec/frontend/set_status_modal/utils_spec.js18
-rw-r--r--spec/frontend/sidebar/components/assignees/assignees_spec.js12
-rw-r--r--spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js2
-rw-r--r--spec/frontend/super_sidebar/components/counter_spec.js56
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js33
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js46
-rw-r--r--spec/frontend/super_sidebar/mock_data.js9
-rw-r--r--spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js150
-rw-r--r--spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js129
-rw-r--r--spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js41
-rw-r--r--spec/frontend/usage_quotas/storage/components/usage_graph_spec.js144
-rw-r--r--spec/frontend/usage_quotas/storage/mock_data.js101
-rw-r--r--spec/frontend/usage_quotas/storage/utils_spec.js88
-rw-r--r--spec/frontend/users/profile/components/report_abuse_button_spec.js79
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js8
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap163
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js8
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js20
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/security_reports/mock_data.js141
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js93
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js28
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js12
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js75
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js37
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js16
-rw-r--r--spec/frontend/vue_shared/components/ci_badge_link_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js36
-rw-r--r--spec/frontend/vue_shared/components/group_select/group_select_spec.js150
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js67
-rw-r--r--spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js58
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js18
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js77
-rw-r--r--spec/frontend/vue_shared/components/markdown/toolbar_spec.js15
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap177
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js70
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_docker_instructions_spec.js.snap3
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_kubernetes_instructions_spec.js.snap3
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js117
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js169
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js28
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js28
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/mock_data.js8
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js201
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js15
-rw-r--r--spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js24
-rw-r--r--spec/frontend/work_items/components/notes/__snapshots__/work_item_note_body_spec.js.snap9
-rw-r--r--spec/frontend/work_items/components/notes/activity_filter_spec.js74
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_body_spec.js32
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js53
-rw-r--r--spec/frontend/work_items/components/work_item_comment_form_spec.js205
-rw-r--r--spec/frontend/work_items/components/work_item_comment_locked_spec.js41
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js18
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js92
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js27
-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.js93
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js29
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js95
-rw-r--r--spec/frontend/work_items/mock_data.js362
-rw-r--r--spec/frontend/work_items/router_spec.js1
305 files changed, 8859 insertions, 3618 deletions
diff --git a/spec/frontend/__mocks__/@cubejs-client/core.js b/spec/frontend/__mocks__/@cubejs-client/core.js
new file mode 100644
index 00000000000..549899aa8d8
--- /dev/null
+++ b/spec/frontend/__mocks__/@cubejs-client/core.js
@@ -0,0 +1,26 @@
+let mockLoad = jest.fn();
+let mockMetadata = jest.fn();
+
+export const CubejsApi = jest.fn().mockImplementation(() => ({
+ load: mockLoad,
+ meta: mockMetadata,
+}));
+
+export const HttpTransport = jest.fn();
+
+export const GRANULARITIES = [
+ {
+ name: 'seconds',
+ title: 'Seconds',
+ },
+];
+
+// eslint-disable-next-line no-underscore-dangle
+export const __setMockLoad = (x) => {
+ mockLoad = x;
+};
+
+// eslint-disable-next-line no-underscore-dangle
+export const __setMockMetadata = (x) => {
+ mockMetadata = x;
+};
diff --git a/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
new file mode 100644
index 00000000000..6efd9fb1dd0
--- /dev/null
+++ b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
@@ -0,0 +1,126 @@
+import { GlDrawer, GlForm, GlFormGroup, GlFormRadioGroup } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+
+jest.mock('~/lib/utils/common_utils', () => ({
+ contentTop: jest.fn(),
+}));
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
+
+describe('AbuseCategorySelector', () => {
+ let wrapper;
+
+ const ACTION_PATH = '/abuse_reports/add_category';
+ const USER_ID = '1';
+ const REPORTED_FROM_URL = 'http://example.com';
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(AbuseCategorySelector, {
+ propsData: {
+ ...props,
+ },
+ provide: {
+ reportAbusePath: ACTION_PATH,
+ reportedUserId: USER_ID,
+ reportedFromUrl: REPORTED_FROM_URL,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent({ showDrawer: true });
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+ const findTitle = () => wrapper.findByTestId('category-drawer-title');
+
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
+
+ const findCSRFToken = () => findForm().find('input[name="authenticity_token"]');
+ const findUserId = () => wrapper.findByTestId('input-user-id');
+ const findReferer = () => wrapper.findByTestId('input-referer');
+
+ const findSubmitFormButton = () => wrapper.findByTestId('submit-form-button');
+
+ describe('Drawer', () => {
+ it('is open when prop showDrawer = true', () => {
+ expect(findDrawer().exists()).toBe(true);
+ expect(findDrawer().props('open')).toBe(true);
+ expect(findDrawer().props('zIndex')).toBe(300);
+ });
+
+ it('renders title', () => {
+ expect(findTitle().text()).toBe(wrapper.vm.$options.i18n.title);
+ });
+
+ it('emits close-drawer event', async () => {
+ await findDrawer().vm.$emit('close');
+
+ expect(wrapper.emitted('close-drawer')).toHaveLength(1);
+ });
+
+ describe('when props showDrawer = false', () => {
+ beforeEach(() => {
+ createComponent({ showDrawer: false });
+ });
+
+ it('hides the drawer', () => {
+ expect(findDrawer().props('open')).toBe(false);
+ });
+ });
+ });
+
+ describe('Select category form', () => {
+ it('renders POST form with path', () => {
+ expect(findForm().attributes()).toMatchObject({
+ method: 'post',
+ action: ACTION_PATH,
+ });
+ });
+
+ it('renders csrf token', () => {
+ expect(findCSRFToken().attributes('value')).toBe('mock-csrf-token');
+ });
+
+ it('renders label', () => {
+ expect(findFormGroup().exists()).toBe(true);
+ expect(findFormGroup().attributes('label')).toBe(wrapper.vm.$options.i18n.label);
+ });
+
+ it('renders radio group', () => {
+ expect(findRadioGroup().exists()).toBe(true);
+ expect(findRadioGroup().props('options')).toEqual(wrapper.vm.$options.categoryOptions);
+ expect(findRadioGroup().attributes('name')).toBe('abuse_report[category]');
+ expect(findRadioGroup().attributes('required')).not.toBeUndefined();
+ });
+
+ it('renders userId as a hidden fields', () => {
+ expect(findUserId().attributes()).toMatchObject({
+ type: 'hidden',
+ name: 'user_id',
+ value: USER_ID,
+ });
+ });
+
+ it('renders referer as a hidden fields', () => {
+ expect(findReferer().attributes()).toMatchObject({
+ type: 'hidden',
+ name: 'abuse_report[reported_from_url]',
+ value: REPORTED_FROM_URL,
+ });
+ });
+
+ it('renders submit button', () => {
+ expect(findSubmitFormButton().exists()).toBe(true);
+ expect(findSubmitFormButton().text()).toBe(wrapper.vm.$options.i18n.next);
+ });
+ });
+});
diff --git a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
index 88ea79f38b3..36c0ac303ba 100644
--- a/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
+++ b/spec/frontend/admin/broadcast_messages/components/message_form_spec.js
@@ -3,7 +3,7 @@ import { GlBroadcastMessage, GlForm } from '@gitlab/ui';
import AxiosMockAdapter from 'axios-mock-adapter';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status';
import MessageForm from '~/admin/broadcast_messages/components/message_form.vue';
import {
BROADCAST_MESSAGES_PATH,
@@ -160,7 +160,7 @@ describe('MessageForm', () => {
it('shows an error alert if the create request fails', async () => {
createComponent({ broadcastMessage: { id: undefined } });
- axiosMock.onPost(BROADCAST_MESSAGES_PATH).replyOnce(httpStatus.BAD_REQUEST);
+ axiosMock.onPost(BROADCAST_MESSAGES_PATH).replyOnce(HTTP_STATUS_BAD_REQUEST);
findForm().vm.$emit('submit', { preventDefault: () => {} });
await waitForPromises();
@@ -187,7 +187,7 @@ describe('MessageForm', () => {
it('shows an error alert if the update request fails', async () => {
const id = 1337;
createComponent({ broadcastMessage: { id } });
- axiosMock.onPost(`${BROADCAST_MESSAGES_PATH}/${id}`).replyOnce(httpStatus.BAD_REQUEST);
+ axiosMock.onPost(`${BROADCAST_MESSAGES_PATH}/${id}`).replyOnce(HTTP_STATUS_BAD_REQUEST);
findForm().vm.$emit('submit', { preventDefault: () => {} });
await waitForPromises();
diff --git a/spec/frontend/admin/users/components/user_date_spec.js b/spec/frontend/admin/users/components/user_date_spec.js
index af262c6d3f0..73be33d5a9d 100644
--- a/spec/frontend/admin/users/components/user_date_spec.js
+++ b/spec/frontend/admin/users/components/user_date_spec.js
@@ -24,7 +24,7 @@ describe('FormatDate component', () => {
it.each`
date | dateFormat | output
- ${mockDate} | ${undefined} | ${'13 Nov, 2020'}
+ ${mockDate} | ${undefined} | ${'Nov 13, 2020'}
${null} | ${undefined} | ${'Never'}
${undefined} | ${undefined} | ${'Never'}
${mockDate} | ${ISO_SHORT_FORMAT} | ${'2020-11-13'}
diff --git a/spec/frontend/admin/users/mock_data.js b/spec/frontend/admin/users/mock_data.js
index 193ac3fa043..17cddebfcaf 100644
--- a/spec/frontend/admin/users/mock_data.js
+++ b/spec/frontend/admin/users/mock_data.js
@@ -62,3 +62,11 @@ export const userDeletionObstacles = [
{ name: 'schedule1', type: OBSTACLE_TYPES.oncallSchedules },
{ name: 'policy1', type: OBSTACLE_TYPES.escalationPolicies },
];
+
+export const userStatus = {
+ emoji: 'basketball',
+ message: 'test',
+ availability: 'busy',
+ message_html: 'test',
+ clear_status_at: '2023-01-04T10:00:00.000Z',
+};
diff --git a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
index 62a3e07186a..a15c78cc456 100644
--- a/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
+++ b/spec/frontend/alerts_settings/components/alerts_settings_wrapper_spec.js
@@ -32,7 +32,7 @@ import {
} from '~/alerts_settings/utils/error_messages';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
+import { HTTP_STATUS_FORBIDDEN, HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import {
createHttpVariables,
updateHttpVariables,
@@ -365,7 +365,7 @@ describe('AlertsSettingsWrapper', () => {
});
it('shows an error alert when integration is not activated', async () => {
- mock.onPost(/(.*)/).replyOnce(httpStatusCodes.FORBIDDEN);
+ mock.onPost(/(.*)/).replyOnce(HTTP_STATUS_FORBIDDEN);
await wrapper.vm.testAlertPayload({ endpoint: '', data: '', token: '' });
expect(createAlert).toHaveBeenCalledWith({
message: INTEGRATION_INACTIVE_PAYLOAD_TEST_ERROR,
diff --git a/spec/frontend/analytics/cycle_analytics/store/actions_spec.js b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
index f87807804c9..3030fca126b 100644
--- a/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
+++ b/spec/frontend/analytics/cycle_analytics/store/actions_spec.js
@@ -3,7 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/analytics/cycle_analytics/store/actions';
import * as getters from '~/analytics/cycle_analytics/store/getters';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
allowedStages,
selectedStage,
@@ -197,7 +197,7 @@ describe('Project Value Stream Analytics actions', () => {
selectedStage,
};
mock = new MockAdapter(axios);
- mock.onGet(mockStagePath).reply(httpStatusCodes.OK, reviewEvents, headers);
+ mock.onGet(mockStagePath).reply(HTTP_STATUS_OK, reviewEvents, headers);
});
it(`commits the 'RECEIVE_STAGE_DATA_SUCCESS' mutation`, () =>
@@ -223,7 +223,7 @@ describe('Project Value Stream Analytics actions', () => {
selectedStage,
};
mock = new MockAdapter(axios);
- mock.onGet(mockStagePath).reply(httpStatusCodes.OK, { error: tooMuchDataError });
+ mock.onGet(mockStagePath).reply(HTTP_STATUS_OK, { error: tooMuchDataError });
});
it(`commits the 'RECEIVE_STAGE_DATA_ERROR' mutation`, () =>
@@ -247,7 +247,7 @@ describe('Project Value Stream Analytics actions', () => {
selectedStage,
};
mock = new MockAdapter(axios);
- mock.onGet(mockStagePath).reply(httpStatusCodes.BAD_REQUEST);
+ mock.onGet(mockStagePath).reply(HTTP_STATUS_BAD_REQUEST);
});
it(`commits the 'RECEIVE_STAGE_DATA_ERROR' mutation`, () =>
@@ -269,7 +269,7 @@ describe('Project Value Stream Analytics actions', () => {
endpoints: mockEndpoints,
};
mock = new MockAdapter(axios);
- mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
+ mock.onGet(mockValueStreamPath).reply(HTTP_STATUS_OK);
});
it(`commits the 'REQUEST_VALUE_STREAMS' mutation`, () =>
@@ -284,7 +284,7 @@ describe('Project Value Stream Analytics actions', () => {
describe('with a failing request', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST);
+ mock.onGet(mockValueStreamPath).reply(HTTP_STATUS_BAD_REQUEST);
});
it(`commits the 'RECEIVE_VALUE_STREAMS_ERROR' mutation`, () =>
@@ -294,7 +294,7 @@ describe('Project Value Stream Analytics actions', () => {
payload: {},
expectedMutations: [
{ type: 'REQUEST_VALUE_STREAMS' },
- { type: 'RECEIVE_VALUE_STREAMS_ERROR', payload: httpStatusCodes.BAD_REQUEST },
+ { type: 'RECEIVE_VALUE_STREAMS_ERROR', payload: HTTP_STATUS_BAD_REQUEST },
],
expectedActions: [],
}));
@@ -337,7 +337,7 @@ describe('Project Value Stream Analytics actions', () => {
selectedValueStream,
};
mock = new MockAdapter(axios);
- mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
+ mock.onGet(mockValueStreamPath).reply(HTTP_STATUS_OK);
});
it(`commits the 'REQUEST_VALUE_STREAM_STAGES' and 'RECEIVE_VALUE_STREAM_STAGES_SUCCESS' mutations`, () =>
@@ -355,7 +355,7 @@ describe('Project Value Stream Analytics actions', () => {
describe('with a failing request', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST);
+ mock.onGet(mockValueStreamPath).reply(HTTP_STATUS_BAD_REQUEST);
});
it(`commits the 'RECEIVE_VALUE_STREAM_STAGES_ERROR' mutation`, () =>
@@ -365,7 +365,7 @@ describe('Project Value Stream Analytics actions', () => {
payload: {},
expectedMutations: [
{ type: 'REQUEST_VALUE_STREAM_STAGES' },
- { type: 'RECEIVE_VALUE_STREAM_STAGES_ERROR', payload: httpStatusCodes.BAD_REQUEST },
+ { type: 'RECEIVE_VALUE_STREAM_STAGES_ERROR', payload: HTTP_STATUS_BAD_REQUEST },
],
expectedActions: [],
}));
@@ -382,7 +382,7 @@ describe('Project Value Stream Analytics actions', () => {
];
const stageMedianError = new Error(
- `Request failed with status code ${httpStatusCodes.BAD_REQUEST}`,
+ `Request failed with status code ${HTTP_STATUS_BAD_REQUEST}`,
);
beforeEach(() => {
@@ -392,7 +392,7 @@ describe('Project Value Stream Analytics actions', () => {
stages: allowedStages,
};
mock = new MockAdapter(axios);
- mock.onGet(mockValueStreamPath).reply(httpStatusCodes.OK);
+ mock.onGet(mockValueStreamPath).reply(HTTP_STATUS_OK);
});
it(`commits the 'REQUEST_STAGE_MEDIANS' and 'RECEIVE_STAGE_MEDIANS_SUCCESS' mutations`, () =>
@@ -410,7 +410,7 @@ describe('Project Value Stream Analytics actions', () => {
describe('with a failing request', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST);
+ mock.onGet(mockValueStreamPath).reply(HTTP_STATUS_BAD_REQUEST);
});
it(`commits the 'RECEIVE_VALUE_STREAM_STAGES_ERROR' mutation`, () =>
@@ -435,9 +435,7 @@ describe('Project Value Stream Analytics actions', () => {
{ id: 'code', count: 3 },
];
- const stageCountError = new Error(
- `Request failed with status code ${httpStatusCodes.BAD_REQUEST}`,
- );
+ const stageCountError = new Error(`Request failed with status code ${HTTP_STATUS_BAD_REQUEST}`);
beforeEach(() => {
state = {
@@ -448,11 +446,11 @@ describe('Project Value Stream Analytics actions', () => {
mock = new MockAdapter(axios);
mock
.onGet(mockValueStreamPath)
- .replyOnce(httpStatusCodes.OK, { count: 1 })
+ .replyOnce(HTTP_STATUS_OK, { count: 1 })
.onGet(mockValueStreamPath)
- .replyOnce(httpStatusCodes.OK, { count: 2 })
+ .replyOnce(HTTP_STATUS_OK, { count: 2 })
.onGet(mockValueStreamPath)
- .replyOnce(httpStatusCodes.OK, { count: 3 });
+ .replyOnce(HTTP_STATUS_OK, { count: 3 });
});
it(`commits the 'REQUEST_STAGE_COUNTS' and 'RECEIVE_STAGE_COUNTS_SUCCESS' mutations`, () =>
@@ -470,7 +468,7 @@ describe('Project Value Stream Analytics actions', () => {
describe('with a failing request', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(mockValueStreamPath).reply(httpStatusCodes.BAD_REQUEST);
+ mock.onGet(mockValueStreamPath).reply(HTTP_STATUS_BAD_REQUEST);
});
it(`commits the 'RECEIVE_STAGE_COUNTS_ERROR' mutation`, () =>
diff --git a/spec/frontend/api/groups_api_spec.js b/spec/frontend/api/groups_api_spec.js
index 9de588a02aa..c354d8a9416 100644
--- a/spec/frontend/api/groups_api_spec.js
+++ b/spec/frontend/api/groups_api_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import getGroupTransferLocationsResponse from 'test_fixtures/api/groups/transfer_locations.json';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import { DEFAULT_PER_PAGE } from '~/api';
import { updateGroup, getGroupTransferLocations } from '~/api/groups_api';
@@ -35,7 +35,7 @@ describe('GroupsApi', () => {
beforeEach(() => {
mock.onPut(expectedUrl).reply(({ data }) => {
- return [httpStatus.OK, { id: mockGroupId, ...JSON.parse(data) }];
+ return [HTTP_STATUS_OK, { id: mockGroupId, ...JSON.parse(data) }];
});
});
diff --git a/spec/frontend/api/harbor_registry_spec.js b/spec/frontend/api/harbor_registry_spec.js
index 8a4c377ebd1..db4b189835e 100644
--- a/spec/frontend/api/harbor_registry_spec.js
+++ b/spec/frontend/api/harbor_registry_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import * as harborRegistryApi from '~/api/harbor_registry';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('~/api/harbor_registry', () => {
let mock;
@@ -37,7 +37,7 @@ describe('~/api/harbor_registry', () => {
location: 'http://demo.harbor.com/harbor/projects/2/repositories/image-1',
},
];
- mock.onGet(expectedUrl).reply(httpStatus.OK, expectResponse);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, expectResponse);
return harborRegistryApi.getHarborRepositoriesList(expectedParams).then(({ data }) => {
expect(data).toEqual(expectResponse);
@@ -66,7 +66,7 @@ describe('~/api/harbor_registry', () => {
tags: ['v2', 'v1', 'latest'],
},
];
- mock.onGet(expectedUrl).reply(httpStatus.OK, expectResponse);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, expectResponse);
return harborRegistryApi.getHarborArtifacts(expectedParams).then(({ data }) => {
expect(data).toEqual(expectResponse);
@@ -97,7 +97,7 @@ describe('~/api/harbor_registry', () => {
immutable: false,
},
];
- mock.onGet(expectedUrl).reply(httpStatus.OK, expectResponse);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, expectResponse);
return harborRegistryApi.getHarborTags(expectedParams).then(({ data }) => {
expect(data).toEqual(expectResponse);
diff --git a/spec/frontend/api/packages_api_spec.js b/spec/frontend/api/packages_api_spec.js
index d55d2036dcf..5f517bcf358 100644
--- a/spec/frontend/api/packages_api_spec.js
+++ b/spec/frontend/api/packages_api_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { publishPackage } from '~/api/packages_api';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Api', () => {
const dummyApiVersion = 'v3000';
@@ -35,7 +35,7 @@ describe('Api', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/packages/generic/${name}/${packageVersion}/${name}`;
jest.spyOn(axios, 'put');
- mock.onPut(expectedUrl).replyOnce(httpStatus.OK, apiResponse);
+ mock.onPut(expectedUrl).replyOnce(HTTP_STATUS_OK, apiResponse);
return publishPackage(
{
diff --git a/spec/frontend/api/tags_api_spec.js b/spec/frontend/api/tags_api_spec.js
index a7436bf6a50..af3533f52b7 100644
--- a/spec/frontend/api/tags_api_spec.js
+++ b/spec/frontend/api/tags_api_spec.js
@@ -1,7 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import * as tagsApi from '~/api/tags_api';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('~/api/tags_api.js', () => {
let mock;
@@ -25,7 +25,7 @@ describe('~/api/tags_api.js', () => {
it('fetches a tag of a given tag name of a particular project', () => {
const tagName = 'tag-name';
const expectedUrl = `/api/v7/projects/${projectId}/repository/tags/${tagName}`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, {
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, {
name: tagName,
});
diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js
index ba6b73e8c1a..9e901cf0f71 100644
--- a/spec/frontend/api/user_api_spec.js
+++ b/spec/frontend/api/user_api_spec.js
@@ -1,8 +1,13 @@
import MockAdapter from 'axios-mock-adapter';
-import { followUser, unfollowUser, associationsCount } from '~/api/user_api';
+import { followUser, unfollowUser, associationsCount, updateUserStatus } from '~/api/user_api';
import axios from '~/lib/utils/axios_utils';
-import { associationsCount as associationsCountData } from 'jest/admin/users/mock_data';
+import {
+ associationsCount as associationsCountData,
+ userStatus as mockUserStatus,
+} from 'jest/admin/users/mock_data';
+import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
+import { timeRanges } from '~/vue_shared/constants';
describe('~/api/user_api', () => {
let axiosMock;
@@ -62,4 +67,30 @@ describe('~/api/user_api', () => {
expect(axiosMock.history.get[0].url).toBe(expectedUrl);
});
});
+
+ describe('updateUserStatus', () => {
+ it('calls correct URL and returns expected response', async () => {
+ const expectedUrl = '/api/v4/user/status';
+ const expectedData = {
+ emoji: 'basketball',
+ message: 'test',
+ availability: AVAILABILITY_STATUS.BUSY,
+ clear_status_after: timeRanges[0].shortcut,
+ };
+ const expectedResponse = { data: mockUserStatus };
+
+ axiosMock.onPatch(expectedUrl).replyOnce(200, expectedResponse);
+
+ await expect(
+ updateUserStatus({
+ emoji: 'basketball',
+ message: 'test',
+ availability: AVAILABILITY_STATUS.BUSY,
+ clearStatusAfter: timeRanges[0].shortcut,
+ }),
+ ).resolves.toEqual(expect.objectContaining({ data: expectedResponse }));
+ expect(axiosMock.history.patch[0].url).toBe(expectedUrl);
+ expect(JSON.parse(axiosMock.history.patch[0].data)).toEqual(expectedData);
+ });
+ });
});
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 5209d9c2d2c..39fbe02480d 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -1,10 +1,13 @@
import MockAdapter from 'axios-mock-adapter';
import Api, { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
-import httpStatus, {
+import {
HTTP_STATUS_ACCEPTED,
HTTP_STATUS_CREATED,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -64,7 +67,7 @@ describe('Api', () => {
it('fetch all group packages', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/packages`;
jest.spyOn(axios, 'get');
- mock.onGet(expectedUrl).replyOnce(httpStatus.OK, apiResponse);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, apiResponse);
return Api.groupPackages(groupId).then(({ data }) => {
expect(data).toEqual(apiResponse);
@@ -77,7 +80,7 @@ describe('Api', () => {
it('fetch all project packages', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/packages`;
jest.spyOn(axios, 'get');
- mock.onGet(expectedUrl).replyOnce(httpStatus.OK, apiResponse);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, apiResponse);
return Api.projectPackages(projectId).then(({ data }) => {
expect(data).toEqual(apiResponse);
@@ -99,7 +102,7 @@ describe('Api', () => {
const expectedUrl = `foo`;
jest.spyOn(Api, 'buildProjectPackageUrl').mockReturnValue(expectedUrl);
jest.spyOn(axios, 'get');
- mock.onGet(expectedUrl).replyOnce(httpStatus.OK, apiResponse);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, apiResponse);
return Api.projectPackage(projectId, packageId).then(({ data }) => {
expect(data).toEqual(apiResponse);
@@ -114,7 +117,7 @@ describe('Api', () => {
jest.spyOn(Api, 'buildProjectPackageUrl').mockReturnValue(expectedUrl);
jest.spyOn(axios, 'delete');
- mock.onDelete(expectedUrl).replyOnce(httpStatus.OK, true);
+ mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_OK, true);
return Api.deleteProjectPackage(projectId, packageId).then(({ data }) => {
expect(data).toEqual(true);
@@ -130,7 +133,7 @@ describe('Api', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/packages/${packageId}/package_files/${packageFileId}`;
jest.spyOn(axios, 'delete');
- mock.onDelete(expectedUrl).replyOnce(httpStatus.OK, true);
+ mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_OK, true);
return Api.deleteProjectPackageFile(projectId, packageId, packageFileId).then(
({ data }) => {
@@ -150,7 +153,7 @@ describe('Api', () => {
jest.spyOn(axios, 'get');
jest.spyOn(Api, 'buildUrl').mockReturnValueOnce(expectedUrl);
- mock.onGet(expectedUrl).replyOnce(httpStatus.OK, apiResponse);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, apiResponse);
const { data } = await Api.containerRegistryDetails(1);
@@ -164,7 +167,7 @@ describe('Api', () => {
it('fetches a group', () => {
const groupId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, {
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, {
name: 'test',
});
@@ -182,7 +185,7 @@ describe('Api', () => {
const groupId = '54321';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/members`;
const expectedData = [{ id: 7 }];
- mock.onGet(expectedUrl).reply(httpStatus.OK, expectedData);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, expectedData);
return Api.groupMembers(groupId).then(({ data }) => {
expect(data).toEqual(expectedData);
@@ -232,7 +235,7 @@ describe('Api', () => {
web_url: 'https://gitlab.com/groups/gitlab-org/-/milestones/42',
},
];
- mock.onGet(expectedUrl).reply(httpStatus.OK, expectedData);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, expectedData);
return Api.groupMilestones(groupId).then(({ data }) => {
expect(data).toEqual(expectedData);
@@ -245,7 +248,7 @@ describe('Api', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups.json`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
name: 'test',
},
@@ -266,7 +269,7 @@ describe('Api', () => {
const options = { params: { search: 'foo' } };
const expectedGroup = 'gitlab-org';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${expectedGroup}/labels`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
id: 1,
name: 'Foo Label',
@@ -284,7 +287,7 @@ describe('Api', () => {
it('fetches namespaces', () => {
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/namespaces.json`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
name: 'test',
},
@@ -306,7 +309,7 @@ describe('Api', () => {
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
window.gon.current_user_id = 1;
- mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
name: 'test',
},
@@ -325,7 +328,7 @@ describe('Api', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects.json`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
name: 'test',
},
@@ -345,7 +348,7 @@ describe('Api', () => {
it('update a project with the given payload', () => {
const projectPath = 'foo';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}`;
- mock.onPut(expectedUrl).reply(httpStatus.OK, { foo: 'bar' });
+ mock.onPut(expectedUrl).reply(HTTP_STATUS_OK, { foo: 'bar' });
return Api.updateProject(projectPath, { foo: 'bar' }).then(({ data }) => {
expect(data.foo).toBe('bar');
@@ -359,7 +362,7 @@ describe('Api', () => {
const options = { unused: 'option' };
const projectPath = 'gitlab-org%2Fgitlab-ce';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/users`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
name: 'test',
},
@@ -378,7 +381,7 @@ describe('Api', () => {
it('fetches all merge requests for a project', () => {
const mockData = [{ source_branch: 'foo' }, { source_branch: 'bar' }];
- mock.onGet(expectedUrl).reply(httpStatus.OK, mockData);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, mockData);
return Api.projectMergeRequests(projectPath).then(({ data }) => {
expect(data.length).toEqual(2);
expect(data[0].source_branch).toBe('foo');
@@ -391,7 +394,7 @@ describe('Api', () => {
source_branch: 'bar',
};
const mockData = [{ source_branch: 'bar' }];
- mock.onGet(expectedUrl, { params }).reply(httpStatus.OK, mockData);
+ mock.onGet(expectedUrl, { params }).reply(HTTP_STATUS_OK, mockData);
return Api.projectMergeRequests(projectPath, params).then(({ data }) => {
expect(data.length).toEqual(1);
@@ -405,7 +408,7 @@ describe('Api', () => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, {
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, {
title: 'test',
});
@@ -420,7 +423,7 @@ describe('Api', () => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/changes`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, {
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, {
title: 'test',
});
@@ -435,7 +438,7 @@ describe('Api', () => {
const projectPath = 'abc';
const mergeRequestId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/merge_requests/${mergeRequestId}/versions`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
id: 123,
},
@@ -454,7 +457,7 @@ describe('Api', () => {
const params = { scope: 'active' };
const mockData = [{ id: 4 }];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectPath}/runners`;
- mock.onGet(expectedUrl, { params }).reply(httpStatus.OK, mockData);
+ mock.onGet(expectedUrl, { params }).reply(HTTP_STATUS_OK, mockData);
return Api.projectRunners(projectPath, { params }).then(({ data }) => {
expect(data).toEqual(mockData);
@@ -561,7 +564,7 @@ describe('Api', () => {
expect(config.data).toBe(JSON.stringify(expectedData));
return [
- httpStatus.OK,
+ HTTP_STATUS_OK,
{
name: 'test',
},
@@ -584,7 +587,7 @@ describe('Api', () => {
expect(config.data).toBe(JSON.stringify({ color: labelData.color }));
return [
- httpStatus.OK,
+ HTTP_STATUS_OK,
{
...labelData,
},
@@ -605,7 +608,7 @@ describe('Api', () => {
const groupId = '123456';
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
name: 'test',
},
@@ -660,7 +663,7 @@ describe('Api', () => {
)}/repository/commits/${sha}`;
it('fetches a single commit', () => {
- mock.onGet(expectedUrl).reply(httpStatus.OK, { id: sha });
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { id: sha });
return Api.commit(projectId, sha).then(({ data: commit }) => {
expect(commit.id).toBe(sha);
@@ -668,7 +671,7 @@ describe('Api', () => {
});
it('fetches a single commit without stats', () => {
- mock.onGet(expectedUrl, { params: { stats: false } }).reply(httpStatus.OK, { id: sha });
+ mock.onGet(expectedUrl, { params: { stats: false } }).reply(HTTP_STATUS_OK, { id: sha });
return Api.commit(projectId, sha, { stats: false }).then(({ data: commit }) => {
expect(commit.id).toBe(sha);
@@ -686,7 +689,7 @@ describe('Api', () => {
)}`;
it('fetches an issue template', () => {
- mock.onGet(expectedUrl).reply(httpStatus.OK, 'test');
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, 'test');
return new Promise((resolve) => {
Api.issueTemplate(namespace, project, templateKey, templateType, (_, response) => {
@@ -698,7 +701,7 @@ describe('Api', () => {
describe('when an error occurs while fetching an issue template', () => {
it('rejects the Promise', () => {
- mock.onGet(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return new Promise((resolve) => {
Api.issueTemplate(namespace, project, templateKey, templateType, () => {
@@ -720,7 +723,7 @@ describe('Api', () => {
const expectedData = [
{ key: 'Template1', name: 'Template 1', content: 'This is template 1!' },
];
- mock.onGet(expectedUrl).reply(httpStatus.OK, expectedData);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, expectedData);
return new Promise((resolve) => {
Api.issueTemplates(namespace, project, templateType, (_, response) => {
@@ -736,7 +739,7 @@ describe('Api', () => {
describe('when an error occurs while fetching issue templates', () => {
it('rejects the Promise', () => {
- mock.onGet(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
Api.issueTemplates(namespace, project, templateType, () => {
expect(mock.history.get).toHaveLength(1);
@@ -749,7 +752,7 @@ describe('Api', () => {
it('fetches a list of templates', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/gitlab-org%2Fgitlab-ce/templates/licenses`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, 'test');
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, 'test');
return new Promise((resolve) => {
Api.projectTemplates('gitlab-org/gitlab-ce', 'licenses', {}, (response) => {
@@ -765,7 +768,7 @@ describe('Api', () => {
const data = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/gitlab-org%2Fgitlab-ce/templates/licenses/test%20license`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, 'test');
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, 'test');
return new Promise((resolve) => {
Api.projectTemplate(
@@ -787,7 +790,7 @@ describe('Api', () => {
const query = 'dummy query';
const options = { unused: 'option' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users.json`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
name: 'test',
},
@@ -804,7 +807,7 @@ describe('Api', () => {
it('fetches single user', () => {
const userId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, {
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, {
name: 'testuser',
});
@@ -817,7 +820,7 @@ describe('Api', () => {
describe('user counts', () => {
it('fetches single user counts', () => {
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/user_counts`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, {
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, {
merge_requests: 4,
});
@@ -831,7 +834,7 @@ describe('Api', () => {
it('fetches single user status', () => {
const userId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}/status`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, {
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, {
message: 'testmessage',
});
@@ -847,7 +850,7 @@ describe('Api', () => {
const options = { unused: 'option' };
const userId = '123456';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/users/${userId}/projects`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
name: 'test',
},
@@ -868,7 +871,7 @@ describe('Api', () => {
const projectId = 'example/foobar';
const commitSha = 'abc123def';
const expectedUrl = `${dummyUrlRoot}/${projectId}/commit/${commitSha}/pipelines`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
name: 'test',
},
@@ -893,7 +896,7 @@ describe('Api', () => {
name: 'test',
},
];
- mock.onGet(expectedUrl, { params }).reply(httpStatus.OK, payload);
+ mock.onGet(expectedUrl, { params }).reply(HTTP_STATUS_OK, payload);
const { data } = await Api.pipelineJobs(projectId, pipelineId, params);
expect(data).toEqual(payload);
@@ -912,7 +915,7 @@ describe('Api', () => {
jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl).replyOnce(httpStatus.OK, {
+ mock.onPost(expectedUrl).replyOnce(HTTP_STATUS_OK, {
name: branch,
});
@@ -932,7 +935,7 @@ describe('Api', () => {
jest.spyOn(axios, 'get');
- mock.onGet(expectedUrl).replyOnce(httpStatus.OK, ['fork']);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, ['fork']);
return Api.projectForks(dummyProjectPath, { visibility: 'private' }).then(({ data }) => {
expect(data).toEqual(['fork']);
@@ -1021,7 +1024,7 @@ describe('Api', () => {
describe('when releases are successfully returned', () => {
it('resolves the Promise', () => {
- mock.onGet(expectedUrl).replyOnce(httpStatus.OK);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK);
return Api.releases(dummyProjectPath).then(() => {
expect(mock.history.get).toHaveLength(1);
@@ -1031,7 +1034,7 @@ describe('Api', () => {
describe('when an error occurs while fetching releases', () => {
it('rejects the Promise', () => {
- mock.onGet(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.releases(dummyProjectPath).catch(() => {
expect(mock.history.get).toHaveLength(1);
@@ -1045,7 +1048,7 @@ describe('Api', () => {
describe('when the release is successfully returned', () => {
it('resolves the Promise', () => {
- mock.onGet(expectedUrl).replyOnce(httpStatus.OK);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK);
return Api.release(dummyProjectPath, dummyTagName).then(() => {
expect(mock.history.get).toHaveLength(1);
@@ -1055,7 +1058,7 @@ describe('Api', () => {
describe('when an error occurs while fetching the release', () => {
it('rejects the Promise', () => {
- mock.onGet(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.release(dummyProjectPath, dummyTagName).catch(() => {
expect(mock.history.get).toHaveLength(1);
@@ -1083,7 +1086,7 @@ describe('Api', () => {
describe('when an error occurs while creating the release', () => {
it('rejects the Promise', () => {
- mock.onPost(expectedUrl, release).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ mock.onPost(expectedUrl, release).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.createRelease(dummyProjectPath, release).catch(() => {
expect(mock.history.post).toHaveLength(1);
@@ -1101,7 +1104,7 @@ describe('Api', () => {
describe('when the release is successfully updated', () => {
it('resolves the Promise', () => {
- mock.onPut(expectedUrl, release).replyOnce(httpStatus.OK);
+ mock.onPut(expectedUrl, release).replyOnce(HTTP_STATUS_OK);
return Api.updateRelease(dummyProjectPath, dummyTagName, release).then(() => {
expect(mock.history.put).toHaveLength(1);
@@ -1111,7 +1114,7 @@ describe('Api', () => {
describe('when an error occurs while updating the release', () => {
it('rejects the Promise', () => {
- mock.onPut(expectedUrl, release).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ mock.onPut(expectedUrl, release).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.updateRelease(dummyProjectPath, dummyTagName, release).catch(() => {
expect(mock.history.put).toHaveLength(1);
@@ -1139,7 +1142,7 @@ describe('Api', () => {
describe('when an error occurs while creating the Release', () => {
it('rejects the Promise', () => {
- mock.onPost(expectedUrl, expectedLink).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ mock.onPost(expectedUrl, expectedLink).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.createReleaseLink(dummyProjectPath, dummyTagName, expectedLink).catch(() => {
expect(mock.history.post).toHaveLength(1);
@@ -1154,7 +1157,7 @@ describe('Api', () => {
describe('when the Release is successfully deleted', () => {
it('resolves the Promise', () => {
- mock.onDelete(expectedUrl).replyOnce(httpStatus.OK);
+ mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_OK);
return Api.deleteReleaseLink(dummyProjectPath, dummyTagName, dummyLinkId).then(() => {
expect(mock.history.delete).toHaveLength(1);
@@ -1164,7 +1167,7 @@ describe('Api', () => {
describe('when an error occurs while deleting the Release', () => {
it('rejects the Promise', () => {
- mock.onDelete(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.deleteReleaseLink(dummyProjectPath, dummyTagName, dummyLinkId).catch(() => {
expect(mock.history.delete).toHaveLength(1);
@@ -1183,7 +1186,7 @@ describe('Api', () => {
describe('when the raw file is successfully fetched', () => {
beforeEach(() => {
- mock.onGet(expectedUrl).replyOnce(httpStatus.OK);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK);
});
it('resolves the Promise', () => {
@@ -1206,7 +1209,7 @@ describe('Api', () => {
describe('when an error occurs while getting a raw file', () => {
it('rejects the Promise', () => {
- mock.onPost(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ mock.onPost(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.getRawFile(dummyProjectPath, dummyFilePath).catch(() => {
expect(mock.history.get).toHaveLength(1);
@@ -1238,7 +1241,7 @@ describe('Api', () => {
describe('when an error occurs while getting a raw file', () => {
it('rejects the Promise', () => {
- mock.onPost(expectedUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ mock.onPost(expectedUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Api.createProjectMergeRequest(dummyProjectPath).catch(() => {
expect(mock.history.post).toHaveLength(1);
@@ -1253,7 +1256,7 @@ describe('Api', () => {
const issue = 1;
const expectedArray = [1, 2, 3];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/issues/${issue}`;
- mock.onPut(expectedUrl).reply(httpStatus.OK, { assigneeIds: expectedArray });
+ mock.onPut(expectedUrl).reply(HTTP_STATUS_OK, { assigneeIds: expectedArray });
return Api.updateIssue(projectId, issue, { assigneeIds: expectedArray }).then(({ data }) => {
expect(data.assigneeIds).toEqual(expectedArray);
@@ -1267,7 +1270,7 @@ describe('Api', () => {
const mergeRequest = 1;
const expectedArray = [1, 2, 3];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/merge_requests/${mergeRequest}`;
- mock.onPut(expectedUrl).reply(httpStatus.OK, { assigneeIds: expectedArray });
+ mock.onPut(expectedUrl).reply(HTTP_STATUS_OK, { assigneeIds: expectedArray });
return Api.updateMergeRequest(projectId, mergeRequest, { assigneeIds: expectedArray }).then(
({ data }) => {
@@ -1283,7 +1286,7 @@ describe('Api', () => {
const options = { unused: 'option' };
const projectId = 8;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/repository/tags`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
name: 'test',
},
@@ -1308,7 +1311,7 @@ describe('Api', () => {
updated_at: '2020-07-10T05:10:35.122Z',
};
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/freeze_periods`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, [freezePeriod]);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [freezePeriod]);
return Api.freezePeriods(projectId).then(({ data }) => {
expect(data[0]).toStrictEqual(freezePeriod);
@@ -1368,7 +1371,7 @@ describe('Api', () => {
describe('when the freeze period is successfully updated', () => {
it('resolves the Promise', () => {
- mock.onPut(expectedUrl, options).replyOnce(httpStatus.OK, expectedResult);
+ mock.onPut(expectedUrl, options).replyOnce(HTTP_STATUS_OK, expectedResult);
return Api.updateFreezePeriod(projectId, options).then(({ data }) => {
expect(data).toStrictEqual(expectedResult);
@@ -1392,7 +1395,7 @@ describe('Api', () => {
jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl).replyOnce(httpStatus.OK, {
+ mock.onPost(expectedUrl).replyOnce(HTTP_STATUS_OK, {
web_url: redirectUrl,
});
@@ -1423,7 +1426,7 @@ describe('Api', () => {
it('returns null', () => {
jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl).replyOnce(httpStatus.OK, true);
+ mock.onPost(expectedUrl).replyOnce(HTTP_STATUS_OK, true);
expect(axios.post).toHaveBeenCalledTimes(0);
expect(Api.trackRedisCounterEvent(event)).toEqual(null);
@@ -1437,7 +1440,7 @@ describe('Api', () => {
it('resolves the Promise', () => {
jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl, { event }).replyOnce(httpStatus.OK, true);
+ mock.onPost(expectedUrl, { event }).replyOnce(HTTP_STATUS_OK, true);
return Api.trackRedisCounterEvent(event).then(({ data }) => {
expect(data).toEqual(true);
@@ -1483,7 +1486,7 @@ describe('Api', () => {
it('resolves the Promise', () => {
jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl, { event }).replyOnce(httpStatus.OK, true);
+ mock.onPost(expectedUrl, { event }).replyOnce(HTTP_STATUS_OK, true);
return Api.trackRedisHllUserEvent(event).then(({ data }) => {
expect(data).toEqual(true);
@@ -1544,7 +1547,7 @@ describe('Api', () => {
];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/deploy_keys`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, deployKeys);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, deployKeys);
const params = { page: 2, public: true };
const { data } = await Api.deployKeys(params);
@@ -1569,7 +1572,7 @@ describe('Api', () => {
];
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/secure_files`;
- mock.onGet(expectedUrl).reply(httpStatus.OK, secureFiles);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles);
const { data } = await Api.projectSecureFiles(projectId, {});
expect(data).toEqual(secureFiles);
@@ -1589,7 +1592,7 @@ describe('Api', () => {
};
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/${projectId}/secure_files`;
- mock.onPost(expectedUrl).reply(httpStatus.OK, secureFile);
+ mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, secureFile);
const { data } = await Api.uploadProjectSecureFile(projectId, 'some data');
expect(data).toEqual(secureFile);
@@ -1639,7 +1642,7 @@ describe('Api', () => {
describe('fetchFeatureFlagUserLists', () => {
it('GETs the right url', () => {
- mock.onGet(expectedUrl).replyOnce(httpStatus.OK, []);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, []);
return Api.fetchFeatureFlagUserLists(projectId).then(({ data }) => {
expect(data).toEqual([]);
@@ -1649,7 +1652,7 @@ describe('Api', () => {
describe('searchFeatureFlagUserLists', () => {
it('GETs the right url', () => {
- mock.onGet(expectedUrl, { params: { search: 'test' } }).replyOnce(httpStatus.OK, []);
+ mock.onGet(expectedUrl, { params: { search: 'test' } }).replyOnce(HTTP_STATUS_OK, []);
return Api.searchFeatureFlagUserLists(projectId, 'test').then(({ data }) => {
expect(data).toEqual([]);
@@ -1663,7 +1666,7 @@ describe('Api', () => {
name: 'mock_user_list',
user_xids: '1,2,3,4',
};
- mock.onPost(expectedUrl, mockUserListData).replyOnce(httpStatus.OK, mockUserList);
+ mock.onPost(expectedUrl, mockUserListData).replyOnce(HTTP_STATUS_OK, mockUserList);
return Api.createFeatureFlagUserList(projectId, mockUserListData).then(({ data }) => {
expect(data).toEqual(mockUserList);
@@ -1673,7 +1676,7 @@ describe('Api', () => {
describe('fetchFeatureFlagUserList', () => {
it('GETs the right url', () => {
- mock.onGet(`${expectedUrl}/1`).replyOnce(httpStatus.OK, mockUserList);
+ mock.onGet(`${expectedUrl}/1`).replyOnce(HTTP_STATUS_OK, mockUserList);
return Api.fetchFeatureFlagUserList(projectId, 1).then(({ data }) => {
expect(data).toEqual(mockUserList);
@@ -1685,7 +1688,7 @@ describe('Api', () => {
it('PUTs the right url', () => {
mock
.onPut(`${expectedUrl}/1`)
- .replyOnce(httpStatus.OK, { ...mockUserList, user_xids: '5' });
+ .replyOnce(HTTP_STATUS_OK, { ...mockUserList, user_xids: '5' });
return Api.updateFeatureFlagUserList(projectId, {
...mockUserList,
@@ -1698,7 +1701,7 @@ describe('Api', () => {
describe('deleteFeatureFlagUserList', () => {
it('DELETEs the right url', () => {
- mock.onDelete(`${expectedUrl}/1`).replyOnce(httpStatus.OK, 'deleted');
+ mock.onDelete(`${expectedUrl}/1`).replyOnce(HTTP_STATUS_OK, 'deleted');
return Api.deleteFeatureFlagUserList(projectId, 1).then(({ data }) => {
expect(data).toBe('deleted');
@@ -1715,12 +1718,12 @@ describe('Api', () => {
it('returns 404 for non-existing branch', () => {
jest.spyOn(axios, 'get');
- mock.onGet(expectedUrl).replyOnce(httpStatus.NOT_FOUND, {
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_NOT_FOUND, {
message: '404 Not found',
});
return Api.projectProtectedBranch(dummyProjectId, branchName).catch((error) => {
- expect(error.response.status).toBe(httpStatus.NOT_FOUND);
+ expect(error.response.status).toBe(HTTP_STATUS_NOT_FOUND);
expect(axios.get).toHaveBeenCalledWith(expectedUrl);
});
});
@@ -1730,7 +1733,7 @@ describe('Api', () => {
jest.spyOn(axios, 'get');
- mock.onGet(expectedUrl).replyOnce(httpStatus.OK, expectedObj);
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedObj);
return Api.projectProtectedBranch(dummyProjectId, branchName).then((data) => {
expect(data).toEqual(expectedObj);
diff --git a/spec/frontend/artifacts/components/artifact_row_spec.js b/spec/frontend/artifacts/components/artifact_row_spec.js
index dcc0d684f13..2a7156bf480 100644
--- a/spec/frontend/artifacts/components/artifact_row_spec.js
+++ b/spec/frontend/artifacts/components/artifact_row_spec.js
@@ -16,13 +16,14 @@ describe('ArtifactRow component', () => {
const findDownloadButton = () => wrapper.findByTestId('job-artifact-row-download-button');
const findDeleteButton = () => wrapper.findByTestId('job-artifact-row-delete-button');
- const createComponent = (mountFn = shallowMountExtended) => {
- wrapper = mountFn(ArtifactRow, {
+ const createComponent = ({ canDestroyArtifacts = true } = {}) => {
+ wrapper = shallowMountExtended(ArtifactRow, {
propsData: {
artifact,
isLoading: false,
isLastRow: false,
},
+ provide: { canDestroyArtifacts },
stubs: { GlBadge, GlButton, GlFriendlyWrap },
});
};
@@ -50,12 +51,24 @@ describe('ArtifactRow component', () => {
it('displays the download button as a link to the download path', () => {
expect(findDownloadButton().attributes('href')).toBe(artifact.downloadPath);
});
+ });
+
+ describe('delete button', () => {
+ it('does not show when user does not have permission', () => {
+ createComponent({ canDestroyArtifacts: false });
+
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+
+ it('shows when user has permission', () => {
+ createComponent();
- it('displays the delete button', () => {
expect(findDeleteButton().exists()).toBe(true);
});
- it('emits the delete event when the delete button is clicked', async () => {
+ it('emits the delete event when clicked', async () => {
+ createComponent();
+
expect(wrapper.emitted('delete')).toBeUndefined();
findDeleteButton().trigger('click');
diff --git a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js
index c6ad13462f9..d006e0285d2 100644
--- a/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js
+++ b/spec/frontend/artifacts/components/artifacts_table_row_details_spec.js
@@ -40,6 +40,7 @@ describe('ArtifactsTableRowDetails component', () => {
refetchArtifacts,
queryVariables: {},
},
+ provide: { canDestroyArtifacts: true },
data() {
return { deletingArtifactId: null };
},
diff --git a/spec/frontend/artifacts/components/feedback_banner_spec.js b/spec/frontend/artifacts/components/feedback_banner_spec.js
new file mode 100644
index 00000000000..3421486020a
--- /dev/null
+++ b/spec/frontend/artifacts/components/feedback_banner_spec.js
@@ -0,0 +1,63 @@
+import { GlBanner } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import FeedbackBanner from '~/artifacts/components/feedback_banner.vue';
+import { makeMockUserCalloutDismisser } from 'helpers/mock_user_callout_dismisser';
+import {
+ I18N_FEEDBACK_BANNER_TITLE,
+ I18N_FEEDBACK_BANNER_BUTTON,
+ FEEDBACK_URL,
+} from '~/artifacts/constants';
+
+const mockBannerImagePath = 'banner/image/path';
+
+describe('Artifacts management feedback banner', () => {
+ let wrapper;
+ let userCalloutDismissSpy;
+
+ const findBanner = () => wrapper.findComponent(GlBanner);
+
+ const createComponent = ({ shouldShowCallout = true } = {}) => {
+ userCalloutDismissSpy = jest.fn();
+
+ wrapper = shallowMount(FeedbackBanner, {
+ provide: {
+ artifactsManagementFeedbackImagePath: mockBannerImagePath,
+ },
+ stubs: {
+ UserCalloutDismisser: makeMockUserCalloutDismisser({
+ dismiss: userCalloutDismissSpy,
+ shouldShowCallout,
+ }),
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('is displayed with the correct props', () => {
+ createComponent();
+
+ expect(findBanner().props()).toMatchObject({
+ title: I18N_FEEDBACK_BANNER_TITLE,
+ buttonText: I18N_FEEDBACK_BANNER_BUTTON,
+ buttonLink: FEEDBACK_URL,
+ svgPath: mockBannerImagePath,
+ });
+ });
+
+ it('dismisses the callout when closed', () => {
+ createComponent();
+
+ findBanner().vm.$emit('close');
+
+ expect(userCalloutDismissSpy).toHaveBeenCalled();
+ });
+
+ it('is not displayed once it has been dismissed', () => {
+ createComponent({ shouldShowCallout: false });
+
+ expect(findBanner().exists()).toBe(false);
+ });
+});
diff --git a/spec/frontend/artifacts/components/job_artifacts_table_spec.js b/spec/frontend/artifacts/components/job_artifacts_table_spec.js
index 131b4b99bb2..dbe4598f599 100644
--- a/spec/frontend/artifacts/components/job_artifacts_table_spec.js
+++ b/spec/frontend/artifacts/components/job_artifacts_table_spec.js
@@ -5,6 +5,7 @@ import getJobArtifactsResponse from 'test_fixtures/graphql/artifacts/graphql/que
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import waitForPromises from 'helpers/wait_for_promises';
import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue';
+import FeedbackBanner from '~/artifacts/components/feedback_banner.vue';
import ArtifactsTableRowDetails from '~/artifacts/components/artifacts_table_row_details.vue';
import ArtifactDeleteModal from '~/artifacts/components/artifact_delete_modal.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -23,6 +24,8 @@ describe('JobArtifactsTable component', () => {
let wrapper;
let requestHandlers;
+ const findBanner = () => wrapper.findComponent(FeedbackBanner);
+
const findLoadingState = () => wrapper.findComponent(GlLoadingIcon);
const findTable = () => wrapper.findComponent(GlTable);
const findDetailsRows = () => wrapper.findAllComponents(ArtifactsTableRowDetails);
@@ -79,13 +82,18 @@ describe('JobArtifactsTable component', () => {
getJobArtifactsQuery: jest.fn().mockResolvedValue(getJobArtifactsResponse),
},
data = {},
+ canDestroyArtifacts = true,
) => {
requestHandlers = handlers;
wrapper = mountExtended(JobArtifactsTable, {
apolloProvider: createMockApollo([
[getJobArtifactsQuery, requestHandlers.getJobArtifactsQuery],
]),
- provide: { projectPath: 'project/path' },
+ provide: {
+ projectPath: 'project/path',
+ canDestroyArtifacts,
+ artifactsManagementFeedbackImagePath: 'banner/image/path',
+ },
data() {
return data;
},
@@ -96,6 +104,12 @@ describe('JobArtifactsTable component', () => {
wrapper.destroy();
});
+ it('renders feedback banner', () => {
+ createComponent();
+
+ expect(findBanner().exists()).toBe(true);
+ });
+
it('when loading, shows a loading state', () => {
createComponent();
@@ -283,6 +297,14 @@ describe('JobArtifactsTable component', () => {
});
describe('delete button', () => {
+ it('does not show when user does not have permission', async () => {
+ createComponent({}, {}, false);
+
+ await waitForPromises();
+
+ expect(findDeleteButton().exists()).toBe(false);
+ });
+
it('shows a disabled delete button for now (coming soon)', async () => {
createComponent();
diff --git a/spec/frontend/autosave_spec.js b/spec/frontend/autosave_spec.js
index 7a9262cd004..88460221168 100644
--- a/spec/frontend/autosave_spec.js
+++ b/spec/frontend/autosave_spec.js
@@ -1,4 +1,3 @@
-import $ from 'jquery';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import Autosave from '~/autosave';
import AccessorUtilities from '~/lib/utils/accessor';
@@ -7,12 +6,19 @@ describe('Autosave', () => {
useLocalStorageSpy();
let autosave;
- const field = $('<textarea></textarea>');
- const checkbox = $('<input type="checkbox">');
+ const field = document.createElement('textarea');
+ const checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
const key = 'key';
const fallbackKey = 'fallbackKey';
const lockVersionKey = 'lockVersionKey';
const lockVersion = 1;
+ const getAutosaveKey = () => `autosave/${key}`;
+ const getAutosaveLockKey = () => `autosave/${key}/lockVersion`;
+
+ afterEach(() => {
+ autosave?.dispose?.();
+ });
describe('class constructor', () => {
beforeEach(() => {
@@ -43,18 +49,10 @@ describe('Autosave', () => {
});
describe('restore', () => {
- beforeEach(() => {
- autosave = {
- field,
- key,
- };
- });
-
describe('if .isLocalStorageAvailable is `false`', () => {
beforeEach(() => {
- autosave.isLocalStorageAvailable = false;
-
- Autosave.prototype.restore.call(autosave);
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(false);
+ autosave = new Autosave(field, key);
});
it('should not call .getItem', () => {
@@ -63,97 +61,73 @@ describe('Autosave', () => {
});
describe('if .isLocalStorageAvailable is `true`', () => {
- beforeEach(() => {
- autosave.isLocalStorageAvailable = true;
- });
-
it('should call .getItem', () => {
- Autosave.prototype.restore.call(autosave);
-
- expect(window.localStorage.getItem).toHaveBeenCalledWith(key);
+ autosave = new Autosave(field, key);
+ expect(window.localStorage.getItem.mock.calls).toEqual([[getAutosaveKey()], []]);
});
- it('triggers jquery event', () => {
- jest.spyOn(autosave.field, 'trigger').mockImplementation(() => {});
-
- Autosave.prototype.restore.call(autosave);
-
- expect(field.trigger).toHaveBeenCalled();
- });
-
- it('triggers native event', () => {
- const fieldElement = autosave.field.get(0);
- const eventHandler = jest.fn();
- fieldElement.addEventListener('change', eventHandler);
-
- Autosave.prototype.restore.call(autosave);
+ describe('if saved value is present', () => {
+ const storedValue = 'bar';
- expect(eventHandler).toHaveBeenCalledTimes(1);
- fieldElement.removeEventListener('change', eventHandler);
- });
-
- describe('if field type is checkbox', () => {
beforeEach(() => {
- autosave = {
- field: checkbox,
- key,
- isLocalStorageAvailable: true,
- type: 'checkbox',
- };
+ field.value = 'foo';
+ window.localStorage.setItem(getAutosaveKey(), storedValue);
});
- it('should restore', () => {
- window.localStorage.setItem(key, true);
- expect(checkbox.is(':checked')).toBe(false);
- Autosave.prototype.restore.call(autosave);
- expect(checkbox.is(':checked')).toBe(true);
+ it('restores the value', () => {
+ autosave = new Autosave(field, key);
+ expect(field.value).toEqual(storedValue);
});
- });
- });
- describe('if field gets deleted from DOM', () => {
- beforeEach(() => {
- autosave.field = $('.not-a-real-element');
- });
+ it('triggers native event', () => {
+ const eventHandler = jest.fn();
+ field.addEventListener('change', eventHandler);
+ autosave = new Autosave(field, key);
- it('does not trigger event', () => {
- jest.spyOn(field, 'trigger');
+ expect(eventHandler).toHaveBeenCalledTimes(1);
+ field.removeEventListener('change', eventHandler);
+ });
+
+ describe('if field type is checkbox', () => {
+ beforeEach(() => {
+ checkbox.checked = false;
+ window.localStorage.setItem(getAutosaveKey(), true);
+ autosave = new Autosave(checkbox, key);
+ });
- expect(field.trigger).not.toHaveBeenCalled();
+ it('should restore', () => {
+ expect(checkbox.checked).toBe(true);
+ });
+ });
});
});
});
describe('getSavedLockVersion', () => {
- beforeEach(() => {
- autosave = {
- field,
- key,
- lockVersionKey,
- };
- });
-
describe('if .isLocalStorageAvailable is `false`', () => {
beforeEach(() => {
- autosave.isLocalStorageAvailable = false;
-
- Autosave.prototype.getSavedLockVersion.call(autosave);
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(false);
+ autosave = new Autosave(field, key);
});
it('should not call .getItem', () => {
+ autosave.getSavedLockVersion();
expect(window.localStorage.getItem).not.toHaveBeenCalled();
});
});
describe('if .isLocalStorageAvailable is `true`', () => {
beforeEach(() => {
- autosave.isLocalStorageAvailable = true;
+ autosave = new Autosave(field, key);
});
it('should call .getItem', () => {
- Autosave.prototype.getSavedLockVersion.call(autosave);
-
- expect(window.localStorage.getItem).toHaveBeenCalledWith(lockVersionKey);
+ autosave.getSavedLockVersion();
+ expect(window.localStorage.getItem.mock.calls).toEqual([
+ [getAutosaveKey()],
+ [],
+ [getAutosaveLockKey()],
+ ]);
});
});
});
@@ -162,7 +136,7 @@ describe('Autosave', () => {
beforeEach(() => {
autosave = { reset: jest.fn() };
autosave.field = field;
- field.val('value');
+ field.value = 'value';
});
describe('if .isLocalStorageAvailable is `false`', () => {
@@ -200,14 +174,14 @@ describe('Autosave', () => {
});
it('should save true when checkbox on', () => {
- checkbox.prop('checked', true);
+ checkbox.checked = true;
Autosave.prototype.save.call(autosave);
expect(window.localStorage.setItem).toHaveBeenCalledWith(key, true);
});
it('should call reset when checkbox off', () => {
autosave.reset = jest.fn();
- checkbox.prop('checked', false);
+ checkbox.checked = false;
Autosave.prototype.save.call(autosave);
expect(autosave.reset).toHaveBeenCalled();
expect(window.localStorage.setItem).not.toHaveBeenCalled();
diff --git a/spec/frontend/batch_comments/components/submit_dropdown_spec.js b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
index 462ef7e7280..003a6d86371 100644
--- a/spec/frontend/batch_comments/components/submit_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/submit_dropdown_spec.js
@@ -3,6 +3,8 @@ import Vuex from 'vuex';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import SubmitDropdown from '~/batch_comments/components/submit_dropdown.vue';
+jest.mock('~/autosave');
+
Vue.use(Vuex);
let wrapper;
diff --git a/spec/frontend/behaviors/markdown/render_gfm_spec.js b/spec/frontend/behaviors/markdown/render_gfm_spec.js
new file mode 100644
index 00000000000..0bbb92282e5
--- /dev/null
+++ b/spec/frontend/behaviors/markdown/render_gfm_spec.js
@@ -0,0 +1,9 @@
+import { renderGFM } from '~/behaviors/markdown/render_gfm';
+
+describe('renderGFM', () => {
+ it('handles a missing element', () => {
+ expect(() => {
+ renderGFM();
+ }).not.toThrow();
+ });
+});
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index d05e057095d..2c8e6306431 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -1,7 +1,7 @@
import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { range } from 'lodash';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { nextTick } from 'vue';
import setWindowLocation from 'helpers/set_window_location_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
@@ -17,6 +17,8 @@ import { mockLabelList, mockIssue, mockIssueFullPath } from './mock_data';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/boards/eventhub');
+Vue.use(Vuex);
+
describe('Board card component', () => {
const user = {
id: 1,
@@ -52,25 +54,19 @@ describe('Board card component', () => {
const performSearchMock = jest.fn();
- const createStore = ({ isProjectBoard = false } = {}) => {
+ const createStore = () => {
store = new Vuex.Store({
- ...defaultStore,
actions: {
performSearch: performSearchMock,
},
state: {
...defaultStore.state,
- issuableType: issuableTypes.issue,
isShowingLabels: true,
},
- getters: {
- isGroupBoard: () => true,
- isProjectBoard: () => isProjectBoard,
- },
});
};
- const createWrapper = ({ props = {}, isEpicBoard = false } = {}) => {
+ const createWrapper = ({ props = {}, isEpicBoard = false, isGroupBoard = true } = {}) => {
wrapper = mountExtended(BoardCardInner, {
store,
propsData: {
@@ -97,6 +93,8 @@ describe('Board card component', () => {
rootPath: '/',
scopedLabelsAvailable: false,
isEpicBoard,
+ issuableType: issuableTypes.issue,
+ isGroupBoard,
},
});
};
@@ -164,8 +162,8 @@ describe('Board card component', () => {
});
it('does not render item reference path', () => {
- createStore({ isProjectBoard: true });
- createWrapper();
+ createStore();
+ createWrapper({ isGroupBoard: false });
expect(wrapper.find('.board-card-number').text()).not.toContain(mockIssueFullPath);
});
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index c5c3faf1712..1ba546f24a8 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -58,8 +58,6 @@ export default function createComponent({
...state,
},
getters: {
- isGroupBoard: () => false,
- isProjectBoard: () => true,
isEpicBoard: () => false,
...getters,
},
@@ -88,7 +86,6 @@ export default function createComponent({
apolloProvider: fakeApollo,
store,
propsData: {
- disabled: false,
list,
boardItems: [issue],
canAdminList: true,
@@ -97,12 +94,16 @@ export default function createComponent({
provide: {
groupId: null,
rootPath: '/',
+ fullPath: 'gitlab-org',
boardId: '1',
weightFeatureAvailable: false,
boardWeight: null,
canAdminList: true,
isIssueBoard: true,
isEpicBoard: false,
+ isGroupBoard: false,
+ isProjectBoard: true,
+ disabled: false,
...provide,
},
stubs,
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index 34c0504143c..abe8c230bd8 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -267,7 +267,7 @@ describe('Board list component', () => {
describe('when dragging is not allowed', () => {
beforeEach(() => {
wrapper = createComponent({
- componentProps: {
+ provide: {
disabled: true,
},
});
diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js
index c209f2f82e6..872a67a71fb 100644
--- a/spec/frontend/boards/components/board_app_spec.js
+++ b/spec/frontend/boards/components/board_app_spec.js
@@ -23,11 +23,10 @@ describe('BoardApp', () => {
});
};
- const createComponent = ({ provide = { disabled: true } } = {}) => {
+ const createComponent = () => {
wrapper = shallowMount(BoardApp, {
store,
provide: {
- ...provide,
fullBoardId: 'gid://gitlab/Board/1',
},
});
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index 38b79e2e3f3..f8ad7c468c1 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -29,9 +29,6 @@ describe('Board card', () => {
...initialState,
},
actions: mockActions,
- getters: {
- isProjectBoard: () => false,
- },
});
};
@@ -52,7 +49,6 @@ describe('Board card', () => {
propsData: {
list: mockLabelList,
item,
- disabled: false,
index: 0,
...propsData,
},
@@ -61,6 +57,10 @@ describe('Board card', () => {
rootPath: '/',
scopedLabelsAvailable: false,
isEpicBoard: false,
+ issuableType: 'issue',
+ isProjectBoard: false,
+ isGroupBoard: true,
+ disabled: false,
...provide,
},
});
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index c13f7caba76..d34e228a2d7 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -34,7 +34,6 @@ describe('Board Column Component', () => {
wrapper = shallowMount(BoardColumn, {
store,
propsData: {
- disabled: false,
list: listMock,
},
});
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 0d5b1d16e30..51c42b48535 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -7,7 +7,7 @@ import SidebarDropdownWidget from 'ee_else_ce/sidebar/components/sidebar_dropdow
import { stubComponent } from 'helpers/stub_component';
import BoardContentSidebar from '~/boards/components/board_content_sidebar.vue';
import BoardSidebarTitle from '~/boards/components/sidebar/board_sidebar_title.vue';
-import { ISSUABLE } from '~/boards/constants';
+import { ISSUABLE, issuableTypes } from '~/boards/constants';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
@@ -26,7 +26,6 @@ describe('BoardContentSidebar', () => {
sidebarType: ISSUABLE,
issues: { [mockIssue.id]: { ...mockIssue, epic: null } },
activeId: mockIssue.id,
- issuableType: 'issue',
},
getters: {
activeBoardItem: () => {
@@ -35,7 +34,6 @@ describe('BoardContentSidebar', () => {
groupPathForActiveIssue: () => mockIssueGroupPath,
projectPathForActiveIssue: () => mockIssueProjectPath,
isSidebarOpen: () => true,
- isGroupBoard: () => false,
...mockGetters,
},
actions: mockActions,
@@ -55,6 +53,8 @@ describe('BoardContentSidebar', () => {
canUpdate: true,
rootPath: '/',
groupId: 1,
+ issuableType: issuableTypes.issue,
+ isGroupBoard: false,
},
store,
stubs: {
diff --git a/spec/frontend/boards/components/board_content_spec.js b/spec/frontend/boards/components/board_content_spec.js
index 82e7ab48e7d..97596c86198 100644
--- a/spec/frontend/boards/components/board_content_spec.js
+++ b/spec/frontend/boards/components/board_content_spec.js
@@ -60,7 +60,6 @@ describe('BoardContent', () => {
wrapper = shallowMount(BoardContent, {
apolloProvider: fakeApollo,
propsData: {
- disabled: false,
boardId: 'gid://gitlab/Board/1',
...props,
},
@@ -71,6 +70,8 @@ describe('BoardContent', () => {
issuableType,
isIssueBoard,
isEpicBoard,
+ isGroupBoard: true,
+ disabled: false,
isApolloBoard,
},
store,
diff --git a/spec/frontend/boards/components/board_filtered_search_spec.js b/spec/frontend/boards/components/board_filtered_search_spec.js
index e80c66f7fb8..4c0cc36889c 100644
--- a/spec/frontend/boards/components/board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/board_filtered_search_spec.js
@@ -139,6 +139,7 @@ describe('BoardFilteredSearch', () => {
{ type: TOKEN_TYPE_ITERATION, value: { data: 'Any&3', operator: '=' } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v1.0.0', operator: '=' } },
{ type: TOKEN_TYPE_HEALTH, value: { data: 'onTrack', operator: '=' } },
+ { type: TOKEN_TYPE_HEALTH, value: { data: 'atRisk', operator: '!=' } },
];
jest.spyOn(urlUtility, 'updateHistory');
findFilteredSearch().vm.$emit('onFilter', mockFilters);
@@ -147,7 +148,7 @@ describe('BoardFilteredSearch', () => {
title: '',
replace: true,
url:
- 'http://test.host/?author_username=root&label_name[]=label&label_name[]=label%262&assignee_username=root&milestone_title=New%20Milestone&iteration_id=Any&iteration_cadence_id=3&types=INCIDENT&weight=2&release_tag=v1.0.0&health_status=onTrack',
+ 'http://test.host/?not[health_status]=atRisk&author_username=root&label_name[]=label&label_name[]=label%262&assignee_username=root&milestone_title=New%20Milestone&iteration_id=Any&iteration_cadence_id=3&types=INCIDENT&weight=2&release_tag=v1.0.0&health_status=onTrack',
});
});
diff --git a/spec/frontend/boards/components/board_form_spec.js b/spec/frontend/boards/components/board_form_spec.js
index fdc16b46167..f8154145d43 100644
--- a/spec/frontend/boards/components/board_form_spec.js
+++ b/spec/frontend/boards/components/board_form_spec.js
@@ -53,10 +53,6 @@ describe('BoardForm', () => {
const setErrorMock = jest.fn();
const store = new Vuex.Store({
- getters: {
- isGroupBoard: () => true,
- isProjectBoard: () => false,
- },
actions: {
setBoard: setBoardMock,
setError: setErrorMock,
@@ -73,6 +69,8 @@ describe('BoardForm', () => {
},
provide: {
boardBaseUrl: 'root',
+ isGroupBoard: true,
+ isProjectBoard: false,
},
mocks: {
$apollo: {
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index 4633612891c..a16b99728c3 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -68,7 +68,6 @@ describe('Board List Header Component', () => {
apolloProvider: fakeApollo,
store,
propsData: {
- disabled: false,
list: listMock,
},
provide: {
@@ -76,6 +75,7 @@ describe('Board List Header Component', () => {
weightFeatureAvailable: false,
currentUserId,
isEpicBoard: false,
+ disabled: false,
},
}),
);
diff --git a/spec/frontend/boards/components/board_new_issue_spec.js b/spec/frontend/boards/components/board_new_issue_spec.js
index f097f42476a..c3e69ba0e40 100644
--- a/spec/frontend/boards/components/board_new_issue_spec.js
+++ b/spec/frontend/boards/components/board_new_issue_spec.js
@@ -14,9 +14,10 @@ const addListNewIssuesSpy = jest.fn().mockResolvedValue();
const mockActions = { addListNewIssue: addListNewIssuesSpy };
const createComponent = ({
- state = { selectedProject: mockGroupProjects[0], fullPath: mockGroupProjects[0].fullPath },
+ state = { selectedProject: mockGroupProjects[0] },
actions = mockActions,
- getters = { isGroupBoard: () => true, getBoardItemsByList: () => () => [] },
+ getters = { getBoardItemsByList: () => () => [] },
+ isGroupBoard = true,
} = {}) =>
shallowMount(BoardNewIssue, {
store: new Vuex.Store({
@@ -29,8 +30,10 @@ const createComponent = ({
},
provide: {
groupId: 1,
+ fullPath: mockGroupProjects[0].fullPath,
weightFeatureAvailable: false,
boardWeight: null,
+ isGroupBoard,
},
stubs: {
BoardNewItem,
@@ -84,9 +87,9 @@ describe('Issue boards new issue form', () => {
beforeEach(() => {
wrapper = createComponent({
getters: {
- isGroupBoard: () => true,
getBoardItemsByList: () => () => [mockIssue, mockIssue2],
},
+ isGroupBoard: true,
});
});
@@ -128,7 +131,7 @@ describe('Issue boards new issue form', () => {
describe('when in project issue board', () => {
beforeEach(() => {
wrapper = createComponent({
- getters: { isGroupBoard: () => false },
+ isGroupBoard: false,
});
});
diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js
index 08b5042f70f..af492145eb0 100644
--- a/spec/frontend/boards/components/board_top_bar_spec.js
+++ b/spec/frontend/boards/components/board_top_bar_spec.js
@@ -33,6 +33,7 @@ describe('BoardTopBar', () => {
boardType: 'group',
releasesFetchPath: '/releases',
isIssueBoard: true,
+ isGroupBoard: true,
...provide,
},
stubs: { IssueBoardFilteredSearch },
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index f3be66db36f..7b61ca5e6fd 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -10,7 +10,6 @@ import groupBoardsQuery from '~/boards/graphql/group_boards.query.graphql';
import projectBoardsQuery from '~/boards/graphql/project_boards.query.graphql';
import groupRecentBoardsQuery from '~/boards/graphql/group_recent_boards.query.graphql';
import projectRecentBoardsQuery from '~/boards/graphql/project_recent_boards.query.graphql';
-import defaultStore from '~/boards/stores';
import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import {
@@ -28,25 +27,20 @@ import {
const throttleDuration = 1;
Vue.use(VueApollo);
+Vue.use(Vuex);
describe('BoardsSelector', () => {
let wrapper;
let fakeApollo;
let store;
- const createStore = ({ isGroupBoard = false, isProjectBoard = false } = {}) => {
+ const createStore = () => {
store = new Vuex.Store({
- ...defaultStore,
actions: {
setError: jest.fn(),
setBoardConfig: jest.fn(),
},
- getters: {
- isGroupBoard: () => isGroupBoard,
- isProjectBoard: () => isProjectBoard,
- },
state: {
- boardType: isGroupBoard ? 'group' : 'project',
board: mockBoard,
},
});
@@ -86,6 +80,8 @@ describe('BoardsSelector', () => {
const createComponent = ({
projectBoardsQueryHandler = projectBoardsQueryHandlerSuccess,
projectRecentBoardsQueryHandler = projectRecentBoardsQueryHandlerSuccess,
+ isGroupBoard = false,
+ isProjectBoard = false,
} = {}) => {
fakeApollo = createMockApollo([
[projectBoardsQuery, projectBoardsQueryHandler],
@@ -109,6 +105,9 @@ describe('BoardsSelector', () => {
multipleIssueBoardsAvailable: true,
scopedIssueBoardFeatureEnabled: true,
weights: [],
+ boardType: isGroupBoard ? 'group' : 'project',
+ isGroupBoard,
+ isProjectBoard,
},
});
};
@@ -120,8 +119,8 @@ describe('BoardsSelector', () => {
describe('template', () => {
beforeEach(() => {
- createStore({ isProjectBoard: true });
- createComponent();
+ createStore();
+ createComponent({ isProjectBoard: true });
});
describe('loading', () => {
@@ -229,11 +228,11 @@ describe('BoardsSelector', () => {
${BoardType.group} | ${groupBoardsQueryHandlerSuccess} | ${projectBoardsQueryHandlerSuccess}
${BoardType.project} | ${projectBoardsQueryHandlerSuccess} | ${groupBoardsQueryHandlerSuccess}
`('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => {
- createStore({
- isProjectBoard: boardType === BoardType.project,
+ createStore();
+ createComponent({
isGroupBoard: boardType === BoardType.group,
+ isProjectBoard: boardType === BoardType.project,
});
- createComponent();
await nextTick();
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 513561307cd..57a30ddc512 100644
--- a/spec/frontend/boards/components/issue_board_filtered_search_spec.js
+++ b/spec/frontend/boards/components/issue_board_filtered_search_spec.js
@@ -18,7 +18,7 @@ describe('IssueBoardFilter', () => {
isSignedIn,
releasesFetchPath: '/releases',
fullPath: 'gitlab-org',
- boardType: 'group',
+ isGroupBoard: true,
},
});
};
diff --git a/spec/frontend/boards/stores/getters_spec.js b/spec/frontend/boards/stores/getters_spec.js
index 304f2aad98e..c86a256bd96 100644
--- a/spec/frontend/boards/stores/getters_spec.js
+++ b/spec/frontend/boards/stores/getters_spec.js
@@ -12,42 +12,6 @@ import {
} from '../mock_data';
describe('Boards - Getters', () => {
- describe('isGroupBoard', () => {
- it('returns true when boardType on state is group', () => {
- const state = {
- boardType: 'group',
- };
-
- expect(getters.isGroupBoard(state)).toBe(true);
- });
-
- it('returns false when boardType on state is not group', () => {
- const state = {
- boardType: 'project',
- };
-
- expect(getters.isGroupBoard(state)).toBe(false);
- });
- });
-
- describe('isProjectBoard', () => {
- it('returns true when boardType on state is project', () => {
- const state = {
- boardType: 'project',
- };
-
- expect(getters.isProjectBoard(state)).toBe(true);
- });
-
- it('returns false when boardType on state is not project', () => {
- const state = {
- boardType: 'group',
- };
-
- expect(getters.isProjectBoard(state)).toBe(false);
- });
- });
-
describe('isSidebarOpen', () => {
it('returns true when activeId is not equal to 0', () => {
const state = {
diff --git a/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js b/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
index b2a25bc93ea..002fe7c6e71 100644
--- a/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
+++ b/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
@@ -4,9 +4,11 @@ import { registerCaptchaModalInterceptor } from '~/captcha/captcha_modal_axios_i
import UnsolvedCaptchaError from '~/captcha/unsolved_captcha_error';
import { waitForCaptchaToBeSolved } from '~/captcha/wait_for_captcha_to_be_solved';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes, {
+import {
HTTP_STATUS_CONFLICT,
HTTP_STATUS_METHOD_NOT_ALLOWED,
+ HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
jest.mock('~/captcha/wait_for_captcha_to_be_solved');
@@ -46,7 +48,7 @@ describe('registerCaptchaModalInterceptor', () => {
} = config.headers;
if (captchaResponse === CAPTCHA_RESPONSE && spamLogId === SPAM_LOG_ID) {
- return [httpStatusCodes.OK, { ...data, method: config.method, CAPTCHA_SUCCESS }];
+ return [HTTP_STATUS_OK, { ...data, method: config.method, CAPTCHA_SUCCESS }];
}
return [HTTP_STATUS_CONFLICT, NEEDS_CAPTCHA_RESPONSE];
@@ -64,7 +66,7 @@ describe('registerCaptchaModalInterceptor', () => {
it('successful requests are passed through', async () => {
const { data, status } = await axios[method]('/endpoint-without-captcha');
- expect(status).toEqual(httpStatusCodes.OK);
+ expect(status).toEqual(HTTP_STATUS_OK);
expect(data).toEqual(AXIOS_RESPONSE);
expect(mock.history[method]).toHaveLength(1);
});
@@ -73,7 +75,7 @@ describe('registerCaptchaModalInterceptor', () => {
await expect(() => axios[method]('/endpoint-with-unrelated-error')).rejects.toThrow(
expect.objectContaining({
response: expect.objectContaining({
- status: httpStatusCodes.NOT_FOUND,
+ status: HTTP_STATUS_NOT_FOUND,
data: AXIOS_RESPONSE,
}),
}),
diff --git a/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js b/spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
index 2210b0f48d6..e4abedb412f 100644
--- a/spec/frontend/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
+++ b/spec/frontend/ci/ci_variable_list/ci_variable_list/ci_variable_list_spec.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import VariableList from '~/ci_variable_list/ci_variable_list';
+import VariableList from '~/ci/ci_variable_list/ci_variable_list';
const HIDE_CLASS = 'hide';
diff --git a/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js b/spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
index 57f666e29d6..71e8e6d3afb 100644
--- a/spec/frontend/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
+++ b/spec/frontend/ci/ci_variable_list/ci_variable_list/native_form_variable_list_spec.js
@@ -1,6 +1,6 @@
import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import setupNativeFormVariableList from '~/ci_variable_list/native_form_variable_list';
+import setupNativeFormVariableList from '~/ci/ci_variable_list/native_form_variable_list';
describe('NativeFormVariableList', () => {
let $wrapper;
diff --git a/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
index aa83638773d..5e0c35c9f90 100644
--- a/spec/frontend/ci_variable_list/components/ci_admin_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_admin_variables_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
-import ciAdminVariables from '~/ci_variable_list/components/ci_admin_variables.vue';
-import ciVariableShared from '~/ci_variable_list/components/ci_variable_shared.vue';
+import ciAdminVariables from '~/ci/ci_variable_list/components/ci_admin_variables.vue';
+import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
describe('Ci Project Variable wrapper', () => {
let wrapper;
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
new file mode 100644
index 00000000000..2fd395a1230
--- /dev/null
+++ b/spec/frontend/ci/ci_variable_list/components/ci_environments_dropdown_spec.js
@@ -0,0 +1,118 @@
+import { GlListboxItem, GlCollapsibleListbox, GlDropdownItem, GlIcon } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { allEnvironments } from '~/ci/ci_variable_list/constants';
+import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
+
+describe('Ci environments dropdown', () => {
+ let wrapper;
+
+ const envs = ['dev', 'prod', 'staging'];
+ const defaultProps = { environments: envs, selectedEnvironmentScope: '' };
+
+ const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
+ const findListboxItemByIndex = (index) => wrapper.findAllComponents(GlListboxItem).at(index);
+ const findActiveIconByIndex = (index) => findListboxItemByIndex(index).findComponent(GlIcon);
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findListboxText = () => findListbox().props('toggleText');
+ const findCreateWildcardButton = () => wrapper.findComponent(GlDropdownItem);
+
+ const createComponent = ({ props = {}, searchTerm = '' } = {}) => {
+ wrapper = mount(CiEnvironmentsDropdown, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ });
+
+ findListbox().vm.$emit('search', searchTerm);
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('No environments found', () => {
+ beforeEach(() => {
+ createComponent({ searchTerm: 'stable' });
+ });
+
+ it('renders create button with search term if environments do not contain search term', () => {
+ const button = findCreateWildcardButton();
+ expect(button.exists()).toBe(true);
+ expect(button.text()).toBe('Create wildcard: stable');
+ });
+ });
+
+ describe('Search term is empty', () => {
+ beforeEach(() => {
+ createComponent({ props: { environments: envs } });
+ });
+
+ it('renders all environments when search term is empty', () => {
+ expect(findListboxItemByIndex(0).text()).toBe(envs[0]);
+ expect(findListboxItemByIndex(1).text()).toBe(envs[1]);
+ expect(findListboxItemByIndex(2).text()).toBe(envs[2]);
+ });
+
+ it('does not display active checkmark on the inactive stage', () => {
+ expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
+ });
+ });
+
+ describe('when `*` is the value of selectedEnvironmentScope props', () => {
+ const wildcardScope = '*';
+
+ beforeEach(() => {
+ createComponent({ props: { selectedEnvironmentScope: wildcardScope } });
+ });
+
+ it('shows the `All environments` text and not the wildcard', () => {
+ expect(findListboxText()).toContain(allEnvironments.text);
+ expect(findListboxText()).not.toContain(wildcardScope);
+ });
+ });
+
+ describe('Environments found', () => {
+ const currentEnv = envs[2];
+
+ beforeEach(() => {
+ createComponent({ searchTerm: currentEnv });
+ });
+
+ it('renders only the environment searched for', () => {
+ expect(findAllListboxItems()).toHaveLength(1);
+ expect(findListboxItemByIndex(0).text()).toBe(currentEnv);
+ });
+
+ it('does not display create button', () => {
+ expect(findCreateWildcardButton().exists()).toBe(false);
+ });
+
+ describe('Custom events', () => {
+ describe('when selecting an environment', () => {
+ const itemIndex = 0;
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('emits `select-environment` when an environment is clicked', () => {
+ findListbox().vm.$emit('select', envs[itemIndex]);
+ expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
+ });
+ });
+
+ describe('when creating a new environment from a search term', () => {
+ const search = 'new-env';
+ beforeEach(() => {
+ createComponent({ searchTerm: search });
+ });
+
+ it('emits create-environment-scope', () => {
+ findCreateWildcardButton().vm.$emit('click');
+ expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
index ef624d8e4b4..3f1eebbc6a5 100644
--- a/spec/frontend/ci_variable_list/components/ci_group_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import ciGroupVariables from '~/ci_variable_list/components/ci_group_variables.vue';
-import ciVariableShared from '~/ci_variable_list/components/ci_variable_shared.vue';
+import ciGroupVariables from '~/ci/ci_variable_list/components/ci_group_variables.vue';
+import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
-import { GRAPHQL_GROUP_TYPE } from '~/ci_variable_list/constants';
+import { GRAPHQL_GROUP_TYPE } from '~/ci/ci_variable_list/constants';
const mockProvide = {
glFeatures: {
diff --git a/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
index 53c25e430f2..7230017c560 100644
--- a/spec/frontend/ci_variable_list/components/ci_project_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
@@ -1,10 +1,10 @@
import { shallowMount } from '@vue/test-utils';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import ciProjectVariables from '~/ci_variable_list/components/ci_project_variables.vue';
-import ciVariableShared from '~/ci_variable_list/components/ci_variable_shared.vue';
+import ciProjectVariables from '~/ci/ci_variable_list/components/ci_project_variables.vue';
+import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
-import { GRAPHQL_PROJECT_TYPE } from '~/ci_variable_list/constants';
+import { GRAPHQL_PROJECT_TYPE } from '~/ci/ci_variable_list/constants';
const mockProvide = {
projectFullPath: '/namespace/project',
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
index d177e755591..7838e4884d8 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
@@ -1,8 +1,8 @@
import { GlButton, GlFormInput } from '@gitlab/ui';
import { mockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
-import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
-import CiVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
+import CiEnvironmentsDropdown from '~/ci/ci_variable_list/components/ci_environments_dropdown.vue';
+import CiVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
import {
ADD_VARIABLE_ACTION,
AWS_ACCESS_KEY_ID,
@@ -12,7 +12,7 @@ import {
ENVIRONMENT_SCOPE_LINK_TITLE,
instanceString,
variableOptions,
-} from '~/ci_variable_list/constants';
+} from '~/ci/ci_variable_list/constants';
import { mockVariablesWithScopes } from '../mocks';
import ModalStub from '../stubs';
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
index 5e459ee390f..32af2ec4de9 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_settings_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_settings_spec.js
@@ -1,14 +1,14 @@
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
-import CiVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
-import ciVariableModal from '~/ci_variable_list/components/ci_variable_modal.vue';
-import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
+import CiVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
+import ciVariableModal from '~/ci/ci_variable_list/components/ci_variable_modal.vue';
+import ciVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue';
import {
ADD_VARIABLE_ACTION,
EDIT_VARIABLE_ACTION,
projectString,
-} from '~/ci_variable_list/constants';
-import { mapEnvironmentNames } from '~/ci_variable_list/utils';
+} from '~/ci/ci_variable_list/constants';
+import { mapEnvironmentNames } from '~/ci/ci_variable_list/utils';
import { mockEnvs, mockVariablesWithScopes, newVariable } from '../mocks';
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
index 65a58a1647f..2d39bff8ce0 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_shared_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
@@ -5,16 +5,16 @@ import { shallowMount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import { resolvers } from '~/ci_variable_list/graphql/settings';
+import { resolvers } from '~/ci/ci_variable_list/graphql/settings';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import ciVariableShared from '~/ci_variable_list/components/ci_variable_shared.vue';
-import ciVariableSettings from '~/ci_variable_list/components/ci_variable_settings.vue';
-import ciVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
-import getProjectEnvironments from '~/ci_variable_list/graphql/queries/project_environments.query.graphql';
-import getAdminVariables from '~/ci_variable_list/graphql/queries/variables.query.graphql';
-import getGroupVariables from '~/ci_variable_list/graphql/queries/group_variables.query.graphql';
-import getProjectVariables from '~/ci_variable_list/graphql/queries/project_variables.query.graphql';
+import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
+import ciVariableSettings from '~/ci/ci_variable_list/components/ci_variable_settings.vue';
+import ciVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue';
+import getProjectEnvironments from '~/ci/ci_variable_list/graphql/queries/project_environments.query.graphql';
+import getAdminVariables from '~/ci/ci_variable_list/graphql/queries/variables.query.graphql';
+import getGroupVariables from '~/ci/ci_variable_list/graphql/queries/group_variables.query.graphql';
+import getProjectVariables from '~/ci/ci_variable_list/graphql/queries/project_variables.query.graphql';
import {
ADD_MUTATION_ACTION,
@@ -23,7 +23,7 @@ import {
environmentFetchErrorText,
genericMutationErrorText,
variableFetchErrorText,
-} from '~/ci_variable_list/constants';
+} from '~/ci/ci_variable_list/constants';
import {
createGroupProps,
diff --git a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
index 9891bc397b6..9e2508c56ee 100644
--- a/spec/frontend/ci_variable_list/components/ci_variable_table_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_table_spec.js
@@ -1,8 +1,8 @@
import { GlAlert } from '@gitlab/ui';
import { sprintf } from '~/locale';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import CiVariableTable from '~/ci_variable_list/components/ci_variable_table.vue';
-import { EXCEEDS_VARIABLE_LIMIT_TEXT, projectString } from '~/ci_variable_list/constants';
+import CiVariableTable from '~/ci/ci_variable_list/components/ci_variable_table.vue';
+import { EXCEEDS_VARIABLE_LIMIT_TEXT, projectString } from '~/ci/ci_variable_list/constants';
import { mockVariables } from '../mocks';
describe('Ci variable table', () => {
diff --git a/spec/frontend/ci_variable_list/mocks.js b/spec/frontend/ci/ci_variable_list/mocks.js
index 065e9fa6667..4da4f53f69f 100644
--- a/spec/frontend/ci_variable_list/mocks.js
+++ b/spec/frontend/ci/ci_variable_list/mocks.js
@@ -6,22 +6,22 @@ import {
groupString,
instanceString,
projectString,
-} from '~/ci_variable_list/constants';
-
-import addAdminVariable from '~/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql';
-import deleteAdminVariable from '~/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql';
-import updateAdminVariable from '~/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql';
-import addGroupVariable from '~/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
-import deleteGroupVariable from '~/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
-import updateGroupVariable from '~/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
-import addProjectVariable from '~/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql';
-import deleteProjectVariable from '~/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql';
-import updateProjectVariable from '~/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql';
-
-import getAdminVariables from '~/ci_variable_list/graphql/queries/variables.query.graphql';
-import getGroupVariables from '~/ci_variable_list/graphql/queries/group_variables.query.graphql';
-import getProjectEnvironments from '~/ci_variable_list/graphql/queries/project_environments.query.graphql';
-import getProjectVariables from '~/ci_variable_list/graphql/queries/project_variables.query.graphql';
+} from '~/ci/ci_variable_list/constants';
+
+import addAdminVariable from '~/ci/ci_variable_list/graphql/mutations/admin_add_variable.mutation.graphql';
+import deleteAdminVariable from '~/ci/ci_variable_list/graphql/mutations/admin_delete_variable.mutation.graphql';
+import updateAdminVariable from '~/ci/ci_variable_list/graphql/mutations/admin_update_variable.mutation.graphql';
+import addGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_add_variable.mutation.graphql';
+import deleteGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_delete_variable.mutation.graphql';
+import updateGroupVariable from '~/ci/ci_variable_list/graphql/mutations/group_update_variable.mutation.graphql';
+import addProjectVariable from '~/ci/ci_variable_list/graphql/mutations/project_add_variable.mutation.graphql';
+import deleteProjectVariable from '~/ci/ci_variable_list/graphql/mutations/project_delete_variable.mutation.graphql';
+import updateProjectVariable from '~/ci/ci_variable_list/graphql/mutations/project_update_variable.mutation.graphql';
+
+import getAdminVariables from '~/ci/ci_variable_list/graphql/queries/variables.query.graphql';
+import getGroupVariables from '~/ci/ci_variable_list/graphql/queries/group_variables.query.graphql';
+import getProjectEnvironments from '~/ci/ci_variable_list/graphql/queries/project_environments.query.graphql';
+import getProjectVariables from '~/ci/ci_variable_list/graphql/queries/project_variables.query.graphql';
export const devName = 'dev';
export const prodName = 'prod';
diff --git a/spec/frontend/ci_variable_list/services/mock_data.js b/spec/frontend/ci/ci_variable_list/services/mock_data.js
index 44f4db93c63..44f4db93c63 100644
--- a/spec/frontend/ci_variable_list/services/mock_data.js
+++ b/spec/frontend/ci/ci_variable_list/services/mock_data.js
diff --git a/spec/frontend/ci_variable_list/stubs.js b/spec/frontend/ci/ci_variable_list/stubs.js
index 5769d6190f6..5769d6190f6 100644
--- a/spec/frontend/ci_variable_list/stubs.js
+++ b/spec/frontend/ci/ci_variable_list/stubs.js
diff --git a/spec/frontend/ci_variable_list/utils_spec.js b/spec/frontend/ci/ci_variable_list/utils_spec.js
index 081c399792f..beeae71376a 100644
--- a/spec/frontend/ci_variable_list/utils_spec.js
+++ b/spec/frontend/ci/ci_variable_list/utils_spec.js
@@ -2,8 +2,8 @@ import {
createJoinedEnvironments,
convertEnvironmentScope,
mapEnvironmentNames,
-} from '~/ci_variable_list/utils';
-import { allEnvironments } from '~/ci_variable_list/constants';
+} from '~/ci/ci_variable_list/utils';
+import { allEnvironments } from '~/ci/ci_variable_list/constants';
describe('utils', () => {
const environments = ['dev', 'prod'];
diff --git a/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
index 63e23c41263..ec987be8cb8 100644
--- a/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/text_editor_spec.js
@@ -26,14 +26,13 @@ describe('Pipeline Editor | Text editor component', () => {
props: ['value', 'fileName', 'editorOptions', 'debounceValue'],
};
- const createComponent = (glFeatures = {}, mountFn = shallowMount) => {
+ const createComponent = (mountFn = shallowMount) => {
wrapper = mountFn(TextEditor, {
provide: {
projectPath: mockProjectPath,
projectNamespace: mockProjectNamespace,
ciConfigPath: mockCiConfigPath,
defaultBranch: mockDefaultBranch,
- glFeatures,
},
propsData: {
commitSha: mockCommitSha,
@@ -107,28 +106,14 @@ describe('Pipeline Editor | Text editor component', () => {
});
describe('CI schema', () => {
- describe('when `schema_linting` feature flag is on', () => {
- beforeEach(() => {
- createComponent({ schemaLinting: true });
- findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
- });
-
- it('configures editor with syntax highlight', () => {
- expect(mockUse).toHaveBeenCalledTimes(1);
- expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
- });
+ beforeEach(() => {
+ createComponent();
+ findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
});
- describe('when `schema_linting` feature flag is off', () => {
- beforeEach(() => {
- createComponent();
- findEditor().vm.$emit(EDITOR_READY_EVENT, editorInstanceDetail);
- });
-
- it('does not call the register CI schema function', () => {
- expect(mockUse).not.toHaveBeenCalled();
- expect(mockRegisterCiSchema).not.toHaveBeenCalled();
- });
+ it('configures editor with syntax highlight', () => {
+ expect(mockUse).toHaveBeenCalledTimes(1);
+ expect(mockRegisterCiSchema).toHaveBeenCalledTimes(1);
});
});
});
diff --git a/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js b/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
index e54c72a758f..6a6cc3a14de 100644
--- a/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
+++ b/spec/frontend/ci/pipeline_editor/graphql/resolvers_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import { mockLintResponse } from '../mock_data';
@@ -20,7 +20,7 @@ describe('~/ci/pipeline_editor/graphql/resolvers', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
- mock.onPost(endpoint).reply(httpStatus.OK, mockLintResponse);
+ mock.onPost(endpoint).reply(HTTP_STATUS_OK, mockLintResponse);
result = await resolvers.Mutation.lintCI(null, {
endpoint,
diff --git a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
index 2360dd7d103..cd16045f92d 100644
--- a/spec/frontend/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
@@ -8,12 +8,16 @@ import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_help
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_BAD_REQUEST,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
-import PipelineNewForm from '~/pipeline_new/components/pipeline_new_form.vue';
-import ciConfigVariablesQuery from '~/pipeline_new/graphql/queries/ci_config_variables.graphql';
-import { resolvers } from '~/pipeline_new/graphql/resolvers';
-import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue';
+import PipelineNewForm from '~/ci/pipeline_new/components/pipeline_new_form.vue';
+import ciConfigVariablesQuery from '~/ci/pipeline_new/graphql/queries/ci_config_variables.graphql';
+import { resolvers } from '~/ci/pipeline_new/graphql/resolvers';
+import RefsDropdown from '~/ci/pipeline_new/components/refs_dropdown.vue';
import {
mockCreditCardValidationRequiredError,
mockCiConfigVariablesResponse,
@@ -108,7 +112,7 @@ describe('Pipeline New Form', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
mockCiConfigVariables = jest.fn();
- mock.onGet(projectRefsEndpoint).reply(httpStatusCodes.OK, mockRefs);
+ mock.onGet(projectRefsEndpoint).reply(HTTP_STATUS_OK, mockRefs);
dummySubmitEvent = {
preventDefault: jest.fn(),
@@ -173,7 +177,7 @@ describe('Pipeline New Form', () => {
describe('Pipeline creation', () => {
beforeEach(async () => {
mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
- mock.onPost(pipelinesPath).reply(httpStatusCodes.OK, newPipelinePostResponse);
+ mock.onPost(pipelinesPath).reply(HTTP_STATUS_OK, newPipelinePostResponse);
});
it('does not submit the native HTML form', async () => {
@@ -365,7 +369,7 @@ describe('Pipeline New Form', () => {
beforeEach(() => {
mock
.onGet(projectRefsEndpoint, { params: { search: '' } })
- .reply(httpStatusCodes.INTERNAL_SERVER_ERROR);
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
findRefsDropdown().vm.$emit('loadingError');
});
@@ -378,7 +382,7 @@ describe('Pipeline New Form', () => {
describe('when the error response can be handled', () => {
beforeEach(async () => {
- mock.onPost(pipelinesPath).reply(httpStatusCodes.BAD_REQUEST, mockError);
+ mock.onPost(pipelinesPath).reply(HTTP_STATUS_BAD_REQUEST, mockError);
findForm().vm.$emit('submit', dummySubmitEvent);
@@ -416,7 +420,7 @@ describe('Pipeline New Form', () => {
beforeEach(async () => {
mock
.onPost(pipelinesPath)
- .reply(httpStatusCodes.BAD_REQUEST, mockCreditCardValidationRequiredError);
+ .reply(HTTP_STATUS_BAD_REQUEST, mockCreditCardValidationRequiredError);
window.gon = {
subscriptions_url: TEST_HOST,
@@ -449,9 +453,7 @@ describe('Pipeline New Form', () => {
describe('when the error response cannot be handled', () => {
beforeEach(async () => {
- mock
- .onPost(pipelinesPath)
- .reply(httpStatusCodes.INTERNAL_SERVER_ERROR, 'something went wrong');
+ mock.onPost(pipelinesPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, 'something went wrong');
findForm().vm.$emit('submit', dummySubmitEvent);
diff --git a/spec/frontend/pipeline_new/components/refs_dropdown_spec.js b/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
index 8cba876c688..cf8009e388f 100644
--- a/spec/frontend/pipeline_new/components/refs_dropdown_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/refs_dropdown_spec.js
@@ -1,13 +1,13 @@
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlListbox, GlListboxItem } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
-import RefsDropdown from '~/pipeline_new/components/refs_dropdown.vue';
+import RefsDropdown from '~/ci/pipeline_new/components/refs_dropdown.vue';
-import { mockRefs, mockFilteredRefs } from '../mock_data';
+import { mockBranches, mockRefs, mockFilteredRefs, mockTags } from '../mock_data';
const projectRefsEndpoint = '/root/project/refs';
const refShortName = 'main';
@@ -19,11 +19,12 @@ describe('Pipeline New Form', () => {
let wrapper;
let mock;
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findRefsDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findDropdown = () => wrapper.findComponent(GlListbox);
+ const findRefsDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
+ const findSearchBox = () => wrapper.findByTestId('listbox-search-input');
+ const findListboxGroups = () => wrapper.findAll('ul[role="group"]');
- const createComponent = (props = {}, mountFn = shallowMount) => {
+ const createComponent = (props = {}, mountFn = shallowMountExtended) => {
wrapper = mountFn(RefsDropdown, {
provide: {
projectRefsEndpoint,
@@ -40,22 +41,15 @@ describe('Pipeline New Form', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(projectRefsEndpoint, { params: { search: '' } }).reply(httpStatusCodes.OK, mockRefs);
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
-
- mock.restore();
+ mock.onGet(projectRefsEndpoint, { params: { search: '' } }).reply(HTTP_STATUS_OK, mockRefs);
});
beforeEach(() => {
createComponent();
});
- it('displays empty dropdown initially', async () => {
- await findDropdown().vm.$emit('show');
+ it('displays empty dropdown initially', () => {
+ findDropdown().vm.$emit('shown');
expect(findRefsDropdownItems()).toHaveLength(0);
});
@@ -66,19 +60,19 @@ describe('Pipeline New Form', () => {
describe('when user opens dropdown', () => {
beforeEach(async () => {
- await findDropdown().vm.$emit('show');
+ createComponent({}, mountExtended);
+ findDropdown().vm.$emit('shown');
await waitForPromises();
});
- it('requests unfiltered tags and branches', async () => {
+ it('requests unfiltered tags and branches', () => {
expect(mock.history.get).toHaveLength(1);
expect(mock.history.get[0].url).toBe(projectRefsEndpoint);
expect(mock.history.get[0].params).toEqual({ search: '' });
});
- it('displays dropdown with branches and tags', async () => {
+ it('displays dropdown with branches and tags', () => {
const refLength = mockRefs.Tags.length + mockRefs.Branches.length;
-
expect(findRefsDropdownItems()).toHaveLength(refLength);
});
@@ -99,7 +93,8 @@ describe('Pipeline New Form', () => {
const selectedIndex = 1;
beforeEach(async () => {
- await findRefsDropdownItems().at(selectedIndex).vm.$emit('click');
+ findRefsDropdownItems().at(selectedIndex).vm.$emit('select', 'refs/heads/branch-1');
+ await waitForPromises();
});
it('component emits @input', () => {
@@ -116,7 +111,7 @@ describe('Pipeline New Form', () => {
beforeEach(async () => {
mock
.onGet(projectRefsEndpoint, { params: { search: mockSearchTerm } })
- .reply(httpStatusCodes.OK, mockFilteredRefs);
+ .reply(HTTP_STATUS_OK, mockFilteredRefs);
await findSearchBox().vm.$emit('input', mockSearchTerm);
await waitForPromises();
@@ -147,20 +142,23 @@ describe('Pipeline New Form', () => {
.onGet(projectRefsEndpoint, {
params: { ref: mockFullName },
})
- .reply(httpStatusCodes.OK, mockRefs);
-
- createComponent({
- value: {
- shortName: mockShortName,
- fullName: mockFullName,
+ .reply(HTTP_STATUS_OK, mockRefs);
+
+ createComponent(
+ {
+ value: {
+ shortName: mockShortName,
+ fullName: mockFullName,
+ },
},
- });
- await findDropdown().vm.$emit('show');
+ mountExtended,
+ );
+ findDropdown().vm.$emit('shown');
await waitForPromises();
});
it('branch is checked', () => {
- expect(findRefsDropdownItems().at(selectedIndex).props('isChecked')).toBe(true);
+ expect(findRefsDropdownItems().at(selectedIndex).props('isSelected')).toBe(true);
});
});
@@ -168,9 +166,9 @@ describe('Pipeline New Form', () => {
beforeEach(async () => {
mock
.onGet(projectRefsEndpoint, { params: { search: '' } })
- .reply(httpStatusCodes.INTERNAL_SERVER_ERROR);
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
- await findDropdown().vm.$emit('show');
+ findDropdown().vm.$emit('shown');
await waitForPromises();
});
@@ -179,4 +177,25 @@ describe('Pipeline New Form', () => {
expect(wrapper.emitted('loadingError')[0]).toEqual([expect.any(Error)]);
});
});
+
+ describe('should display branches and tags based on its length', () => {
+ it.each`
+ mockData | expectedGroupLength | expectedListboxItemsLength
+ ${{ ...mockBranches, Tags: [] }} | ${1} | ${mockBranches.Branches.length}
+ ${{ Branches: [], ...mockTags }} | ${1} | ${mockTags.Tags.length}
+ ${{ ...mockRefs }} | ${2} | ${mockBranches.Branches.length + mockTags.Tags.length}
+ ${{ Branches: undefined, Tags: undefined }} | ${0} | ${0}
+ `(
+ 'should render branches and tags based on presence',
+ async ({ mockData, expectedGroupLength, expectedListboxItemsLength }) => {
+ mock.onGet(projectRefsEndpoint, { params: { search: '' } }).reply(HTTP_STATUS_OK, mockData);
+ createComponent({}, mountExtended);
+ findDropdown().vm.$emit('shown');
+ await waitForPromises();
+
+ expect(findListboxGroups()).toHaveLength(expectedGroupLength);
+ expect(findRefsDropdownItems()).toHaveLength(expectedListboxItemsLength);
+ },
+ );
+ });
});
diff --git a/spec/frontend/pipeline_new/mock_data.js b/spec/frontend/ci/pipeline_new/mock_data.js
index 2af0ef4d7c4..dfb643a0ba4 100644
--- a/spec/frontend/pipeline_new/mock_data.js
+++ b/spec/frontend/ci/pipeline_new/mock_data.js
@@ -1,8 +1,16 @@
-export const mockRefs = {
+export const mockBranches = {
Branches: ['main', 'branch-1', 'branch-2'],
+};
+
+export const mockTags = {
Tags: ['1.0.0', '1.1.0', '1.2.0'],
};
+export const mockRefs = {
+ ...mockBranches,
+ ...mockTags,
+};
+
export const mockFilteredRefs = {
Branches: ['branch-1'],
Tags: ['1.0.0', '1.1.0'],
diff --git a/spec/frontend/pipeline_new/utils/filter_variables_spec.js b/spec/frontend/ci/pipeline_new/utils/filter_variables_spec.js
index 42bc6244456..d1b89704b58 100644
--- a/spec/frontend/pipeline_new/utils/filter_variables_spec.js
+++ b/spec/frontend/ci/pipeline_new/utils/filter_variables_spec.js
@@ -1,4 +1,4 @@
-import filterVariables from '~/pipeline_new/utils/filter_variables';
+import filterVariables from '~/ci/pipeline_new/utils/filter_variables';
import { mockVariables } from '../mock_data';
describe('Filter variables utility function', () => {
diff --git a/spec/frontend/ci/pipeline_new/utils/format_refs_spec.js b/spec/frontend/ci/pipeline_new/utils/format_refs_spec.js
new file mode 100644
index 00000000000..137a9339649
--- /dev/null
+++ b/spec/frontend/ci/pipeline_new/utils/format_refs_spec.js
@@ -0,0 +1,82 @@
+import { BRANCH_REF_TYPE, TAG_REF_TYPE } from '~/ci/pipeline_new/constants';
+import {
+ formatRefs,
+ formatListBoxItems,
+ searchByFullNameInListboxOptions,
+} from '~/ci/pipeline_new/utils/format_refs';
+import { mockBranchRefs, mockTagRefs } from '../mock_data';
+
+describe('Format refs util', () => {
+ it('formats branch ref correctly', () => {
+ expect(formatRefs(mockBranchRefs, BRANCH_REF_TYPE)).toEqual([
+ { fullName: 'refs/heads/main', shortName: 'main' },
+ { fullName: 'refs/heads/dev', shortName: 'dev' },
+ { fullName: 'refs/heads/release', shortName: 'release' },
+ ]);
+ });
+
+ it('formats tag ref correctly', () => {
+ expect(formatRefs(mockTagRefs, TAG_REF_TYPE)).toEqual([
+ { fullName: 'refs/tags/1.0.0', shortName: '1.0.0' },
+ { fullName: 'refs/tags/1.1.0', shortName: '1.1.0' },
+ { fullName: 'refs/tags/1.2.0', shortName: '1.2.0' },
+ ]);
+ });
+});
+
+describe('formatListBoxItems', () => {
+ it('formats branches and tags to listbox items correctly', () => {
+ expect(formatListBoxItems(mockBranchRefs, mockTagRefs)).toEqual([
+ {
+ text: 'Branches',
+ options: [
+ { value: 'refs/heads/main', text: 'main' },
+ { value: 'refs/heads/dev', text: 'dev' },
+ { value: 'refs/heads/release', text: 'release' },
+ ],
+ },
+ {
+ text: 'Tags',
+ options: [
+ { value: 'refs/tags/1.0.0', text: '1.0.0' },
+ { value: 'refs/tags/1.1.0', text: '1.1.0' },
+ { value: 'refs/tags/1.2.0', text: '1.2.0' },
+ ],
+ },
+ ]);
+
+ expect(formatListBoxItems(mockBranchRefs, [])).toEqual([
+ {
+ text: 'Branches',
+ options: [
+ { value: 'refs/heads/main', text: 'main' },
+ { value: 'refs/heads/dev', text: 'dev' },
+ { value: 'refs/heads/release', text: 'release' },
+ ],
+ },
+ ]);
+
+ expect(formatListBoxItems([], mockTagRefs)).toEqual([
+ {
+ text: 'Tags',
+ options: [
+ { value: 'refs/tags/1.0.0', text: '1.0.0' },
+ { value: 'refs/tags/1.1.0', text: '1.1.0' },
+ { value: 'refs/tags/1.2.0', text: '1.2.0' },
+ ],
+ },
+ ]);
+ });
+});
+
+describe('searchByFullNameInListboxOptions', () => {
+ const listbox = formatListBoxItems(mockBranchRefs, mockTagRefs);
+
+ it.each`
+ fullName | expectedResult
+ ${'refs/heads/main'} | ${{ fullName: 'refs/heads/main', shortName: 'main' }}
+ ${'refs/tags/1.0.0'} | ${{ fullName: 'refs/tags/1.0.0', shortName: '1.0.0' }}
+ `('should search item in listbox correctly', ({ fullName, expectedResult }) => {
+ expect(searchByFullNameInListboxOptions(fullName, listbox)).toEqual(expectedResult);
+ });
+});
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 4aa4cdf89a1..611993556e3 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, GlLoadingIcon, GlTabs } from '@gitlab/ui';
+import { GlAlert, GlEmptyState, GlLink, GlLoadingIcon, GlTabs } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { trimText } from 'helpers/text_helper';
@@ -10,13 +10,16 @@ import DeletePipelineScheduleModal from '~/ci/pipeline_schedules/components/dele
import TakeOwnershipModal from '~/ci/pipeline_schedules/components/take_ownership_modal.vue';
import PipelineSchedulesTable from '~/ci/pipeline_schedules/components/table/pipeline_schedules_table.vue';
import deletePipelineScheduleMutation from '~/ci/pipeline_schedules/graphql/mutations/delete_pipeline_schedule.mutation.graphql';
+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 {
mockGetPipelineSchedulesGraphQLResponse,
mockPipelineScheduleNodes,
deleteMutationResponse,
+ playMutationResponse,
takeOwnershipMutationResponse,
+ emptyPipelineSchedulesResponse,
} from '../mock_data';
Vue.use(VueApollo);
@@ -29,10 +32,13 @@ describe('Pipeline schedules app', () => {
let wrapper;
const successHandler = jest.fn().mockResolvedValue(mockGetPipelineSchedulesGraphQLResponse);
+ const successEmptyHandler = jest.fn().mockResolvedValue(emptyPipelineSchedulesResponse);
const failedHandler = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const deleteMutationHandlerSuccess = jest.fn().mockResolvedValue(deleteMutationResponse);
const deleteMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
+ const playMutationHandlerSuccess = jest.fn().mockResolvedValue(playMutationResponse);
+ const playMutationHandlerFailed = jest.fn().mockRejectedValue(new Error('GraphQL error'));
const takeOwnershipMutationHandlerSuccess = jest
.fn()
.mockResolvedValue(takeOwnershipMutationResponse);
@@ -60,14 +66,18 @@ describe('Pipeline schedules app', () => {
const findTable = () => wrapper.findComponent(PipelineSchedulesTable);
const findAlert = () => wrapper.findComponent(GlAlert);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findDeleteModal = () => wrapper.findComponent(DeletePipelineScheduleModal);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findTakeOwnershipModal = () => wrapper.findComponent(TakeOwnershipModal);
const findTabs = () => wrapper.findComponent(GlTabs);
+ const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+ const findLink = () => wrapper.findComponent(GlLink);
const findNewButton = () => wrapper.findByTestId('new-schedule-button');
const findAllTab = () => wrapper.findByTestId('pipeline-schedules-all-tab');
const findActiveTab = () => wrapper.findByTestId('pipeline-schedules-active-tab');
const findInactiveTab = () => wrapper.findByTestId('pipeline-schedules-inactive-tab');
+ const findSchedulesCharacteristics = () =>
+ wrapper.findByTestId('pipeline-schedules-characteristics');
afterEach(() => {
wrapper.destroy();
@@ -181,6 +191,45 @@ describe('Pipeline schedules app', () => {
});
});
+ describe('playing a pipeline schedule', () => {
+ it('shows play mutation error alert', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [playPipelineScheduleMutation, playMutationHandlerFailed],
+ ]);
+
+ await waitForPromises();
+
+ findTable().vm.$emit('playPipelineSchedule');
+
+ await waitForPromises();
+
+ expect(findAlert().text()).toBe('There was a problem playing the pipeline schedule.');
+ });
+
+ it('plays pipeline schedule', async () => {
+ createComponent([
+ [getPipelineSchedulesQuery, successHandler],
+ [playPipelineScheduleMutation, playMutationHandlerSuccess],
+ ]);
+
+ await waitForPromises();
+
+ const scheduleId = mockPipelineScheduleNodes[0].id;
+
+ findTable().vm.$emit('playPipelineSchedule', scheduleId);
+
+ await waitForPromises();
+
+ expect(playMutationHandlerSuccess).toHaveBeenCalledWith({
+ id: scheduleId,
+ });
+ expect(findAlert().text()).toBe(
+ 'Successfully scheduled a pipeline to run. Go to the Pipelines page for details.',
+ );
+ });
+ });
+
describe('taking ownership of a pipeline schedule', () => {
it('shows take ownership mutation error alert', async () => {
createComponent([
@@ -277,4 +326,24 @@ describe('Pipeline schedules app', () => {
expect(wrapper.vm.$apollo.queries.schedules.refetch).toHaveBeenCalledTimes(1);
});
});
+
+ describe('Empty pipeline schedules response', () => {
+ it('should show an empty state', async () => {
+ createComponent([[getPipelineSchedulesQuery, successEmptyHandler]]);
+
+ await waitForPromises();
+
+ const schedulesCharacteristics = findSchedulesCharacteristics();
+
+ expect(findEmptyState().exists()).toBe(true);
+ expect(schedulesCharacteristics.text()).toContain('Runs for a specific branch or tag.');
+ expect(schedulesCharacteristics.text()).toContain('Can have custom CI/CD variables.');
+ expect(schedulesCharacteristics.text()).toContain(
+ 'Runs with the same project permissions as the schedule owner.',
+ );
+
+ expect(findLink().exists()).toBe(true);
+ expect(findLink().text()).toContain('scheduled pipelines documentation.');
+ });
+ });
});
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
index 3364c61d155..6fb6a8bc33b 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_actions_spec.js
@@ -25,6 +25,7 @@ describe('Pipeline schedule actions', () => {
const findAllButtons = () => wrapper.findAllComponents(GlButton);
const findDeleteBtn = () => wrapper.findByTestId('delete-pipeline-schedule-btn');
const findTakeOwnershipBtn = () => wrapper.findByTestId('take-ownership-pipeline-schedule-btn');
+ const findPlayScheduleBtn = () => wrapper.findByTestId('play-pipeline-schedule-btn');
afterEach(() => {
wrapper.destroy();
@@ -61,4 +62,14 @@ describe('Pipeline schedule actions', () => {
showTakeOwnershipModal: [[mockTakeOwnershipNodes[0].id]],
});
});
+
+ it('play button emits playPipelineSchedule event and schedule id', () => {
+ createComponent();
+
+ findPlayScheduleBtn().vm.$emit('click');
+
+ expect(wrapper.emitted()).toEqual({
+ playPipelineSchedule: [[mockPipelineScheduleNodes[0].id]],
+ });
+ });
});
diff --git a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
index 17bf465baf3..0821c59c8a0 100644
--- a/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
+++ b/spec/frontend/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline_spec.js
@@ -1,5 +1,5 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import PipelineScheduleLastPipeline from '~/ci/pipeline_schedules/components/table/cells/pipeline_schedule_last_pipeline.vue';
import { mockPipelineScheduleNodes } from '../../../mock_data';
@@ -18,7 +18,7 @@ describe('Pipeline schedule last pipeline', () => {
});
};
- const findCIBadge = () => wrapper.findComponent(CiBadge);
+ const findCIBadgeLink = () => wrapper.findComponent(CiBadgeLink);
const findStatusText = () => wrapper.findByTestId('pipeline-schedule-status-text');
afterEach(() => {
@@ -28,8 +28,10 @@ describe('Pipeline schedule last pipeline', () => {
it('displays pipeline status', () => {
createComponent();
- expect(findCIBadge().exists()).toBe(true);
- expect(findCIBadge().props('status')).toBe(defaultProps.schedule.lastPipeline.detailedStatus);
+ expect(findCIBadgeLink().exists()).toBe(true);
+ expect(findCIBadgeLink().props('status')).toBe(
+ defaultProps.schedule.lastPipeline.detailedStatus,
+ );
expect(findStatusText().exists()).toBe(false);
});
@@ -37,6 +39,6 @@ describe('Pipeline schedule last pipeline', () => {
createComponent({ schedule: mockPipelineScheduleNodes[0] });
expect(findStatusText().text()).toBe('None');
- expect(findCIBadge().exists()).toBe(false);
+ expect(findCIBadgeLink().exists()).toBe(false);
});
});
diff --git a/spec/frontend/ci/pipeline_schedules/mock_data.js b/spec/frontend/ci/pipeline_schedules/mock_data.js
index 3010f1d06c3..2826c054249 100644
--- a/spec/frontend/ci/pipeline_schedules/mock_data.js
+++ b/spec/frontend/ci/pipeline_schedules/mock_data.js
@@ -32,6 +32,14 @@ export const mockPipelineScheduleNodes = nodes;
export const mockPipelineScheduleAsGuestNodes = guestNodes;
export const mockTakeOwnershipNodes = takeOwnershipNodes;
+export const emptyPipelineSchedulesResponse = {
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/1',
+ pipelineSchedules: { nodes: [], count: 0 },
+ },
+ },
+};
export const deleteMutationResponse = {
data: {
@@ -43,6 +51,16 @@ export const deleteMutationResponse = {
},
};
+export const playMutationResponse = {
+ data: {
+ pipelineSchedulePlay: {
+ clientMutationId: null,
+ errors: [],
+ __typename: 'PipelineSchedulePlayPayload',
+ },
+ },
+};
+
export const takeOwnershipMutationResponse = {
data: {
pipelineScheduleTakeOwnership: {
diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index cb46c668930..0ecafdd7d83 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -13,12 +13,12 @@ import RegistrationTokenResetDropdownItem from '~/ci/runner/components/registrat
import { INSTANCE_TYPE, GROUP_TYPE, PROJECT_TYPE } from '~/ci/runner/constants';
-import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
-import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
+import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/get_runner_platforms.query.graphql';
+import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/get_runner_setup.query.graphql';
import {
- mockGraphqlRunnerPlatforms,
- mockGraphqlInstructions,
+ mockRunnerPlatforms,
+ mockInstructions,
} from 'jest/vue_shared/components/runner_instructions/mock_data';
const mockToken = '0123456789';
@@ -67,8 +67,8 @@ describe('RegistrationDropdown', () => {
const createComponentWithModal = () => {
const requestHandlers = [
- [getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms)],
- [getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockGraphqlInstructions)],
+ [getRunnerPlatformsQuery, jest.fn().mockResolvedValue(mockRunnerPlatforms)],
+ [getRunnerSetupInstructionsQuery, jest.fn().mockResolvedValue(mockInstructions)],
];
createComponent(
diff --git a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js b/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js
deleted file mode 100644
index e9966576cab..00000000000
--- a/spec/frontend/ci_variable_list/components/ci_environments_dropdown_spec.js
+++ /dev/null
@@ -1,139 +0,0 @@
-import { GlDropdown, GlDropdownItem, GlIcon, GlSearchBoxByType } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { allEnvironments } from '~/ci_variable_list/constants';
-import CiEnvironmentsDropdown from '~/ci_variable_list/components/ci_environments_dropdown.vue';
-
-describe('Ci environments dropdown', () => {
- let wrapper;
-
- const envs = ['dev', 'prod', 'staging'];
- const defaultProps = { environments: envs, selectedEnvironmentScope: '' };
-
- const findDropdownText = () => wrapper.findComponent(GlDropdown).text();
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
- const findActiveIconByIndex = (index) => findDropdownItemByIndex(index).findComponent(GlIcon);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
-
- const createComponent = ({ props = {}, searchTerm = '' } = {}) => {
- wrapper = mount(CiEnvironmentsDropdown, {
- propsData: {
- ...defaultProps,
- ...props,
- },
- });
-
- findSearchBox().vm.$emit('input', searchTerm);
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('No environments found', () => {
- beforeEach(() => {
- createComponent({ searchTerm: 'stable' });
- });
-
- it('renders create button with search term if environments do not contain search term', () => {
- expect(findAllDropdownItems()).toHaveLength(2);
- expect(findDropdownItemByIndex(1).text()).toBe('Create wildcard: stable');
- });
-
- it('renders empty results message', () => {
- expect(findDropdownItemByIndex(0).text()).toBe('No matching results');
- });
- });
-
- describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent({ props: { environments: envs } });
- });
-
- it('renders all environments when search term is empty', () => {
- expect(findAllDropdownItems()).toHaveLength(3);
- expect(findDropdownItemByIndex(0).text()).toBe(envs[0]);
- expect(findDropdownItemByIndex(1).text()).toBe(envs[1]);
- expect(findDropdownItemByIndex(2).text()).toBe(envs[2]);
- });
-
- it('should not display active checkmark on the inactive stage', () => {
- expect(findActiveIconByIndex(0).classes('gl-visibility-hidden')).toBe(true);
- });
- });
-
- describe('when `*` is the value of selectedEnvironmentScope props', () => {
- const wildcardScope = '*';
-
- beforeEach(() => {
- createComponent({ props: { selectedEnvironmentScope: wildcardScope } });
- });
-
- it('shows the `All environments` text and not the wildcard', () => {
- expect(findDropdownText()).toContain(allEnvironments.text);
- expect(findDropdownText()).not.toContain(wildcardScope);
- });
- });
-
- describe('Environments found', () => {
- const currentEnv = envs[2];
-
- beforeEach(async () => {
- createComponent({ searchTerm: currentEnv });
- await nextTick();
- });
-
- it('renders only the environment searched for', () => {
- expect(findAllDropdownItems()).toHaveLength(1);
- expect(findDropdownItemByIndex(0).text()).toBe(currentEnv);
- });
-
- it('should not display create button', () => {
- const environments = findAllDropdownItems().filter((env) => env.text().startsWith('Create'));
- expect(environments).toHaveLength(0);
- expect(findAllDropdownItems()).toHaveLength(1);
- });
-
- it('should not display empty results message', () => {
- expect(wrapper.findComponent({ ref: 'noMatchingResults' }).exists()).toBe(false);
- });
-
- it('should clear the search term when showing the dropdown', () => {
- wrapper.findComponent(GlDropdown).trigger('click');
-
- expect(findSearchBox().text()).toBe('');
- });
-
- describe('Custom events', () => {
- describe('when clicking on an environment', () => {
- const itemIndex = 0;
-
- beforeEach(() => {
- createComponent();
- });
-
- it('should emit `select-environment` if an environment is clicked', async () => {
- await nextTick();
-
- await findDropdownItemByIndex(itemIndex).vm.$emit('click');
-
- expect(wrapper.emitted('select-environment')).toEqual([[envs[itemIndex]]]);
- });
- });
-
- describe('when creating a new environment from a search term', () => {
- const search = 'new-env';
- beforeEach(() => {
- createComponent({ searchTerm: search });
- });
-
- it('should emit createClicked if an environment is clicked', async () => {
- await nextTick();
- findDropdownItemByIndex(1).vm.$emit('click');
- expect(wrapper.emitted('create-environment-scope')).toEqual([[search]]);
- });
- });
- });
- });
-});
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index d89a238105b..6865b721441 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -7,7 +7,11 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import PipelinesTable from '~/commit/pipelines/pipelines_table.vue';
-import httpStatusCodes from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_BAD_REQUEST,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_UNAUTHORIZED,
+} from '~/lib/utils/http_status';
import { createAlert } from '~/flash';
import { TOAST_MESSAGE } from '~/pipelines/constants';
import axios from '~/lib/utils/axios_utils';
@@ -243,10 +247,10 @@ describe('Pipelines table in Commits and Merge requests', () => {
'An error occurred while trying to run a new pipeline for this merge request.';
it.each`
- status | message
- ${httpStatusCodes.BAD_REQUEST} | ${defaultMsg}
- ${httpStatusCodes.UNAUTHORIZED} | ${permissionsMsg}
- ${httpStatusCodes.INTERNAL_SERVER_ERROR} | ${defaultMsg}
+ status | message
+ ${HTTP_STATUS_BAD_REQUEST} | ${defaultMsg}
+ ${HTTP_STATUS_UNAUTHORIZED} | ${permissionsMsg}
+ ${HTTP_STATUS_INTERNAL_SERVER_ERROR} | ${defaultMsg}
`('displays permissions error message', async ({ status, message }) => {
const response = { response: { status } };
diff --git a/spec/frontend/constants_spec.js b/spec/frontend/constants_spec.js
new file mode 100644
index 00000000000..b596b62f72c
--- /dev/null
+++ b/spec/frontend/constants_spec.js
@@ -0,0 +1,30 @@
+import * as constants from '~/constants';
+
+describe('Global JS constants', () => {
+ describe('getModifierKey()', () => {
+ afterEach(() => {
+ delete window.gl;
+ });
+
+ it.each`
+ isMac | removeSuffix | expectedKey
+ ${true} | ${false} | ${'⌘'}
+ ${false} | ${false} | ${'Ctrl+'}
+ ${true} | ${true} | ${'⌘'}
+ ${false} | ${true} | ${'Ctrl'}
+ `(
+ 'returns correct keystroke when isMac=$isMac and removeSuffix=$removeSuffix',
+ ({ isMac, removeSuffix, expectedKey }) => {
+ Object.assign(window, {
+ gl: {
+ client: {
+ isMac,
+ },
+ },
+ });
+
+ expect(constants.getModifierKey(removeSuffix)).toBe(expectedKey);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
index 3ebb305afbf..5a725ac1ca4 100644
--- a/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_text_style_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import EditorStateObserver from '~/content_editor/components/editor_state_observer.vue';
import ToolbarTextStyleDropdown from '~/content_editor/components/toolbar_text_style_dropdown.vue';
@@ -22,8 +22,6 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
const buildWrapper = (propsData = {}) => {
wrapper = shallowMountExtended(ToolbarTextStyleDropdown, {
stubs: {
- GlDropdown,
- GlDropdownItem,
EditorStateObserver,
},
provide: {
@@ -35,7 +33,7 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
},
});
};
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
beforeEach(() => {
buildEditor();
@@ -48,9 +46,10 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
it('renders all text styles as dropdown items', () => {
buildWrapper();
- TEXT_STYLE_DROPDOWN_ITEMS.forEach((textStyle) => {
- expect(wrapper.findByText(textStyle.label).exists()).toBe(true);
+ TEXT_STYLE_DROPDOWN_ITEMS.forEach((textStyle, index) => {
+ expect(findListbox().props('items').at(index).text).toContain(textStyle.label);
});
+ expect(findListbox().props('items').length).toBe(TEXT_STYLE_DROPDOWN_ITEMS.length);
});
describe('when there is an active item', () => {
@@ -69,19 +68,11 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
});
it('displays the active text style label as the dropdown toggle text', () => {
- expect(findDropdown().props().text).toBe(activeTextStyle.label);
+ expect(findListbox().props('toggleText')).toBe(activeTextStyle.label);
});
it('sets dropdown as enabled', () => {
- expect(findDropdown().props().disabled).toBe(false);
- });
-
- it('sets active item as active', () => {
- const activeItem = wrapper
- .findAllComponents(GlDropdownItem)
- .filter((item) => item.text() === activeTextStyle.label)
- .at(0);
- expect(activeItem.props().isChecked).toBe(true);
+ expect(findListbox().props('disabled')).toBe(false);
});
});
@@ -93,11 +84,11 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
});
it('sets dropdown as disabled', () => {
- expect(findDropdown().props().disabled).toBe(true);
+ expect(findListbox().props('disabled')).toBe(true);
});
it('sets dropdown toggle text to Text style', () => {
- expect(findDropdown().props().text).toBe('Text style');
+ expect(findListbox().props('toggleText')).toBe('Text style');
});
});
@@ -109,7 +100,7 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
const { editorCommand, commandParams } = textStyle;
const commands = mockChainedCommands(tiptapEditor, [editorCommand, 'focus', 'run']);
- wrapper.findAllComponents(GlDropdownItem).at(index).vm.$emit('click');
+ findListbox().vm.$emit('select', TEXT_STYLE_DROPDOWN_ITEMS[index].label);
expect(commands[editorCommand]).toHaveBeenCalledWith(commandParams || {});
expect(commands.focus).toHaveBeenCalled();
expect(commands.run).toHaveBeenCalled();
@@ -121,7 +112,7 @@ describe('content_editor/components/toolbar_text_style_dropdown', () => {
buildWrapper();
const { contentType, commandParams } = textStyle;
- wrapper.findAllComponents(GlDropdownItem).at(index).vm.$emit('click');
+ findListbox().vm.$emit('select', TEXT_STYLE_DROPDOWN_ITEMS[index].label);
expect(wrapper.emitted('execute')).toEqual([
[
{
diff --git a/spec/frontend/content_editor/extensions/attachment_spec.js b/spec/frontend/content_editor/extensions/attachment_spec.js
index d528096be34..6b804b3b4c6 100644
--- a/spec/frontend/content_editor/extensions/attachment_spec.js
+++ b/spec/frontend/content_editor/extensions/attachment_spec.js
@@ -8,7 +8,7 @@ import Video from '~/content_editor/extensions/video';
import Link from '~/content_editor/extensions/link';
import Loading from '~/content_editor/extensions/loading';
import { VARIANT_DANGER } from '~/flash';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import eventHubFactory from '~/helpers/event_hub_factory';
import { createTestEditor, createDocBuilder } from '../test_utils';
import {
@@ -132,7 +132,7 @@ describe('content_editor/extensions/attachment', () => {
};
beforeEach(() => {
- mock.onPost().reply(httpStatus.OK, successResponse);
+ mock.onPost().reply(HTTP_STATUS_OK, successResponse);
});
it('inserts a media content with src set to the encoded content and uploading true', async () => {
@@ -167,7 +167,7 @@ describe('content_editor/extensions/attachment', () => {
describe('when uploading request fails', () => {
beforeEach(() => {
- mock.onPost().reply(httpStatus.INTERNAL_SERVER_ERROR);
+ mock.onPost().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('resets the doc to original state', async () => {
@@ -209,7 +209,7 @@ describe('content_editor/extensions/attachment', () => {
};
beforeEach(() => {
- mock.onPost().reply(httpStatus.OK, successResponse);
+ mock.onPost().reply(HTTP_STATUS_OK, successResponse);
});
it('inserts a loading mark', async () => {
@@ -246,7 +246,7 @@ describe('content_editor/extensions/attachment', () => {
describe('when uploading request fails', () => {
beforeEach(() => {
- mock.onPost().reply(httpStatus.INTERNAL_SERVER_ERROR);
+ mock.onPost().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('resets the doc to orginal state', async () => {
diff --git a/spec/frontend/content_editor/extensions/link_spec.js b/spec/frontend/content_editor/extensions/link_spec.js
index bb841357d37..ead898554d1 100644
--- a/spec/frontend/content_editor/extensions/link_spec.js
+++ b/spec/frontend/content_editor/extensions/link_spec.js
@@ -33,7 +33,7 @@ describe('content_editor/extensions/link', () => {
${'documentation](readme.md'} | ${() => p('documentation](readme.md')}
${'http://example.com '} | ${() => p(link({ href: 'http://example.com' }, 'http://example.com'))}
${'https://example.com '} | ${() => p(link({ href: 'https://example.com' }, 'https://example.com'))}
- ${'www.example.com '} | ${() => p(link({ href: 'http://www.example.com' }, 'www.example.com'))}
+ ${'www.example.com '} | ${() => p(link({ href: 'www.example.com' }, 'www.example.com'))}
${'example.com/ab.html '} | ${() => p('example.com/ab.html')}
${'https://www.google.com '} | ${() => p(link({ href: 'https://www.google.com' }, 'https://www.google.com'))}
`('with input=$input, then should insert a $insertedNode', ({ input, insertedNode }) => {
diff --git a/spec/frontend/content_editor/markdown_processing_spec.js b/spec/frontend/content_editor/markdown_processing_spec.js
deleted file mode 100644
index 3930f47289a..00000000000
--- a/spec/frontend/content_editor/markdown_processing_spec.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import path from 'path';
-import { describeMarkdownProcessing } from 'jest/content_editor/markdown_processing_spec_helper';
-
-jest.mock('~/emoji');
-
-const markdownYamlPath = path.join(
- __dirname,
- '..',
- '..',
- 'fixtures',
- 'markdown',
- 'markdown_golden_master_examples.yml',
-);
-
-// See spec/fixtures/markdown/markdown_golden_master_examples.yml for documentation on how this spec works.
-describeMarkdownProcessing('CE markdown processing in ContentEditor', markdownYamlPath);
diff --git a/spec/frontend/content_editor/markdown_processing_spec_helper.js b/spec/frontend/content_editor/markdown_processing_spec_helper.js
deleted file mode 100644
index 6f10f294fb0..00000000000
--- a/spec/frontend/content_editor/markdown_processing_spec_helper.js
+++ /dev/null
@@ -1,92 +0,0 @@
-import fs from 'fs';
-import jsYaml from 'js-yaml';
-import { memoize } from 'lodash';
-import MockAdapter from 'axios-mock-adapter';
-import axios from 'axios';
-import { createContentEditor } from '~/content_editor';
-import httpStatus from '~/lib/utils/http_status';
-
-const getFocusedMarkdownExamples = memoize(
- () => process.env.FOCUSED_MARKDOWN_EXAMPLES?.split(',') || [],
-);
-
-const includeExample = ({ name }) => {
- const focusedMarkdownExamples = getFocusedMarkdownExamples();
- if (!focusedMarkdownExamples.length) {
- return true;
- }
- return focusedMarkdownExamples.includes(name);
-};
-
-const getPendingReason = (pendingStringOrObject) => {
- if (!pendingStringOrObject) {
- return null;
- }
- if (typeof pendingStringOrObject === 'string') {
- return pendingStringOrObject;
- }
- if (pendingStringOrObject.frontend) {
- return pendingStringOrObject.frontend;
- }
-
- return null;
-};
-
-const loadMarkdownApiExamples = (markdownYamlPath) => {
- const apiMarkdownYamlText = fs.readFileSync(markdownYamlPath);
- const apiMarkdownExampleObjects = jsYaml.safeLoad(apiMarkdownYamlText);
-
- return apiMarkdownExampleObjects
- .filter(includeExample)
- .map(({ name, pending, markdown, html }) => [
- name,
- { pendingReason: getPendingReason(pending), markdown, html },
- ]);
-};
-
-const testSerializesHtmlToMarkdownForElement = async ({ markdown, html }) => {
- const mock = new MockAdapter(axios);
-
- // Ignore any API requests from the suggestions plugin
- mock.onGet().reply(httpStatus.OK, []);
-
- const contentEditor = createContentEditor({
- // Overwrite renderMarkdown to always return this specific html
- renderMarkdown: () => html,
- });
-
- await contentEditor.setSerializedContent(markdown);
-
- // This serializes the ContentEditor document, which was based on the HTML, to markdown
- const serializedContent = contentEditor.getSerializedContent();
-
- // Assert that the markdown we ended up with after sending it through all the ContentEditor
- // plumbing matches the original markdown from the YAML.
- expect(serializedContent.trim()).toBe(markdown.trim());
-
- mock.restore();
-};
-
-// describeMarkdownProcesssing
-//
-// This is used to dynamically generate examples (for both CE and EE) to ensure
-// we generate same markdown that was provided to Markdown API.
-//
-// eslint-disable-next-line jest/no-export
-export const describeMarkdownProcessing = (description, markdownYamlPath) => {
- const examples = loadMarkdownApiExamples(markdownYamlPath);
-
- describe(description, () => {
- describe.each(examples)('%s', (name, { pendingReason, ...example }) => {
- const exampleName = 'correctly serializes HTML to markdown';
- if (pendingReason) {
- it.todo(`${exampleName}: ${pendingReason}`);
- return;
- }
-
- it(`${exampleName}`, async () => {
- await testSerializesHtmlToMarkdownForElement(example);
- });
- });
- });
-};
diff --git a/spec/frontend/content_editor/markdown_snapshot_spec.js b/spec/frontend/content_editor/markdown_snapshot_spec.js
index 146208bf8c7..fd64003420e 100644
--- a/spec/frontend/content_editor/markdown_snapshot_spec.js
+++ b/spec/frontend/content_editor/markdown_snapshot_spec.js
@@ -1,11 +1,96 @@
-import { describeMarkdownSnapshots } from 'jest/content_editor/markdown_snapshot_spec_helper';
-
-jest.mock('~/emoji');
-
// See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
// for documentation on this spec.
//
// NOTE: Unlike the backend markdown_snapshot_spec.rb which has a CE and EE version, there is only
// one version of this spec. This is because the frontend markdown rendering does not require EE-only
// backend features.
-describeMarkdownSnapshots('markdown example snapshots in ContentEditor');
+
+import jsYaml from 'js-yaml';
+import { pick } from 'lodash';
+import glfmExampleStatusYml from '../../../glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml';
+import markdownYml from '../../../glfm_specification/output_example_snapshots/markdown.yml';
+import htmlYml from '../../../glfm_specification/output_example_snapshots/html.yml';
+import prosemirrorJsonYml from '../../../glfm_specification/output_example_snapshots/prosemirror_json.yml';
+import {
+ IMPLEMENTATION_ERROR_MSG,
+ renderHtmlAndJsonForAllExamples,
+} from './render_html_and_json_for_all_examples';
+
+jest.mock('~/emoji');
+
+const filterExamples = (examples) => {
+ const focusedMarkdownExamples = process.env.FOCUSED_MARKDOWN_EXAMPLES?.split(',') || [];
+ if (!focusedMarkdownExamples.length) {
+ return examples;
+ }
+ return pick(examples, focusedMarkdownExamples);
+};
+
+const loadExamples = (yaml) => {
+ const examples = jsYaml.safeLoad(yaml, {});
+ return filterExamples(examples);
+};
+
+describe('markdown example snapshots in ContentEditor', () => {
+ let actualHtmlAndJsonExamples;
+ let skipRunningSnapshotWysiwygHtmlTests;
+ let skipRunningSnapshotProsemirrorJsonTests;
+
+ const exampleStatuses = loadExamples(glfmExampleStatusYml);
+ const markdownExamples = loadExamples(markdownYml);
+ const expectedHtmlExamples = loadExamples(htmlYml);
+ const expectedProseMirrorJsonExamples = loadExamples(prosemirrorJsonYml);
+ const exampleNames = Object.keys(markdownExamples);
+
+ beforeAll(async () => {
+ return renderHtmlAndJsonForAllExamples(markdownExamples).then((examples) => {
+ actualHtmlAndJsonExamples = examples;
+ });
+ });
+
+ describe.each(exampleNames)('%s', (name) => {
+ const exampleNamePrefix = 'verifies conversion of GLFM to';
+ skipRunningSnapshotWysiwygHtmlTests =
+ exampleStatuses[name]?.skip_running_snapshot_wysiwyg_html_tests;
+ skipRunningSnapshotProsemirrorJsonTests =
+ exampleStatuses[name]?.skip_running_snapshot_prosemirror_json_tests;
+
+ const markdown = markdownExamples[name];
+
+ if (skipRunningSnapshotWysiwygHtmlTests) {
+ it.todo(`${exampleNamePrefix} HTML: ${skipRunningSnapshotWysiwygHtmlTests}`);
+ } else {
+ it(`${exampleNamePrefix} HTML`, async () => {
+ const expectedHtml = expectedHtmlExamples[name].wysiwyg;
+ const { html: actualHtml } = actualHtmlAndJsonExamples[name];
+
+ // noinspection JSUnresolvedFunction (required to avoid RubyMine type inspection warning, because custom matchers auto-imported via Jest test setup are not automatically resolved - see https://youtrack.jetbrains.com/issue/WEB-42350/matcher-for-jest-is-not-recognized-but-it-is-runable)
+ expect(actualHtml).toMatchExpectedForMarkdown(
+ 'HTML',
+ name,
+ markdown,
+ IMPLEMENTATION_ERROR_MSG,
+ expectedHtml,
+ );
+ });
+ }
+
+ if (skipRunningSnapshotProsemirrorJsonTests) {
+ it.todo(`${exampleNamePrefix} ProseMirror JSON: ${skipRunningSnapshotProsemirrorJsonTests}`);
+ } else {
+ it(`${exampleNamePrefix} ProseMirror JSON`, async () => {
+ const expectedJson = expectedProseMirrorJsonExamples[name];
+ const { json: actualJson } = actualHtmlAndJsonExamples[name];
+
+ // noinspection JSUnresolvedFunction
+ expect(actualJson).toMatchExpectedForMarkdown(
+ 'JSON',
+ name,
+ markdown,
+ IMPLEMENTATION_ERROR_MSG,
+ expectedJson,
+ );
+ });
+ }
+ });
+});
diff --git a/spec/frontend/content_editor/markdown_snapshot_spec_helper.js b/spec/frontend/content_editor/markdown_snapshot_spec_helper.js
deleted file mode 100644
index 64988c5b717..00000000000
--- a/spec/frontend/content_editor/markdown_snapshot_spec_helper.js
+++ /dev/null
@@ -1,96 +0,0 @@
-// See https://docs.gitlab.com/ee/development/gitlab_flavored_markdown/specification_guide/#markdown-snapshot-testing
-// for documentation on this spec.
-
-import jsYaml from 'js-yaml';
-import { pick } from 'lodash';
-import glfmExampleStatusYml from '../../../glfm_specification/input/gitlab_flavored_markdown/glfm_example_status.yml';
-import markdownYml from '../../../glfm_specification/output_example_snapshots/markdown.yml';
-import htmlYml from '../../../glfm_specification/output_example_snapshots/html.yml';
-import prosemirrorJsonYml from '../../../glfm_specification/output_example_snapshots/prosemirror_json.yml';
-import {
- IMPLEMENTATION_ERROR_MSG,
- renderHtmlAndJsonForAllExamples,
-} from './render_html_and_json_for_all_examples';
-
-const filterExamples = (examples) => {
- const focusedMarkdownExamples = process.env.FOCUSED_MARKDOWN_EXAMPLES?.split(',') || [];
- if (!focusedMarkdownExamples.length) {
- return examples;
- }
- return pick(examples, focusedMarkdownExamples);
-};
-
-const loadExamples = (yaml) => {
- const examples = jsYaml.safeLoad(yaml, {});
- return filterExamples(examples);
-};
-
-// eslint-disable-next-line jest/no-export
-export const describeMarkdownSnapshots = (description) => {
- let actualHtmlAndJsonExamples;
- let skipRunningSnapshotWysiwygHtmlTests;
- let skipRunningSnapshotProsemirrorJsonTests;
-
- const exampleStatuses = loadExamples(glfmExampleStatusYml);
- const markdownExamples = loadExamples(markdownYml);
- const expectedHtmlExamples = loadExamples(htmlYml);
- const expectedProseMirrorJsonExamples = loadExamples(prosemirrorJsonYml);
-
- beforeAll(async () => {
- return renderHtmlAndJsonForAllExamples(markdownExamples).then((examples) => {
- actualHtmlAndJsonExamples = examples;
- });
- });
-
- describe(description, () => {
- const exampleNames = Object.keys(markdownExamples);
-
- describe.each(exampleNames)('%s', (name) => {
- const exampleNamePrefix = 'verifies conversion of GLFM to';
- skipRunningSnapshotWysiwygHtmlTests =
- exampleStatuses[name]?.skip_running_snapshot_wysiwyg_html_tests;
- skipRunningSnapshotProsemirrorJsonTests =
- exampleStatuses[name]?.skip_running_snapshot_prosemirror_json_tests;
-
- const markdown = markdownExamples[name];
-
- if (skipRunningSnapshotWysiwygHtmlTests) {
- it.todo(`${exampleNamePrefix} HTML: ${skipRunningSnapshotWysiwygHtmlTests}`);
- } else {
- it(`${exampleNamePrefix} HTML`, async () => {
- const expectedHtml = expectedHtmlExamples[name].wysiwyg;
- const { html: actualHtml } = actualHtmlAndJsonExamples[name];
-
- // noinspection JSUnresolvedFunction (required to avoid RubyMine type inspection warning, because custom matchers auto-imported via Jest test setup are not automatically resolved - see https://youtrack.jetbrains.com/issue/WEB-42350/matcher-for-jest-is-not-recognized-but-it-is-runable)
- expect(actualHtml).toMatchExpectedForMarkdown(
- 'HTML',
- name,
- markdown,
- IMPLEMENTATION_ERROR_MSG,
- expectedHtml,
- );
- });
- }
-
- if (skipRunningSnapshotProsemirrorJsonTests) {
- it.todo(
- `${exampleNamePrefix} ProseMirror JSON: ${skipRunningSnapshotProsemirrorJsonTests}`,
- );
- } else {
- it(`${exampleNamePrefix} ProseMirror JSON`, async () => {
- const expectedJson = expectedProseMirrorJsonExamples[name];
- const { json: actualJson } = actualHtmlAndJsonExamples[name];
-
- // noinspection JSUnresolvedFunction
- expect(actualJson).toMatchExpectedForMarkdown(
- 'JSON',
- name,
- markdown,
- IMPLEMENTATION_ERROR_MSG,
- expectedJson,
- );
- });
- }
- });
- });
-};
diff --git a/spec/frontend/content_editor/services/upload_helpers_spec.js b/spec/frontend/content_editor/services/upload_helpers_spec.js
index ee9333232db..3423e4db3dc 100644
--- a/spec/frontend/content_editor/services/upload_helpers_spec.js
+++ b/spec/frontend/content_editor/services/upload_helpers_spec.js
@@ -1,7 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { uploadFile } from '~/content_editor/services/upload_helpers';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('content_editor/services/upload_helpers', () => {
const uploadsPath = '/uploads';
@@ -26,7 +26,7 @@ describe('content_editor/services/upload_helpers', () => {
renderedMarkdown = parseHTML(renderedAttachmentLinkFixture);
mock = new MockAdapter(axios);
- mock.onPost(uploadsPath, formData).reply(httpStatus.OK, successResponse);
+ mock.onPost(uploadsPath, formData).reply(HTTP_STATUS_OK, successResponse);
renderMarkdown = jest.fn().mockResolvedValue(renderedAttachmentLinkFixture);
});
diff --git a/spec/frontend/deploy_freeze/store/mutations_spec.js b/spec/frontend/deploy_freeze/store/mutations_spec.js
index 984105d6655..a1e80ef0e6c 100644
--- a/spec/frontend/deploy_freeze/store/mutations_spec.js
+++ b/spec/frontend/deploy_freeze/store/mutations_spec.js
@@ -33,9 +33,9 @@ describe('Deploy freeze mutations', () => {
describe('RECEIVE_FREEZE_PERIODS_SUCCESS', () => {
it('should set freeze periods and format timezones from identifiers to names', () => {
const timezoneNames = {
- 'Europe/Berlin': '[UTC + 2] Berlin',
+ 'Europe/Berlin': '[UTC+2] Berlin',
'Etc/UTC': '[UTC 0] UTC',
- 'America/New_York': '[UTC - 4] Eastern Time (US & Canada)',
+ 'America/New_York': '[UTC-4] Eastern Time (US & Canada)',
};
mutations[types.RECEIVE_FREEZE_PERIODS_SUCCESS](stateCopy, freezePeriodsFixture);
diff --git a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
index 5fd61b25edc..f4d4f9cf896 100644
--- a/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
+++ b/spec/frontend/design_management/components/design_notes/design_reply_form_spec.js
@@ -5,6 +5,7 @@ import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_m
import DesignReplyForm from '~/design_management/components/design_notes/design_reply_form.vue';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+jest.mock('~/autosave');
describe('Design reply form component', () => {
let wrapper;
@@ -78,12 +79,11 @@ describe('Design reply form component', () => {
createComponent({ discussionId });
await nextTick();
- // We discourage testing `wrapper.vm` properties but
- // since `autosave` library instantiates on component
- // there's no other way to test whether instantiation
- // happened correctly or not.
- expect(wrapper.vm.autosaveDiscussion).toBeInstanceOf(Autosave);
- expect(wrapper.vm.autosaveDiscussion.key).toBe(`autosave/Discussion/6/${shortDiscussionId}`);
+ expect(Autosave).toHaveBeenCalledWith(expect.any(Element), [
+ 'Discussion',
+ 6,
+ shortDiscussionId,
+ ]);
},
);
@@ -141,7 +141,7 @@ describe('Design reply form component', () => {
});
it('emits submitForm event on Comment button click', async () => {
- const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
+ const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
findSubmitButton().vm.$emit('click');
@@ -151,7 +151,7 @@ describe('Design reply form component', () => {
});
it('emits submitForm event on textarea ctrl+enter keydown', async () => {
- const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
+ const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
findTextarea().trigger('keydown.enter', {
ctrlKey: true,
@@ -163,7 +163,7 @@ describe('Design reply form component', () => {
});
it('emits submitForm event on textarea meta+enter keydown', async () => {
- const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
+ const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
findTextarea().trigger('keydown.enter', {
metaKey: true,
@@ -178,7 +178,7 @@ describe('Design reply form component', () => {
findTextarea().setValue('test2');
await nextTick();
- expect(wrapper.emitted('input')).toEqual([['test'], ['test2']]);
+ expect(wrapper.emitted('input')).toEqual([['test2']]);
});
it('emits cancelForm event on Escape key if text was not changed', () => {
@@ -211,7 +211,7 @@ describe('Design reply form component', () => {
it('emits cancelForm event when confirmed', async () => {
confirmAction.mockResolvedValueOnce(true);
- const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
+ const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
wrapper.setProps({ value: 'test3' });
await nextTick();
@@ -228,7 +228,7 @@ describe('Design reply form component', () => {
it("doesn't emit cancelForm event when not confirmed", async () => {
confirmAction.mockResolvedValueOnce(false);
- const autosaveResetSpy = jest.spyOn(wrapper.vm.autosaveDiscussion, 'reset');
+ const autosaveResetSpy = jest.spyOn(Autosave.prototype, 'reset');
wrapper.setProps({ value: 'test3' });
await nextTick();
diff --git a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
index 1acbf14db88..a4af73dd194 100644
--- a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
+++ b/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
@@ -12,6 +12,7 @@ exports[`Design management design version dropdown component renders design vers
toggletext="Showing latest version"
variant="default"
>
+
<!---->
<!---->
@@ -24,6 +25,7 @@ exports[`Design management design version dropdown component renders design vers
tabindex="-1"
>
<gl-listbox-item-stub
+ data-testid="listbox-item-gid://gitlab/DesignManagement::Version/1"
ischeckcentered="true"
>
<span
@@ -66,6 +68,7 @@ exports[`Design management design version dropdown component renders design vers
</span>
</gl-listbox-item-stub>
<gl-listbox-item-stub
+ data-testid="listbox-item-gid://gitlab/DesignManagement::Version/2"
ischeckcentered="true"
>
<span
@@ -107,6 +110,10 @@ exports[`Design management design version dropdown component renders design vers
</span>
</span>
</gl-listbox-item-stub>
+
+ <!---->
+
+ <!---->
</ul>
<!---->
@@ -126,6 +133,7 @@ exports[`Design management design version dropdown component renders design vers
toggletext="Showing latest version"
variant="default"
>
+
<!---->
<!---->
@@ -138,6 +146,7 @@ exports[`Design management design version dropdown component renders design vers
tabindex="-1"
>
<gl-listbox-item-stub
+ data-testid="listbox-item-gid://gitlab/DesignManagement::Version/1"
ischeckcentered="true"
>
<span
@@ -180,6 +189,7 @@ exports[`Design management design version dropdown component renders design vers
</span>
</gl-listbox-item-stub>
<gl-listbox-item-stub
+ data-testid="listbox-item-gid://gitlab/DesignManagement::Version/2"
ischeckcentered="true"
>
<span
@@ -221,6 +231,10 @@ exports[`Design management design version dropdown component renders design vers
</span>
</span>
</gl-listbox-item-stub>
+
+ <!---->
+
+ <!---->
</ul>
<!---->
diff --git a/spec/frontend/diff_spec.js b/spec/frontend/diff_spec.js
new file mode 100644
index 00000000000..759ae32ac51
--- /dev/null
+++ b/spec/frontend/diff_spec.js
@@ -0,0 +1,72 @@
+import createEventHub from '~/helpers/event_hub_factory';
+
+import Diff from '~/diff';
+
+describe('Diff', () => {
+ describe('diff <-> tabs interactions', () => {
+ let hub;
+
+ beforeEach(() => {
+ hub = createEventHub();
+ });
+
+ describe('constructor', () => {
+ it("takes in the `mergeRequestEventHub` when it's provided", () => {
+ const diff = new Diff({ mergeRequestEventHub: hub });
+
+ expect(diff.mrHub).toBe(hub);
+ });
+
+ it('does not fatal if no event hub is provided', () => {
+ expect(() => {
+ new Diff(); /* eslint-disable-line no-new */
+ }).not.toThrow();
+ });
+
+ it("doesn't set the mrHub property if none is provided by the construction arguments", () => {
+ const diff = new Diff();
+
+ expect(diff.mrHub).toBe(undefined);
+ });
+ });
+
+ describe('viewTypeSwitch', () => {
+ const clickPath = '/path/somewhere?params=exist';
+ const jsonPath = 'http://test.host/path/somewhere.json?params=exist';
+ const simulatejQueryClick = {
+ originalEvent: {
+ target: {
+ getAttribute() {
+ return clickPath;
+ },
+ },
+ preventDefault: jest.fn(),
+ stopPropagation: jest.fn(),
+ },
+ };
+
+ it('emits the correct switch view event when called and there is an `mrHub`', async () => {
+ const diff = new Diff({ mergeRequestEventHub: hub });
+ const hubEmit = new Promise((resolve) => {
+ hub.$on('diff:switch-view-type', resolve);
+ });
+
+ diff.viewTypeSwitch(simulatejQueryClick);
+ const { source } = await hubEmit;
+
+ expect(simulatejQueryClick.originalEvent.preventDefault).toHaveBeenCalled();
+ expect(simulatejQueryClick.originalEvent.stopPropagation).toHaveBeenCalled();
+ expect(source).toBe(jsonPath);
+ });
+
+ it('is effectively a noop when there is no `mrHub`', () => {
+ const diff = new Diff();
+
+ expect(diff.mrHub).toBe(undefined);
+ expect(() => {
+ diff.viewTypeSwitch(simulatejQueryClick);
+ }).not.toThrow();
+ });
+ });
+ });
+});
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index 936f4744e94..c8be0bedb4c 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -107,6 +107,7 @@ describe('diffs/components/app', () => {
beforeEach(() => {
const fetchResolver = () => {
store.state.diffs.retrievingBatches = false;
+ store.state.notes.doneFetchingBatchDiscussions = true;
store.state.notes.discussions = 'test';
return Promise.resolve({ real_size: 100 });
};
diff --git a/spec/frontend/diffs/components/diff_file_spec.js b/spec/frontend/diffs/components/diff_file_spec.js
index 944cec77efb..ccfc36f8f16 100644
--- a/spec/frontend/diffs/components/diff_file_spec.js
+++ b/spec/frontend/diffs/components/diff_file_spec.js
@@ -18,7 +18,7 @@ import createDiffsStore from '~/diffs/store/modules';
import { diffViewerModes, diffViewerErrors } from '~/ide/constants';
import axios from '~/lib/utils/axios_utils';
import { scrollToElement } from '~/lib/utils/common_utils';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import createNotesStore from '~/notes/stores/modules';
import { getDiffFileMock } from '../mock_data/diff_file';
import diffFileMockDataUnreadable from '../mock_data/diff_file_unreadable';
@@ -436,7 +436,7 @@ describe('DiffFile', () => {
describe('loading', () => {
it('should have loading icon while loading a collapsed diffs', async () => {
const { load_collapsed_diff_url } = store.state.diffs.diffFiles[0];
- axiosMock.onGet(load_collapsed_diff_url).reply(httpStatus.OK, getReadableFile());
+ axiosMock.onGet(load_collapsed_diff_url).reply(HTTP_STATUS_OK, getReadableFile());
makeFileAutomaticallyCollapsed(store);
wrapper.vm.requestDiff();
@@ -517,7 +517,7 @@ describe('DiffFile', () => {
viewer: { name: 'collapsed', automaticallyCollapsed: true },
};
- axiosMock.onGet(file.load_collapsed_diff_url).reply(httpStatus.OK, getReadableFile());
+ axiosMock.onGet(file.load_collapsed_diff_url).reply(HTTP_STATUS_OK, getReadableFile());
({ wrapper, store } = createComponent({ file, props: { viewDiffsFileByFile: true } }));
diff --git a/spec/frontend/diffs/components/diff_line_note_form_spec.js b/spec/frontend/diffs/components/diff_line_note_form_spec.js
index 9493dc8855e..bd0e3455872 100644
--- a/spec/frontend/diffs/components/diff_line_note_form_spec.js
+++ b/spec/frontend/diffs/components/diff_line_note_form_spec.js
@@ -101,7 +101,8 @@ describe('DiffLineNoteForm', () => {
});
it('should init autosave', () => {
- expect(Autosave).toHaveBeenCalledWith({}, [
+ // we're using shallow mount here so there's no element to pass to Autosave
+ expect(Autosave).toHaveBeenCalledWith(undefined, [
'Note',
'Issue',
98,
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index 0fe70bac6b7..0f7926ccbf9 100644
--- a/spec/frontend/dropzone_input_spec.js
+++ b/spec/frontend/dropzone_input_spec.js
@@ -7,7 +7,7 @@ import { TEST_HOST } from 'spec/test_constants';
import PasteMarkdownTable from '~/behaviors/markdown/paste_markdown_table';
import dropzoneInput from '~/dropzone_input';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
const TEST_FILE = new File([], 'somefile.jpg');
TEST_FILE.upload = {};
@@ -92,7 +92,7 @@ describe('dropzone_input', () => {
],
});
- axiosMock.onPost().reply(httpStatusCodes.OK, { link: { markdown: 'foo' } });
+ axiosMock.onPost().reply(HTTP_STATUS_OK, { link: { markdown: 'foo' } });
await waitForPromises();
expect(axiosMock.history.post[0].data.get('file').name).toHaveLength(246);
});
@@ -131,7 +131,7 @@ describe('dropzone_input', () => {
},
],
});
- axiosMock.onPost().reply(httpStatusCodes.OK, { link: { markdown: 'foo' } });
+ axiosMock.onPost().reply(HTTP_STATUS_OK, { link: { markdown: 'foo' } });
await waitForPromises();
expect(axiosMock.history.post[0].data.get('file').name).toEqual('test.png');
});
diff --git a/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json b/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json
index 666a4852957..17a1b4474b6 100644
--- a/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json
+++ b/spec/frontend/editor/schema/ci/json_tests/positive_tests/gitlab-ci.json
@@ -107,7 +107,6 @@
"container_scanning": "scan2.json",
"dast": "dast.json",
"license_management": "license.json",
- "performance": "performance.json",
"metrics": "metrics.txt"
}
},
@@ -160,7 +159,6 @@
"container_scanning": ["scan2.json"],
"dast": ["dast.json"],
"license_management": ["license.json"],
- "performance": ["performance.json"],
"metrics": ["metrics.txt"]
}
},
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml
index 29f4a0cd76d..996a48f7bc6 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/artifacts.yml
@@ -1,5 +1,30 @@
-# invalid artifact:reports:cyclonedx
+# invalid artifact:reports:browser_performance
+browser_performance no paths:
+ artifacts:
+ reports:
+ browser_performance:
+
+## Lists (or globs) are not allowed!
+browser_performance list of string paths:
+ artifacts:
+ reports:
+ browser_performance:
+ - foo
+ - ./bar/baz
+
+browser_performance mixed list of string paths and globs:
+ artifacts:
+ reports:
+ browser_performance:
+ - ./foo
+ - "bar/*.baz"
+
+browser_performance string array:
+ artifacts:
+ reports:
+ browser_performance: ["foo", "blah"]
+# invalid artifact:reports:cyclonedx
cyclonedx no paths:
artifacts:
reports:
@@ -17,6 +42,19 @@ cyclonedx not an array or string:
- foo
- bar
+# invalid artifacts:reports:coverage_report
+coverage-report-is-string:
+ artifacts:
+ reports:
+ coverage_report: cobertura
+
+# invalid artifact:reports:performance
+# Superceded by: artifact:reports:browser_performance
+performance string path:
+ artifacts:
+ reports:
+ performance: foo
+
# invalid artifacts:when
artifacts-when-unknown:
artifacts:
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules.yml b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules.yml
index d74a681b23b..f4a08492574 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/negative_tests/rules.yml
@@ -12,3 +12,8 @@ wrong path declaration:
rules:
- changes:
paths: { file: 'DOCKER' }
+
+# invalid rules:if
+rules-if-empty:
+ rules:
+ - if: \ No newline at end of file
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml
index a5c9153ee13..70761a09b58 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/artifacts.yml
@@ -1,5 +1,10 @@
-# valid artifact:reports:cyclonedx
+# valid artifact:reports:browser_performance
+browser_performance string path:
+ artifacts:
+ reports:
+ browser_performance: foo
+# valid artifact:reports:cyclonedx
cyclonedx string path:
artifacts:
reports:
@@ -24,6 +29,19 @@ cylonedx mixed list of string paths and globs:
- ./foo
- "bar/*.baz"
+# valid artifacts:reports:coverage_report
+coverage-report-cobertura:
+ artifacts:
+ reports:
+ coverage_report:
+ coverage_format: cobertura
+ path: coverage/cobertura-coverage.xml
+
+coverage-report-null:
+ artifacts:
+ reports:
+ coverage_report: null
+
# valid artifacts:when
artifacts-when-on-failure:
artifacts:
diff --git a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml
index ef604f707b5..5dfaf323b22 100644
--- a/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml
+++ b/spec/frontend/editor/schema/ci/yaml_tests/positive_tests/rules.yml
@@ -28,3 +28,7 @@ workflow:
variables:
IS_A_FEATURE: 'true'
when: always
+
+# valid rules:null
+rules-null:
+ rules: null
diff --git a/spec/frontend/environments/environment_details/deployment_job_spec.js b/spec/frontend/environments/environment_details/deployment_job_spec.js
new file mode 100644
index 00000000000..9bb61abb293
--- /dev/null
+++ b/spec/frontend/environments/environment_details/deployment_job_spec.js
@@ -0,0 +1,49 @@
+import { GlTruncate, GlLink, GlBadge } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import DeploymentJob from '~/environments/environment_details/components/deployment_job.vue';
+
+describe('app/assets/javascripts/environments/environment_details/components/deployment_job.vue', () => {
+ const jobData = {
+ webPath: 'http://example.com',
+ label: 'example job',
+ };
+ let wrapper;
+
+ const createWrapper = ({ job }) => {
+ return mountExtended(DeploymentJob, {
+ propsData: {
+ job,
+ },
+ });
+ };
+
+ describe('when the job data exists', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ job: jobData });
+ });
+
+ it('should render a link with a correct href', () => {
+ const jobLink = wrapper.findComponent(GlLink);
+ expect(jobLink.exists()).toBe(true);
+ expect(jobLink.attributes().href).toBe(jobData.webPath);
+ });
+ it('should render a truncated label', () => {
+ const truncatedLabel = wrapper.findComponent(GlTruncate);
+ expect(truncatedLabel.exists()).toBe(true);
+ expect(truncatedLabel.props().text).toBe(jobData.label);
+ });
+ });
+
+ describe('when the job data does not exist', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ job: null });
+ });
+
+ it('should render a badge with the text "API"', () => {
+ const badge = wrapper.findComponent(GlBadge);
+ expect(badge.exists()).toBe(true);
+ expect(badge.props().variant).toBe('info');
+ expect(badge.text()).toBe('API');
+ });
+ });
+});
diff --git a/spec/frontend/environments/environment_details/deployment_status_link_spec.js b/spec/frontend/environments/environment_details/deployment_status_link_spec.js
new file mode 100644
index 00000000000..5db7740423a
--- /dev/null
+++ b/spec/frontend/environments/environment_details/deployment_status_link_spec.js
@@ -0,0 +1,57 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import DeploymentStatusLink from '~/environments/environment_details/components/deployment_status_link.vue';
+import DeploymentStatusBadge from '~/environments/components/deployment_status_badge.vue';
+
+describe('app/assets/javascripts/environments/environment_details/components/deployment_status_link.vue', () => {
+ const testData = {
+ webPath: 'http://example.com',
+ status: 'success',
+ };
+ let wrapper;
+
+ const createWrapper = (props) => {
+ return mountExtended(DeploymentStatusLink, {
+ propsData: props,
+ });
+ };
+
+ describe('when the job link exists', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({
+ deploymentJob: { webPath: testData.webPath },
+ status: testData.status,
+ });
+ });
+
+ it('should render a link with a correct href', () => {
+ const jobLink = wrapper.findByTestId('deployment-status-job-link');
+ expect(jobLink.exists()).toBe(true);
+ expect(jobLink.attributes().href).toBe(testData.webPath);
+ });
+
+ it('should render a status badge', () => {
+ const statusBadge = wrapper.findComponent(DeploymentStatusBadge);
+ expect(statusBadge.exists()).toBe(true);
+ expect(statusBadge.props().status).toBe(testData.status);
+ });
+ });
+
+ describe('when no deployment job is provided', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({
+ status: testData.status,
+ });
+ });
+
+ it('should render a link with a correct href', () => {
+ const jobLink = wrapper.findByTestId('deployment-status-job-link');
+ expect(jobLink.exists()).toBe(false);
+ });
+
+ it('should render only a status badge', () => {
+ const statusBadge = wrapper.findComponent(DeploymentStatusBadge);
+ expect(statusBadge.exists()).toBe(true);
+ expect(statusBadge.props().status).toBe(testData.status);
+ });
+ });
+});
diff --git a/spec/frontend/environments/environment_details/deployment_triggerer_spec.js b/spec/frontend/environments/environment_details/deployment_triggerer_spec.js
new file mode 100644
index 00000000000..48af82661bf
--- /dev/null
+++ b/spec/frontend/environments/environment_details/deployment_triggerer_spec.js
@@ -0,0 +1,51 @@
+import { GlAvatar, GlAvatarLink } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import DeploymentTriggerer from '~/environments/environment_details/components/deployment_triggerer.vue';
+
+describe('app/assets/javascripts/environments/environment_details/components/deployment_triggerer.vue', () => {
+ const triggererData = {
+ id: 'gid://gitlab/User/1',
+ webUrl: 'http://gdk.test:3000/root',
+ name: 'Administrator',
+ avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ };
+ let wrapper;
+
+ const createWrapper = ({ triggerer }) => {
+ return mountExtended(DeploymentTriggerer, {
+ propsData: {
+ triggerer,
+ },
+ });
+ };
+
+ describe('when the triggerer data exists', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ triggerer: triggererData });
+ });
+
+ it('should render an avatar link with a correct href', () => {
+ const triggererAvatarLink = wrapper.findComponent(GlAvatarLink);
+ expect(triggererAvatarLink.exists()).toBe(true);
+ expect(triggererAvatarLink.attributes().href).toBe(triggererData.webUrl);
+ });
+
+ it('should render an avatar', () => {
+ const triggererAvatar = wrapper.findComponent(GlAvatar);
+ expect(triggererAvatar.exists()).toBe(true);
+ expect(triggererAvatar.attributes().title).toBe(triggererData.name);
+ expect(triggererAvatar.props().src).toBe(triggererData.avatarUrl);
+ });
+ });
+
+ describe('when the triggerer data does not exist', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ triggerer: null });
+ });
+
+ it('should render nothing', () => {
+ const avatarLink = wrapper.findComponent(GlAvatarLink);
+ expect(avatarLink.exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/environments/environment_details/empty_state_spec.js b/spec/frontend/environments/environment_details/empty_state_spec.js
new file mode 100644
index 00000000000..aaf597d68ed
--- /dev/null
+++ b/spec/frontend/environments/environment_details/empty_state_spec.js
@@ -0,0 +1,39 @@
+import { GlEmptyState } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import EmptyState from '~/environments/environment_details/empty_state.vue';
+import {
+ translations,
+ environmentsHelpPagePath,
+ codeBlockPlaceholders,
+} from '~/environments/environment_details/constants';
+
+describe('~/environments/environment_details/empty_state.vue', () => {
+ let wrapper;
+
+ const createWrapper = () => {
+ return mountExtended(EmptyState);
+ };
+
+ describe('when Empty State is rendered for environment details page', () => {
+ beforeEach(() => {
+ wrapper = createWrapper();
+ });
+
+ it('should render the proper title', () => {
+ expect(wrapper.text()).toContain(translations.emptyStateTitle);
+ });
+
+ it('should render GlEmptyState component with correct props', () => {
+ const glEmptyStateComponent = wrapper.findComponent(GlEmptyState);
+ expect(glEmptyStateComponent.props().primaryButtonText).toBe(
+ translations.emptyStatePrimaryButton,
+ );
+ expect(glEmptyStateComponent.props().primaryButtonLink).toBe(environmentsHelpPagePath);
+ });
+
+ it('should render formatted description', () => {
+ expect(wrapper.text()).not.toContain(codeBlockPlaceholders.code[0]);
+ expect(wrapper.text()).not.toContain(codeBlockPlaceholders.code[1]);
+ });
+ });
+});
diff --git a/spec/frontend/environments/environment_details/page_spec.js b/spec/frontend/environments/environment_details/page_spec.js
new file mode 100644
index 00000000000..3a1a3238abe
--- /dev/null
+++ b/spec/frontend/environments/environment_details/page_spec.js
@@ -0,0 +1,69 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlLoadingIcon, GlTableLite } from '@gitlab/ui';
+import resolvedEnvironmentDetails from 'test_fixtures/graphql/environments/graphql/queries/environment_details.query.graphql.json';
+import emptyEnvironmentDetails from 'test_fixtures/graphql/environments/graphql/queries/environment_details.query.graphql.empty.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import EnvironmentsDetailPage from '~/environments/environment_details/index.vue';
+import EmptyState from '~/environments/environment_details/empty_state.vue';
+import getEnvironmentDetails from '~/environments/graphql/queries/environment_details.query.graphql';
+import createMockApollo from '../../__helpers__/mock_apollo_helper';
+import waitForPromises from '../../__helpers__/wait_for_promises';
+
+describe('~/environments/environment_details/page.vue', () => {
+ Vue.use(VueApollo);
+
+ let wrapper;
+
+ const defaultWrapperParameters = {
+ resolvedData: resolvedEnvironmentDetails,
+ };
+
+ const createWrapper = ({ resolvedData } = defaultWrapperParameters) => {
+ const mockApollo = createMockApollo([
+ [getEnvironmentDetails, jest.fn().mockResolvedValue(resolvedData)],
+ ]);
+
+ return mountExtended(EnvironmentsDetailPage, {
+ apolloProvider: mockApollo,
+ propsData: {
+ projectFullPath: 'gitlab-group/test-project',
+ environmentName: 'test-environment-name',
+ },
+ });
+ };
+
+ describe('when fetching data', () => {
+ it('should show a loading indicator', () => {
+ wrapper = createWrapper();
+
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(wrapper.findComponent(GlTableLite).exists()).not.toBe(true);
+ });
+ });
+
+ describe('when data is fetched', () => {
+ describe('and there are deployments', () => {
+ beforeEach(async () => {
+ wrapper = createWrapper();
+ await waitForPromises();
+ });
+ it('should render a table when query is loaded', async () => {
+ expect(wrapper.findComponent(GlLoadingIcon).exists()).not.toBe(true);
+ expect(wrapper.findComponent(GlTableLite).exists()).toBe(true);
+ });
+ });
+
+ describe('and there are no deployments', () => {
+ beforeEach(async () => {
+ wrapper = createWrapper({ resolvedData: emptyEnvironmentDetails });
+ await waitForPromises();
+ });
+
+ it('should render empty state component', async () => {
+ expect(wrapper.findComponent(GlTableLite).exists()).toBe(false);
+ expect(wrapper.findComponent(EmptyState).exists()).toBe(true);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/environments/environment_details/pagination_spec.js b/spec/frontend/environments/environment_details/pagination_spec.js
new file mode 100644
index 00000000000..107f3c3dd5e
--- /dev/null
+++ b/spec/frontend/environments/environment_details/pagination_spec.js
@@ -0,0 +1,157 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import Pagination from '~/environments/environment_details/pagination.vue';
+
+describe('~/environments/environment_details/pagniation.vue', () => {
+ const mockRouter = {
+ push: jest.fn(),
+ };
+
+ const pageInfo = {
+ startCursor: 'eyJpZCI6IjE2In0',
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ };
+ let wrapper;
+
+ const createWrapper = (pageInfoProp) => {
+ return mountExtended(Pagination, {
+ propsData: {
+ pageInfo: pageInfoProp,
+ },
+ mocks: {
+ $router: mockRouter,
+ },
+ });
+ };
+
+ describe('when neither next nor previous page exists', () => {
+ beforeEach(() => {
+ const emptyPageInfo = { ...pageInfo, hasPreviousPage: false, hasNextPage: false };
+ wrapper = createWrapper(emptyPageInfo);
+ });
+
+ it('should not render pagination component', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
+
+ describe('when Pagination is rendered for environment details page', () => {
+ beforeEach(() => {
+ wrapper = createWrapper(pageInfo);
+ });
+
+ it('should pass correct props to keyset pagination', () => {
+ const glPagination = wrapper.findComponent(GlKeysetPagination);
+ expect(glPagination.exists()).toBe(true);
+ expect(glPagination.props()).toEqual(expect.objectContaining(pageInfo));
+ });
+
+ describe.each([
+ {
+ testPageInfo: pageInfo,
+ expectedAfter: `after=${pageInfo.endCursor}`,
+ expectedBefore: `before=${pageInfo.startCursor}`,
+ },
+ {
+ testPageInfo: { ...pageInfo, hasNextPage: true, hasPreviousPage: false },
+ expectedAfter: `after=${pageInfo.endCursor}`,
+ expectedBefore: '',
+ },
+ {
+ testPageInfo: { ...pageInfo, hasNextPage: false, hasPreviousPage: true },
+ expectedAfter: '',
+ expectedBefore: `before=${pageInfo.startCursor}`,
+ },
+ ])(
+ 'button links generation for $testPageInfo',
+ ({ testPageInfo, expectedAfter, expectedBefore }) => {
+ beforeEach(() => {
+ wrapper = createWrapper(testPageInfo);
+ });
+
+ it(`should have button links defined as ${expectedAfter || 'empty'} and
+ ${expectedBefore || 'empty'}`, () => {
+ const glPagination = wrapper.findComponent(GlKeysetPagination);
+ expect(glPagination.props().prevButtonLink).toContain(expectedBefore);
+ expect(glPagination.props().nextButtonLink).toContain(expectedAfter);
+ });
+ },
+ );
+
+ describe.each([
+ {
+ clickEvent: {
+ shiftKey: false,
+ ctrlKey: false,
+ altKey: false,
+ metaKey: false,
+ },
+ isDefaultPrevented: true,
+ },
+ {
+ clickEvent: {
+ shiftKey: true,
+ ctrlKey: false,
+ altKey: false,
+ metaKey: false,
+ },
+ isDefaultPrevented: false,
+ },
+ {
+ clickEvent: {
+ shiftKey: false,
+ ctrlKey: true,
+ altKey: false,
+ metaKey: false,
+ },
+ isDefaultPrevented: false,
+ },
+ {
+ clickEvent: {
+ shiftKey: false,
+ ctrlKey: false,
+ altKey: true,
+ metaKey: false,
+ },
+ isDefaultPrevented: false,
+ },
+ {
+ clickEvent: {
+ shiftKey: false,
+ ctrlKey: false,
+ altKey: false,
+ metaKey: true,
+ },
+ isDefaultPrevented: false,
+ },
+ ])(
+ 'when a pagination button is clicked with $clickEvent',
+ ({ clickEvent, isDefaultPrevented }) => {
+ let clickEventMock;
+ beforeEach(() => {
+ clickEventMock = { ...clickEvent, preventDefault: jest.fn() };
+ });
+
+ it(`should ${isDefaultPrevented ? '' : 'not '}prevent default event`, () => {
+ const pagination = wrapper.findComponent(GlKeysetPagination);
+ pagination.vm.$emit('click', clickEventMock);
+ expect(clickEventMock.preventDefault).toHaveBeenCalledTimes(isDefaultPrevented ? 1 : 0);
+ });
+ },
+ );
+
+ it('should navigate to a correct previous page', () => {
+ const pagination = wrapper.findComponent(GlKeysetPagination);
+ pagination.vm.$emit('prev', pageInfo.startCursor);
+ expect(mockRouter.push).toHaveBeenCalledWith({ query: { before: pageInfo.startCursor } });
+ });
+
+ it('should navigate to a correct next page', () => {
+ const pagination = wrapper.findComponent(GlKeysetPagination);
+ pagination.vm.$emit('next', pageInfo.endCursor);
+ expect(mockRouter.push).toHaveBeenCalledWith({ query: { after: pageInfo.endCursor } });
+ });
+ });
+});
diff --git a/spec/frontend/environments/environment_details_page_spec.js b/spec/frontend/environments/environment_details_page_spec.js
deleted file mode 100644
index 5a02b34250f..00000000000
--- a/spec/frontend/environments/environment_details_page_spec.js
+++ /dev/null
@@ -1,50 +0,0 @@
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import { GlLoadingIcon, GlTableLite } from '@gitlab/ui';
-import resolvedEnvironmentDetails from 'test_fixtures/graphql/environments/graphql/queries/environment_details.query.graphql.json';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
-import createMockApollo from '../__helpers__/mock_apollo_helper';
-import waitForPromises from '../__helpers__/wait_for_promises';
-import EnvironmentsDetailPage from '../../../app/assets/javascripts/environments/environment_details/index.vue';
-import getEnvironmentDetails from '../../../app/assets/javascripts/environments/graphql/queries/environment_details.query.graphql';
-
-describe('~/environments/environment_details/page.vue', () => {
- Vue.use(VueApollo);
-
- let wrapper;
-
- const createWrapper = () => {
- const mockApollo = createMockApollo([
- [getEnvironmentDetails, jest.fn().mockResolvedValue(resolvedEnvironmentDetails)],
- ]);
-
- return mountExtended(EnvironmentsDetailPage, {
- apolloProvider: mockApollo,
- propsData: {
- projectFullPath: resolvedEnvironmentDetails.data.project.fullPath,
- environmentName: resolvedEnvironmentDetails.data.project.environment.name,
- },
- });
- };
-
- describe('when fetching data', () => {
- it('should show a loading indicator', () => {
- wrapper = createWrapper();
-
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- expect(wrapper.findComponent(GlTableLite).exists()).not.toBe(true);
- });
- });
-
- describe('when data is fetched', () => {
- beforeEach(async () => {
- wrapper = createWrapper();
- await waitForPromises();
- });
-
- it('should render a table when query is loaded', async () => {
- expect(wrapper.findComponent(GlLoadingIcon).exists()).not.toBe(true);
- expect(wrapper.findComponent(GlTableLite).exists()).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/error_tracking/components/error_tracking_list_spec.js b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
index adb2eaaf04e..31473899145 100644
--- a/spec/frontend/error_tracking/components/error_tracking_list_spec.js
+++ b/spec/frontend/error_tracking/components/error_tracking_list_spec.js
@@ -364,7 +364,23 @@ describe('ErrorTrackingList', () => {
});
it('shows empty state', () => {
- expect(wrapper.findComponent(GlEmptyState).isVisible()).toBe(true);
+ const emptyStateComponent = wrapper.findComponent(GlEmptyState);
+ const emptyStatePrimaryDescription = emptyStateComponent.find('span', {
+ exactText: 'Monitor your errors directly in GitLab.',
+ });
+ const emptyStateSecondaryDescription = emptyStateComponent.find('span', {
+ exactText: 'Error tracking is currently in',
+ });
+ const emptyStateLinks = emptyStateComponent.findAll('a');
+ expect(emptyStateComponent.isVisible()).toBe(true);
+ expect(emptyStatePrimaryDescription.exists()).toBe(true);
+ expect(emptyStateSecondaryDescription.exists()).toBe(true);
+ expect(emptyStateLinks.at(0).attributes('href')).toBe(
+ '/help/operations/error_tracking.html#integrated-error-tracking',
+ );
+ expect(emptyStateLinks.at(1).attributes('href')).toBe(
+ 'https://about.gitlab.com/handbook/product/gitlab-the-product/#open-beta',
+ );
});
});
diff --git a/spec/frontend/error_tracking/store/list/actions_spec.js b/spec/frontend/error_tracking/store/list/actions_spec.js
index 2809bbe834e..590983bd93d 100644
--- a/spec/frontend/error_tracking/store/list/actions_spec.js
+++ b/spec/frontend/error_tracking/store/list/actions_spec.js
@@ -4,7 +4,7 @@ import * as actions from '~/error_tracking/store/list/actions';
import * as types from '~/error_tracking/store/list/mutation_types';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/flash.js');
@@ -23,7 +23,7 @@ describe('error tracking actions', () => {
it('should start polling for data', () => {
const payload = { errors: [{ id: 1 }, { id: 2 }] };
- mock.onGet().reply(httpStatusCodes.OK, payload);
+ mock.onGet().reply(HTTP_STATUS_OK, payload);
return testAction(
actions.startPolling,
{},
@@ -39,7 +39,7 @@ describe('error tracking actions', () => {
});
it('should show flash on API error', async () => {
- mock.onGet().reply(httpStatusCodes.BAD_REQUEST);
+ mock.onGet().reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.startPolling,
diff --git a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
index c9095441d41..8653ebac20d 100644
--- a/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
+++ b/spec/frontend/error_tracking_settings/components/project_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import { pick, clone } from 'lodash';
@@ -42,7 +42,7 @@ describe('error tracking settings project dropdown', () => {
describe('empty project list', () => {
it('renders the dropdown', () => {
expect(wrapper.find('#project-dropdown').exists()).toBe(true);
- expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
+ expect(wrapper.findComponent(GlCollapsibleListbox).exists()).toBe(true);
});
it('shows helper text', () => {
@@ -57,8 +57,10 @@ describe('error tracking settings project dropdown', () => {
});
it('does not contain any dropdown items', () => {
- expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(false);
- expect(wrapper.findComponent(GlDropdown).props('text')).toBe('No projects available');
+ expect(wrapper.findComponent(GlCollapsibleListbox).props('items')).toEqual([]);
+ expect(wrapper.findComponent(GlCollapsibleListbox).props('toggleText')).toBe(
+ 'No projects available',
+ );
});
});
@@ -71,12 +73,12 @@ describe('error tracking settings project dropdown', () => {
it('renders the dropdown', () => {
expect(wrapper.find('#project-dropdown').exists()).toBe(true);
- expect(wrapper.findComponent(GlDropdown).exists()).toBe(true);
+ expect(wrapper.findComponent(GlCollapsibleListbox).exists()).toBe(true);
});
it('contains a number of dropdown items', () => {
- expect(wrapper.findComponent(GlDropdownItem).exists()).toBe(true);
- expect(wrapper.findAllComponents(GlDropdownItem).length).toBe(2);
+ expect(wrapper.findComponent(GlCollapsibleListbox).exists()).toBe(true);
+ expect(wrapper.findComponent(GlCollapsibleListbox).props('items').length).toBe(2);
});
});
diff --git a/spec/frontend/error_tracking_settings/mock.js b/spec/frontend/error_tracking_settings/mock.js
index b2d7a912518..96d93540ba5 100644
--- a/spec/frontend/error_tracking_settings/mock.js
+++ b/spec/frontend/error_tracking_settings/mock.js
@@ -5,12 +5,14 @@ const defaultStore = createStore();
export const projectList = [
{
+ id: '1',
name: 'name',
slug: 'slug',
organizationName: 'organizationName',
organizationSlug: 'organizationSlug',
},
{
+ id: '2',
name: 'name2',
slug: 'slug2',
organizationName: 'organizationName2',
@@ -19,6 +21,7 @@ export const projectList = [
];
export const staleProject = {
+ id: '3',
name: 'staleName',
slug: 'staleSlug',
organizationName: 'staleOrganizationName',
@@ -26,6 +29,7 @@ export const staleProject = {
};
export const normalizedProject = {
+ id: '5',
name: 'name',
slug: 'slug',
organizationName: 'organization_name',
@@ -33,6 +37,7 @@ export const normalizedProject = {
};
export const sampleBackendProject = {
+ id: '5',
name: normalizedProject.name,
slug: normalizedProject.slug,
organization_name: normalizedProject.organizationName,
@@ -45,6 +50,7 @@ export const sampleFrontendSettings = {
integrated: false,
token: 'token',
selectedProject: {
+ id: '5',
slug: normalizedProject.slug,
name: normalizedProject.name,
organizationName: normalizedProject.organizationName,
@@ -58,6 +64,7 @@ export const transformedSettings = {
integrated: false,
token: 'token',
project: {
+ sentry_project_id: '5',
slug: normalizedProject.slug,
name: normalizedProject.name,
organization_name: normalizedProject.organizationName,
diff --git a/spec/frontend/feature_flags/components/environments_dropdown_spec.js b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
index 2b9710c9085..a4738fed37e 100644
--- a/spec/frontend/feature_flags/components/environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/environments_dropdown_spec.js
@@ -6,7 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import EnvironmentsDropdown from '~/feature_flags/components/environments_dropdown.vue';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Feature flags > Environments dropdown', () => {
let wrapper;
@@ -51,7 +51,7 @@ describe('Feature flags > Environments dropdown', () => {
describe('on focus', () => {
it('sets results with the received data', async () => {
- mock.onGet(`${TEST_HOST}/environments.json'`).replyOnce(httpStatusCodes.OK, results);
+ mock.onGet(`${TEST_HOST}/environments.json'`).replyOnce(HTTP_STATUS_OK, results);
factory();
findEnvironmentSearchInput().vm.$emit('focus');
await waitForPromises();
@@ -63,7 +63,7 @@ describe('Feature flags > Environments dropdown', () => {
describe('on keyup', () => {
it('sets results with the received data', async () => {
- mock.onGet(`${TEST_HOST}/environments.json'`).replyOnce(httpStatusCodes.OK, results);
+ mock.onGet(`${TEST_HOST}/environments.json'`).replyOnce(HTTP_STATUS_OK, results);
factory();
findEnvironmentSearchInput().vm.$emit('keyup');
await waitForPromises();
@@ -76,7 +76,7 @@ describe('Feature flags > Environments dropdown', () => {
describe('on input change', () => {
describe('on success', () => {
beforeEach(async () => {
- mock.onGet(`${TEST_HOST}/environments.json'`).replyOnce(httpStatusCodes.OK, results);
+ mock.onGet(`${TEST_HOST}/environments.json'`).replyOnce(HTTP_STATUS_OK, results);
factory();
findEnvironmentSearchInput().vm.$emit('focus');
findEnvironmentSearchInput().vm.$emit('input', 'production');
@@ -128,7 +128,7 @@ describe('Feature flags > Environments dropdown', () => {
describe('on click create button', () => {
beforeEach(async () => {
- mock.onGet(`${TEST_HOST}/environments.json'`).replyOnce(httpStatusCodes.OK, []);
+ mock.onGet(`${TEST_HOST}/environments.json'`).replyOnce(HTTP_STATUS_OK, []);
factory();
findEnvironmentSearchInput().vm.$emit('focus');
findEnvironmentSearchInput().vm.$emit('input', 'production');
diff --git a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
index 1c0c444c296..b71cdf78207 100644
--- a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
@@ -4,7 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import NewEnvironmentsDropdown from '~/feature_flags/components/new_environments_dropdown.vue';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
const TEST_HOST = '/test';
const TEST_SEARCH = 'production';
@@ -74,7 +74,7 @@ describe('New Environments Dropdown', () => {
describe('with results', () => {
let items;
beforeEach(() => {
- axiosMock.onGet(TEST_HOST).reply(httpStatusCodes.OK, ['prod', 'production']);
+ axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, ['prod', 'production']);
wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', 'prod');
return axios.waitForAll().then(() => {
diff --git a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
index d82081041d9..4d5cb26810e 100644
--- a/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
+++ b/spec/frontend/feature_highlight/feature_highlight_helper_spec.js
@@ -2,7 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { dismiss } from '~/feature_highlight/feature_highlight_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes, { HTTP_STATUS_CREATED } from '~/lib/utils/http_status';
+import { HTTP_STATUS_CREATED, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -11,7 +11,6 @@ describe('feature highlight helper', () => {
let mockAxios;
const endpoint = '/-/callouts/dismiss';
const highlightId = '123';
- const { INTERNAL_SERVER_ERROR } = httpStatusCodes;
beforeEach(() => {
mockAxios = new MockAdapter(axios);
@@ -28,7 +27,9 @@ describe('feature highlight helper', () => {
});
it('triggers flash when dismiss request fails', async () => {
- mockAxios.onPost(endpoint, { feature_name: highlightId }).replyOnce(INTERNAL_SERVER_ERROR);
+ mockAxios
+ .onPost(endpoint, { feature_name: highlightId })
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await dismiss(endpoint, highlightId);
diff --git a/spec/frontend/fixtures/environments.rb b/spec/frontend/fixtures/environments.rb
index 3ca5b50ac9c..77e2a96b328 100644
--- a/spec/frontend/fixtures/environments.rb
+++ b/spec/frontend/fixtures/environments.rb
@@ -18,36 +18,55 @@ RSpec.describe 'Environments (JavaScript fixtures)', feature_category: :environm
let(:user) { create(:user) }
let(:role) { :developer }
- let_it_be(:deployment) do
- create(:deployment, :success, environment: environment, deployable: nil)
- end
- let_it_be(:deployment_success) do
- create(:deployment, :success, environment: environment, deployable: build)
- end
+ describe GraphQL::Query, type: :request do
+ environment_details_query_path = 'environments/graphql/queries/environment_details.query.graphql'
- let_it_be(:deployment_failed) do
- create(:deployment, :failed, environment: environment, deployable: build)
- end
+ context 'with no deployments' do
+ it "graphql/#{environment_details_query_path}.empty.json" do
+ 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
+ })
+ expect_graphql_errors_to_be_empty
+ end
+ end
- let_it_be(:deployment_running) do
- create(:deployment, :running, environment: environment, deployable: build)
- end
+ context 'with deployments' do
+ let_it_be(:deployment) do
+ create(:deployment, :success, environment: environment, deployable: nil)
+ end
- describe GraphQL::Query, type: :request do
- environment_details_query_path = 'environments/graphql/queries/environment_details.query.graphql'
+ let_it_be(:deployment_success) do
+ create(:deployment, :success, environment: environment, deployable: build)
+ end
+
+ let_it_be(:deployment_failed) do
+ create(:deployment, :failed, environment: environment, deployable: build)
+ end
+
+ let_it_be(:deployment_running) do
+ create(:deployment, :running, environment: environment, deployable: build)
+ end
+
+ it "graphql/#{environment_details_query_path}.json" do
+ query = get_graphql_query_as_string(environment_details_query_path)
- 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
- })
- expect_graphql_errors_to_be_empty
+ post_graphql(query, current_user: admin,
+ variables:
+ {
+ projectFullPath: project.full_path,
+ environmentName: environment.name,
+ pageSize: 10
+ })
+ expect_graphql_errors_to_be_empty
+ end
end
end
end
diff --git a/spec/frontend/fixtures/issues.rb b/spec/frontend/fixtures/issues.rb
index bc5ece20032..1e6baf30a76 100644
--- a/spec/frontend/fixtures/issues.rb
+++ b/spec/frontend/fixtures/issues.rb
@@ -2,7 +2,7 @@
require 'spec_helper'
-RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller do
+RSpec.describe Projects::IssuesController, '(JavaScript fixtures)', :with_license, type: :controller do
include JavaScriptFixturesHelpers
let(:user) { create(:user, feed_token: 'feedtoken:coldfeed') }
diff --git a/spec/frontend/fixtures/projects.rb b/spec/frontend/fixtures/projects.rb
index 101ba203a57..2ccf2c0392f 100644
--- a/spec/frontend/fixtures/projects.rb
+++ b/spec/frontend/fixtures/projects.rb
@@ -66,4 +66,36 @@ RSpec.describe 'Projects (JavaScript fixtures)', type: :controller do
end
end
end
+
+ describe 'Storage', feature_category: :subscription_cost_management do
+ describe GraphQL::Query, type: :request do
+ include GraphqlHelpers
+ context 'project storage statistics query' do
+ before do
+ project.statistics.update!(
+ repository_size: 3_900_000,
+ lfs_objects_size: 4_800_000,
+ build_artifacts_size: 400_000,
+ pipeline_artifacts_size: 400_000,
+ container_registry_size: 3_900_000,
+ wiki_size: 300_000,
+ packages_size: 3_800_000,
+ uploads_size: 900_000
+ )
+ end
+
+ base_input_path = 'usage_quotas/storage/queries/'
+ base_output_path = 'graphql/usage_quotas/storage/'
+ query_name = 'project_storage.query.graphql'
+
+ it "#{base_output_path}#{query_name}.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: user, variables: { fullPath: project.full_path })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+ end
+ end
end
diff --git a/spec/frontend/fixtures/runner_instructions.rb b/spec/frontend/fixtures/runner_instructions.rb
index 90a01c37479..5659b8023e9 100644
--- a/spec/frontend/fixtures/runner_instructions.rb
+++ b/spec/frontend/fixtures/runner_instructions.rb
@@ -7,7 +7,7 @@ RSpec.describe 'Runner Instructions (JavaScript fixtures)', feature_category: :r
include JavaScriptFixturesHelpers
include GraphqlHelpers
- query_path = 'vue_shared/components/runner_instructions/graphql/queries'
+ query_path = 'vue_shared/components/runner_instructions/graphql'
describe GraphQL::Query do
describe 'get_runner_platforms.query.graphql', type: :request do
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index ade36cd1637..2f0a52a9884 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -1,9 +1,8 @@
import * as Sentry from '@sentry/browser';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import createFlash, {
+import {
hideFlash,
addDismissFlashClickListener,
- FLASH_TYPES,
FLASH_CLOSED_EVENT,
createAlert,
VARIANT_WARNING,
@@ -340,207 +339,6 @@ describe('Flash', () => {
});
});
- describe('createFlash', () => {
- const message = 'test';
- const fadeTransition = false;
- const addBodyClass = true;
- const defaultParams = {
- message,
- actionConfig: null,
- fadeTransition,
- addBodyClass,
- };
-
- describe('no flash-container', () => {
- it('does not add to the DOM', () => {
- const flashEl = createFlash({ message });
-
- expect(flashEl).toBeNull();
-
- expect(document.querySelector('.flash-alert')).toBeNull();
- });
- });
-
- describe('with flash-container', () => {
- beforeEach(() => {
- setHTMLFixture(
- '<div class="content-wrapper js-content-wrapper"><div class="flash-container"></div></div>',
- );
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('adds flash alert element into the document by default', () => {
- createFlash({ ...defaultParams });
-
- expect(document.querySelector('.flash-container .flash-alert')).not.toBeNull();
- expect(document.body.className).toContain('flash-shown');
- });
-
- it('adds flash of a warning type', () => {
- createFlash({ ...defaultParams, type: FLASH_TYPES.WARNING });
-
- expect(document.querySelector('.flash-container .flash-warning')).not.toBeNull();
- expect(document.body.className).toContain('flash-shown');
- });
-
- it('escapes text', () => {
- createFlash({ ...defaultParams, message: '<script>alert("a")</script>' });
-
- const html = document.querySelector('.flash-text').innerHTML;
-
- expect(html).toContain('&lt;script&gt;alert("a")&lt;/script&gt;');
- expect(html).not.toContain('<script>alert("a")</script>');
- });
-
- it('adds flash into specified parent', () => {
- createFlash({ ...defaultParams, parent: document.querySelector('.content-wrapper') });
-
- expect(document.querySelector('.content-wrapper .flash-alert')).not.toBeNull();
- expect(document.querySelector('.content-wrapper').innerText.trim()).toEqual(message);
- });
-
- it('adds container classes when inside content-wrapper', () => {
- createFlash(defaultParams);
-
- expect(document.querySelector('.flash-text').className).toBe('flash-text');
- expect(document.querySelector('.content-wrapper').innerText.trim()).toEqual(message);
- });
-
- it('does not add container when outside of content-wrapper', () => {
- document.querySelector('.content-wrapper').className = 'js-content-wrapper';
- createFlash(defaultParams);
-
- expect(document.querySelector('.flash-text').className.trim()).toContain('flash-text');
- });
-
- it('removes element after clicking', () => {
- createFlash({ ...defaultParams });
-
- document.querySelector('.flash-alert .js-close-icon').click();
-
- expect(document.querySelector('.flash-alert')).toBeNull();
-
- expect(document.body.className).not.toContain('flash-shown');
- });
-
- it('does not capture error using Sentry', () => {
- createFlash({ ...defaultParams, captureError: false, error: new Error('Error!') });
-
- expect(Sentry.captureException).not.toHaveBeenCalled();
- });
-
- it('captures error using Sentry', () => {
- createFlash({ ...defaultParams, captureError: true, error: new Error('Error!') });
-
- expect(Sentry.captureException).toHaveBeenCalledWith(expect.any(Error));
- expect(Sentry.captureException).toHaveBeenCalledWith(
- expect.objectContaining({
- message: 'Error!',
- }),
- );
- });
-
- describe('with actionConfig', () => {
- const findFlashAction = () => document.querySelector('.flash-container .flash-action');
-
- it('adds action link', () => {
- createFlash({
- ...defaultParams,
- actionConfig: {
- title: 'test',
- },
- });
-
- expect(findFlashAction()).not.toBeNull();
- });
-
- it('creates link with href', () => {
- createFlash({
- ...defaultParams,
- actionConfig: {
- href: 'testing',
- title: 'test',
- },
- });
-
- const action = findFlashAction();
-
- expect(action.href).toBe(`${window.location}testing`);
- expect(action.textContent.trim()).toBe('test');
- });
-
- it('uses hash as href when no href is present', () => {
- createFlash({
- ...defaultParams,
- actionConfig: {
- title: 'test',
- },
- });
-
- expect(findFlashAction().href).toBe(`${window.location}#`);
- });
-
- it('adds role when no href is present', () => {
- createFlash({
- ...defaultParams,
- actionConfig: {
- title: 'test',
- },
- });
-
- expect(findFlashAction().getAttribute('role')).toBe('button');
- });
-
- it('escapes the title text', () => {
- createFlash({
- ...defaultParams,
- actionConfig: {
- title: '<script>alert("a")</script>',
- },
- });
-
- const html = findFlashAction().innerHTML;
-
- expect(html).toContain('&lt;script&gt;alert("a")&lt;/script&gt;');
- expect(html).not.toContain('<script>alert("a")</script>');
- });
-
- it('calls actionConfig clickHandler on click', () => {
- const clickHandler = jest.fn();
-
- createFlash({
- ...defaultParams,
- actionConfig: {
- title: 'test',
- clickHandler,
- },
- });
-
- findFlashAction().click();
-
- expect(clickHandler).toHaveBeenCalled();
- });
- });
-
- describe('additional behavior', () => {
- describe('close', () => {
- it('clicks the close icon', () => {
- const flash = createFlash({ ...defaultParams });
- const close = document.querySelector('.flash-alert .js-close-icon');
-
- jest.spyOn(close, 'click');
- flash.close();
-
- expect(close.click.mock.calls.length).toBe(1);
- });
- });
- });
- });
- });
-
describe('addDismissFlashClickListener', () => {
let el;
diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js
index c201bbf4af2..b1e87aca63d 100644
--- a/spec/frontend/frequent_items/components/app_spec.js
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -1,3 +1,4 @@
+import { GlButton, GlIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
@@ -103,6 +104,7 @@ describe('Frequent Items App Component', () => {
expect(loading.exists()).toBe(true);
expect(loading.find('[aria-label="Loading projects"]').exists()).toBe(true);
+ expect(findSectionHeader().exists()).toBe(false);
});
it('should render frequent projects list header', () => {
@@ -112,25 +114,6 @@ describe('Frequent Items App Component', () => {
expect(sectionHeader.text()).toBe('Frequently visited');
});
- it('should render frequent projects list', async () => {
- const expectedResult = getTopFrequentItems(mockFrequentProjects);
- localStorage.setItem(TEST_STORAGE_KEY, JSON.stringify(mockFrequentProjects));
-
- expect(findFrequentItems().length).toBe(1);
-
- triggerDropdownOpen();
- await nextTick();
-
- expect(findFrequentItems().length).toBe(expectedResult.length);
- expect(findFrequentItemsList().props()).toEqual({
- items: expectedResult,
- namespace: TEST_NAMESPACE,
- hasSearchQuery: false,
- isFetchFailed: false,
- matcher: '',
- });
- });
-
it('should render searched projects list', async () => {
mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects.data);
@@ -164,6 +147,47 @@ describe('Frequent Items App Component', () => {
}),
);
});
+
+ describe('with frequent items list', () => {
+ const expectedResult = getTopFrequentItems(mockFrequentProjects);
+
+ beforeEach(async () => {
+ localStorage.setItem(TEST_STORAGE_KEY, JSON.stringify(mockFrequentProjects));
+ triggerDropdownOpen();
+ await nextTick();
+ });
+
+ it('should render edit button within header', () => {
+ const itemEditButton = findSectionHeader().findComponent(GlButton);
+
+ expect(itemEditButton.exists()).toBe(true);
+ expect(itemEditButton.attributes('title')).toBe('Toggle edit mode');
+ expect(itemEditButton.findComponent(GlIcon).props('name')).toBe('pencil');
+ });
+
+ it('should render frequent projects list', () => {
+ expect(findFrequentItems().length).toBe(expectedResult.length);
+ expect(findFrequentItemsList().props()).toEqual({
+ items: expectedResult,
+ namespace: TEST_NAMESPACE,
+ hasSearchQuery: false,
+ isFetchFailed: false,
+ isItemRemovalFailed: false,
+ matcher: '',
+ });
+ });
+
+ it('dispatches action `toggleItemsListEditablity` when edit button is clicked', async () => {
+ const itemEditButton = findSectionHeader().findComponent(GlButton);
+ itemEditButton.vm.$emit('click');
+
+ await nextTick();
+
+ expect(store.dispatch).toHaveBeenCalledWith(
+ `${TEST_VUEX_MODULE}/toggleItemsListEditablity`,
+ );
+ });
+ });
});
describe('with searchClass', () => {
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
index e6673fa78ec..4f2badf869d 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_item_spec.js
@@ -1,5 +1,5 @@
-import { GlButton } from '@gitlab/ui';
-import Vue from 'vue';
+import { GlIcon } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
@@ -12,6 +12,7 @@ import { mockProject } from '../mock_data';
Vue.use(Vuex);
describe('FrequentItemsListItemComponent', () => {
+ const TEST_VUEX_MODULE = 'frequentProjects';
let wrapper;
let trackingSpy;
let store;
@@ -20,11 +21,18 @@ describe('FrequentItemsListItemComponent', () => {
const findAvatar = () => wrapper.findComponent(ProjectAvatar);
const findAllTitles = () => wrapper.findAllByTestId('frequent-items-item-title');
const findNamespace = () => wrapper.findByTestId('frequent-items-item-namespace');
- const findAllButtons = () => wrapper.findAllComponents(GlButton);
+ const findAllFrequentItems = () => wrapper.findAllByTestId('frequent-item-link');
const findAllNamespace = () => wrapper.findAllByTestId('frequent-items-item-namespace');
const findAllAvatars = () => wrapper.findAllComponents(ProjectAvatar);
const findAllMetadataContainers = () =>
wrapper.findAllByTestId('frequent-items-item-metadata-container');
+ const findRemoveButton = () => wrapper.findByTestId('item-remove');
+
+ const toggleItemsListEditablity = async () => {
+ store.dispatch(`${TEST_VUEX_MODULE}/toggleItemsListEditablity`);
+
+ await nextTick();
+ };
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(frequentItemsListItemComponent, {
@@ -38,7 +46,7 @@ describe('FrequentItemsListItemComponent', () => {
...props,
},
provide: {
- vuexModule: 'frequentProjects',
+ vuexModule: TEST_VUEX_MODULE,
},
});
};
@@ -102,7 +110,7 @@ describe('FrequentItemsListItemComponent', () => {
it.each`
name | selector | expected
- ${'button'} | ${findAllButtons} | ${1}
+ ${'list item'} | ${findAllFrequentItems} | ${1}
${'avatar container'} | ${findAllAvatars} | ${1}
${'metadata container'} | ${findAllMetadataContainers} | ${1}
${'title'} | ${findAllTitles} | ${1}
@@ -111,8 +119,37 @@ describe('FrequentItemsListItemComponent', () => {
expect(selector()).toHaveLength(expected);
});
+ it('renders remove button within item when `isItemsListEditable` is true', async () => {
+ await toggleItemsListEditablity();
+
+ const removeButton = findRemoveButton();
+ expect(removeButton.exists()).toBe(true);
+ expect(removeButton.attributes('title')).toBe('Remove');
+ expect(removeButton.findComponent(GlIcon).props('name')).toBe('close');
+ });
+
+ it('dispatches action `removeFrequentItem` when remove button is clicked', async () => {
+ await toggleItemsListEditablity();
+
+ jest.spyOn(store, 'dispatch');
+
+ const removeButton = findRemoveButton();
+ removeButton.vm.$emit(
+ 'click',
+ { stopPropagation: jest.fn(), preventDefault: jest.fn() },
+ mockProject.id,
+ );
+
+ await nextTick();
+
+ expect(store.dispatch).toHaveBeenCalledWith(
+ `${TEST_VUEX_MODULE}/removeFrequentItem`,
+ mockProject.id,
+ );
+ });
+
it('tracks when item link is clicked', () => {
- const link = wrapper.findComponent(GlButton);
+ const link = wrapper.findByTestId('frequent-item-link');
link.vm.$emit('click');
diff --git a/spec/frontend/frequent_items/components/frequent_items_list_spec.js b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
index 9f08a432a3d..d024925f62b 100644
--- a/spec/frontend/frequent_items/components/frequent_items_list_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_list_spec.js
@@ -18,6 +18,7 @@ describe('FrequentItemsListComponent', () => {
namespace: 'projects',
items: mockFrequentProjects,
isFetchFailed: false,
+ isItemRemovalFailed: false,
hasSearchQuery: false,
matcher: 'lab',
...props,
@@ -51,22 +52,34 @@ describe('FrequentItemsListComponent', () => {
});
describe('fetched item messages', () => {
- it('should return appropriate empty list message based on value of `localStorageFailed` prop with projects', async () => {
+ it('should show default empty list message', async () => {
createComponent({
- isFetchFailed: true,
+ items: [],
});
- expect(wrapper.vm.listEmptyMessage).toBe(
- 'This feature requires browser localStorage support',
+ expect(wrapper.findByTestId('frequent-items-list-empty').text()).toContain(
+ 'Projects you visit often will appear here',
);
-
- wrapper.setProps({
- isFetchFailed: false,
- });
- await nextTick();
-
- expect(wrapper.vm.listEmptyMessage).toBe('Projects you visit often will appear here');
});
+
+ it.each`
+ isFetchFailed | isItemRemovalFailed
+ ${true} | ${false}
+ ${false} | ${true}
+ `(
+ 'should show failure message when `isFetchFailed` is $isFetchFailed or `isItemRemovalFailed` is $isItemRemovalFailed',
+ ({ isFetchFailed, isItemRemovalFailed }) => {
+ createComponent({
+ items: [],
+ isFetchFailed,
+ isItemRemovalFailed,
+ });
+
+ expect(wrapper.findByTestId('frequent-items-list-empty').text()).toContain(
+ 'This feature requires browser localStorage support',
+ );
+ },
+ );
});
describe('searched item messages', () => {
diff --git a/spec/frontend/frequent_items/store/actions_spec.js b/spec/frontend/frequent_items/store/actions_spec.js
index 3fc3eaf52a2..4f998cc26da 100644
--- a/spec/frontend/frequent_items/store/actions_spec.js
+++ b/spec/frontend/frequent_items/store/actions_spec.js
@@ -5,6 +5,7 @@ import * as types from '~/frequent_items/store/mutation_types';
import state from '~/frequent_items/store/state';
import AccessorUtilities from '~/lib/utils/accessor';
import axios from '~/lib/utils/axios_utils';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import {
mockNamespace,
mockStorageKey,
@@ -13,6 +14,7 @@ import {
} from '../mock_data';
describe('Frequent Items Dropdown Store Actions', () => {
+ useLocalStorageSpy();
let mockedState;
let mock;
@@ -52,6 +54,18 @@ describe('Frequent Items Dropdown Store Actions', () => {
});
});
+ describe('toggleItemsListEditablity', () => {
+ it('should toggle items list editablity', () => {
+ return testAction(
+ actions.toggleItemsListEditablity,
+ null,
+ mockedState,
+ [{ type: types.TOGGLE_ITEMS_LIST_EDITABILITY }],
+ [],
+ );
+ });
+ });
+
describe('requestFrequentItems', () => {
it('should request frequent items', () => {
return testAction(
@@ -211,4 +225,77 @@ describe('Frequent Items Dropdown Store Actions', () => {
);
});
});
+
+ describe('removeFrequentItemSuccess', () => {
+ it('should remove frequent item on success', () => {
+ return testAction(
+ actions.removeFrequentItemSuccess,
+ { itemId: 1 },
+ mockedState,
+ [
+ {
+ type: types.RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS,
+ payload: { itemId: 1 },
+ },
+ ],
+ [],
+ );
+ });
+ });
+
+ describe('removeFrequentItemError', () => {
+ it('should should not remove frequent item on failure', () => {
+ return testAction(
+ actions.removeFrequentItemError,
+ null,
+ mockedState,
+ [{ type: types.RECEIVE_REMOVE_FREQUENT_ITEM_ERROR }],
+ [],
+ );
+ });
+ });
+
+ describe('removeFrequentItem', () => {
+ beforeEach(() => {
+ mockedState.items = [...mockFrequentProjects];
+ window.localStorage.setItem(mockStorageKey, JSON.stringify(mockFrequentProjects));
+ });
+
+ it('should remove provided itemId from localStorage', () => {
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(true);
+
+ actions.removeFrequentItem(
+ { commit: jest.fn(), dispatch: jest.fn(), state: mockedState },
+ mockFrequentProjects[0].id,
+ );
+
+ expect(window.localStorage.getItem(mockStorageKey)).toBe(
+ JSON.stringify(mockFrequentProjects.slice(1)), // First item was removed
+ );
+ });
+
+ it('should dispatch `removeFrequentItemSuccess` on localStorage update success', () => {
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(true);
+
+ return testAction(
+ actions.removeFrequentItem,
+ mockFrequentProjects[0].id,
+ mockedState,
+ [],
+ [{ type: 'removeFrequentItemSuccess', payload: mockFrequentProjects[0].id }],
+ );
+ });
+
+ it('should dispatch `removeFrequentItemError` on localStorage update failure', () => {
+ jest.spyOn(AccessorUtilities, 'canUseLocalStorage').mockReturnValue(false);
+
+ return testAction(
+ actions.removeFrequentItem,
+ mockFrequentProjects[0].id,
+ mockedState,
+ [],
+ [{ type: 'removeFrequentItemError' }],
+ );
+ });
+ });
});
diff --git a/spec/frontend/frequent_items/store/mutations_spec.js b/spec/frontend/frequent_items/store/mutations_spec.js
index e593c9fae58..1e1878c3377 100644
--- a/spec/frontend/frequent_items/store/mutations_spec.js
+++ b/spec/frontend/frequent_items/store/mutations_spec.js
@@ -44,6 +44,18 @@ describe('Frequent Items dropdown mutations', () => {
});
});
+ describe('TOGGLE_ITEMS_LIST_EDITABILITY', () => {
+ it('should toggle items list editablity', () => {
+ mutations[types.TOGGLE_ITEMS_LIST_EDITABILITY](stateCopy);
+
+ expect(stateCopy.isItemsListEditable).toEqual(true);
+
+ mutations[types.TOGGLE_ITEMS_LIST_EDITABILITY](stateCopy);
+
+ expect(stateCopy.isItemsListEditable).toEqual(false);
+ });
+ });
+
describe('REQUEST_FREQUENT_ITEMS', () => {
it('should set view states when requesting frequent items', () => {
mutations[types.REQUEST_FREQUENT_ITEMS](stateCopy);
@@ -114,4 +126,27 @@ describe('Frequent Items dropdown mutations', () => {
expect(stateCopy.isFetchFailed).toEqual(true);
});
});
+
+ describe('RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS', () => {
+ it('should remove item with provided itemId from the items', () => {
+ stateCopy.isItemRemovalFailed = true;
+ stateCopy.items = mockFrequentProjects;
+
+ mutations[types.RECEIVE_REMOVE_FREQUENT_ITEM_SUCCESS](stateCopy, mockFrequentProjects[0].id);
+
+ expect(stateCopy.items).toHaveLength(mockFrequentProjects.length - 1);
+ expect(stateCopy.items).toEqual([...mockFrequentProjects.slice(1)]);
+ expect(stateCopy.isItemRemovalFailed).toBe(false);
+ });
+ });
+
+ describe('RECEIVE_REMOVE_FREQUENT_ITEM_ERROR', () => {
+ it('should remove item with provided itemId from the items', () => {
+ stateCopy.isItemRemovalFailed = false;
+
+ mutations[types.RECEIVE_REMOVE_FREQUENT_ITEM_ERROR](stateCopy);
+
+ expect(stateCopy.isItemRemovalFailed).toBe(true);
+ });
+ });
});
diff --git a/spec/frontend/gfm_auto_complete/mock_data.js b/spec/frontend/gfm_auto_complete/mock_data.js
index 9c5a9d7ef3d..d58ccaf0f39 100644
--- a/spec/frontend/gfm_auto_complete/mock_data.js
+++ b/spec/frontend/gfm_auto_complete/mock_data.js
@@ -37,8 +37,8 @@ export const crmContactsMock = [
{
id: 1,
email: 'contact.1@email.com',
- firstName: 'Contact',
- lastName: 'One',
+ first_name: 'Contact',
+ last_name: 'One',
search: 'contact.1@email.com',
state: 'active',
set: false,
@@ -46,8 +46,8 @@ export const crmContactsMock = [
{
id: 2,
email: 'contact.2@email.com',
- firstName: 'Contact',
- lastName: 'Two',
+ first_name: 'Contact',
+ last_name: 'Two',
search: 'contact.2@email.com',
state: 'active',
set: false,
@@ -55,8 +55,8 @@ export const crmContactsMock = [
{
id: 3,
email: 'contact.3@email.com',
- firstName: 'Contact',
- lastName: 'Three',
+ first_name: 'Contact',
+ last_name: 'Three',
search: 'contact.3@email.com',
state: 'inactive',
set: false,
@@ -64,8 +64,8 @@ export const crmContactsMock = [
{
id: 4,
email: 'contact.4@email.com',
- firstName: 'Contact',
- lastName: 'Four',
+ first_name: 'Contact',
+ last_name: 'Four',
search: 'contact.4@email.com',
state: 'inactive',
set: true,
@@ -73,8 +73,8 @@ export const crmContactsMock = [
{
id: 5,
email: 'contact.5@email.com',
- firstName: 'Contact',
- lastName: 'Five',
+ first_name: 'Contact',
+ last_name: 'Five',
search: 'contact.5@email.com',
state: 'active',
set: true,
@@ -82,8 +82,8 @@ export const crmContactsMock = [
{
id: 5,
email: 'contact.6@email.com',
- firstName: 'Contact',
- lastName: 'Six',
+ first_name: 'Contact',
+ last_name: 'Six',
search: 'contact.6@email.com',
state: 'active',
set: undefined, // On purpose
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index eeef92d4183..cc2dc084e47 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -4,6 +4,7 @@ import $ from 'jquery';
import labelsFixture from 'test_fixtures/autocomplete_sources/labels.json';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import GfmAutoComplete, {
+ escape,
membersBeforeSave,
highlighter,
CONTACT_STATE_ACTIVE,
@@ -21,6 +22,20 @@ import {
crmContactsMock,
} from 'ee_else_ce_jest/gfm_auto_complete/mock_data';
+describe('escape', () => {
+ it.each`
+ xssPayload | escapedPayload
+ ${'<script>alert(1)</script>'} | ${'&lt;script&gt;alert(1)&lt;/script&gt;'}
+ ${'%3Cscript%3E alert(1) %3C%2Fscript%3E'} | ${'&lt;script&gt; alert(1) &lt;/script&gt;'}
+ ${'%253Cscript%253E alert(1) %253C%252Fscript%253E'} | ${'&lt;script&gt; alert(1) &lt;/script&gt;'}
+ `(
+ 'escapes the input string correctly accounting for multiple encoding',
+ ({ xssPayload, escapedPayload }) => {
+ expect(escape(xssPayload)).toBe(escapedPayload);
+ },
+ );
+});
+
describe('GfmAutoComplete', () => {
const fetchDataMock = { fetchData: jest.fn() };
let gfmAutoCompleteCallbacks = GfmAutoComplete.prototype.getDefaultCallbacks.call(fetchDataMock);
@@ -590,7 +605,7 @@ describe('GfmAutoComplete', () => {
id: 5,
title: '${search}<script>oh no $', // eslint-disable-line no-template-curly-in-string
}),
- ).toBe('<li><small>5</small> &dollar;{search}&lt;script&gt;oh no &dollar;</li>');
+ ).toBe('<li><small>5</small> &amp;dollar;{search}&lt;script&gt;oh no &amp;dollar;</li>');
});
});
@@ -636,7 +651,7 @@ describe('GfmAutoComplete', () => {
availabilityStatus: '',
}),
).toBe(
- '<li>IMG my-group <small>&dollar;{search}&lt;script&gt;oh no &dollar;</small> <i class="icon"/></li>',
+ '<li>IMG my-group <small>&amp;dollar;{search}&lt;script&gt;oh no &amp;dollar;</small> <i class="icon"/></li>',
);
});
@@ -813,7 +828,7 @@ describe('GfmAutoComplete', () => {
const title = '${search}<script>oh no $'; // eslint-disable-line no-template-curly-in-string
expect(GfmAutoComplete.Labels.templateFunction(color, title)).toBe(
- '<li><span class="dropdown-label-box" style="background: #123456"></span> &dollar;{search}&lt;script&gt;oh no &dollar;</li>',
+ '<li><span class="dropdown-label-box" style="background: #123456"></span> &amp;dollar;{search}&lt;script&gt;oh no &amp;dollar;</li>',
);
});
});
@@ -868,7 +883,7 @@ describe('GfmAutoComplete', () => {
const title = '${search}<script>oh no $'; // eslint-disable-line no-template-curly-in-string
expect(GfmAutoComplete.Milestones.templateFunction(title, expired)).toBe(
- '<li>&dollar;{search}&lt;script&gt;oh no &dollar;</li>',
+ '<li>&amp;dollar;{search}&lt;script&gt;oh no &amp;dollar;</li>',
);
});
});
@@ -925,7 +940,9 @@ describe('GfmAutoComplete', () => {
const expectContacts = ({ input, output }) => {
triggerDropdown(input);
- expect(getDropdownItems()).toEqual(output.map((contact) => contact.email));
+ expect(getDropdownItems()).toEqual(
+ output.map((contact) => `${contact.first_name} ${contact.last_name} ${contact.email}`),
+ );
};
describe('with no contacts assigned', () => {
diff --git a/spec/frontend/group_settings/components/shared_runners_form_spec.js b/spec/frontend/group_settings/components/shared_runners_form_spec.js
index 5282c0ed839..85475c749b0 100644
--- a/spec/frontend/group_settings/components/shared_runners_form_spec.js
+++ b/spec/frontend/group_settings/components/shared_runners_form_spec.js
@@ -10,7 +10,7 @@ jest.mock('~/api/groups_api');
const GROUP_ID = '99';
const RUNNER_ENABLED_VALUE = 'enabled';
const RUNNER_DISABLED_VALUE = 'disabled_and_unoverridable';
-const RUNNER_ALLOW_OVERRIDE_VALUE = 'disabled_with_override';
+const RUNNER_ALLOW_OVERRIDE_VALUE = 'disabled_and_overridable';
describe('group_settings/components/shared_runners_form', () => {
let wrapper;
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 211fee31a9c..9092d73571b 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -122,7 +122,7 @@ describe('RepoEditor', () => {
vm.$once('editorSetup', resolve);
});
- const createComponent = async ({ state = {}, activeFile = dummyFile.text, flags = {} } = {}) => {
+ const createComponent = async ({ state = {}, activeFile = dummyFile.text } = {}) => {
const store = prepareStore(state, activeFile);
wrapper = shallowMount(RepoEditor, {
store,
@@ -132,9 +132,6 @@ describe('RepoEditor', () => {
mocks: {
ContentViewer,
},
- provide: {
- glFeatures: flags,
- },
});
await waitForPromises();
vm = wrapper.vm;
@@ -196,12 +193,8 @@ describe('RepoEditor', () => {
});
describe('schema registration for .gitlab-ci.yml', () => {
- const setup = async (activeFile, flagIsOn = true) => {
- await createComponent({
- flags: {
- schemaLinting: flagIsOn,
- },
- });
+ const setup = async (activeFile) => {
+ await createComponent();
vm.editor.registerCiSchema = jest.fn();
if (activeFile) {
wrapper.setProps({ file: activeFile });
@@ -210,15 +203,13 @@ describe('RepoEditor', () => {
await nextTick();
};
it.each`
- flagIsOn | activeFile | shouldUseExtension | desc
- ${false} | ${dummyFile.markdown} | ${false} | ${`file is not CI config; should NOT`}
- ${true} | ${dummyFile.markdown} | ${false} | ${`file is not CI config; should NOT`}
- ${false} | ${dummyFile.ciConfig} | ${false} | ${`file is CI config; should NOT`}
- ${true} | ${dummyFile.ciConfig} | ${true} | ${`file is CI config; should`}
+ activeFile | shouldUseExtension | desc
+ ${dummyFile.markdown} | ${false} | ${`file is not CI config; should NOT`}
+ ${dummyFile.ciConfig} | ${true} | ${`file is CI config; should`}
`(
- 'when the flag is "$flagIsOn", $desc use extension',
- async ({ flagIsOn, activeFile, shouldUseExtension }) => {
- await setup(activeFile, flagIsOn);
+ 'when the activeFile is "$activeFile", $desc use extension',
+ async ({ activeFile, shouldUseExtension }) => {
+ await setup(activeFile);
if (shouldUseExtension) {
expect(applyExtensionSpy).toHaveBeenCalledWith({
diff --git a/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js
index 4b4e96f3b41..ed67a0948e4 100644
--- a/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js
+++ b/spec/frontend/ide/lib/gitlab_web_ide/get_base_config_spec.js
@@ -3,20 +3,32 @@ import { TEST_HOST } from 'helpers/test_constants';
const TEST_GITLAB_WEB_IDE_PUBLIC_PATH = 'test/gitlab-web-ide/public/path';
const TEST_GITLAB_URL = 'https://gdk.test/';
+const TEST_RELATIVE_URL_ROOT = '/gl_rel_root';
describe('~/ide/lib/gitlab_web_ide/get_base_config', () => {
- it('returns base properties for @gitlab/web-ide config', () => {
+ beforeEach(() => {
// why: add trailing "/" to test that it gets removed
process.env.GITLAB_WEB_IDE_PUBLIC_PATH = `${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}/`;
window.gon.gitlab_url = TEST_GITLAB_URL;
+ window.gon.relative_url_root = '';
+ });
- // act
+ it('with default, returns base properties for @gitlab/web-ide config', () => {
const actual = getBaseConfig();
- // asset
expect(actual).toEqual({
baseUrl: `${TEST_HOST}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
gitlabUrl: TEST_GITLAB_URL,
});
});
+
+ it('with relative_url_root, returns baseUrl with relative url root', () => {
+ window.gon.relative_url_root = TEST_RELATIVE_URL_ROOT;
+
+ const actual = getBaseConfig();
+
+ expect(actual).toMatchObject({
+ baseUrl: `${TEST_HOST}${TEST_RELATIVE_URL_ROOT}/${TEST_GITLAB_WEB_IDE_PUBLIC_PATH}`,
+ });
+ });
});
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index 4e8467de759..8601e13f7ca 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -366,17 +366,38 @@ describe('IDE commit module actions', () => {
});
describe('merge request', () => {
- it('redirects to new merge request page', async () => {
- jest.spyOn(eventHub, '$on').mockImplementation();
+ it.each`
+ branchName | targetBranchName | branchNameInURL | targetBranchInURL
+ ${'foo'} | ${'main'} | ${'foo'} | ${'main'}
+ ${'foo#bar'} | ${'main'} | ${'foo%23bar'} | ${'main'}
+ ${'foo#bar'} | ${'not#so#main'} | ${'foo%23bar'} | ${'not%23so%23main'}
+ `(
+ 'redirects to the correct new MR page when new branch is "$branchName" and target branch is "$targetBranchName"',
+ async ({ branchName, targetBranchName, branchNameInURL, targetBranchInURL }) => {
+ Object.assign(store.state.projects.abcproject, {
+ branches: {
+ [targetBranchName]: {
+ name: targetBranchName,
+ workingReference: '1',
+ commit: {
+ id: TEST_COMMIT_SHA,
+ },
+ can_push: true,
+ },
+ },
+ });
+ store.state.currentBranchId = targetBranchName;
+ store.state.commit.newBranchName = branchName;
- store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
- store.state.commit.shouldCreateMR = true;
+ store.state.commit.commitAction = COMMIT_TO_NEW_BRANCH;
+ store.state.commit.shouldCreateMR = true;
- await store.dispatch('commit/commitChanges');
- expect(visitUrl).toHaveBeenCalledWith(
- `webUrl/-/merge_requests/new?merge_request[source_branch]=${store.getters['commit/placeholderBranchName']}&merge_request[target_branch]=main&nav_source=webide`,
- );
- });
+ await store.dispatch('commit/commitChanges');
+ expect(visitUrl).toHaveBeenCalledWith(
+ `webUrl/-/merge_requests/new?merge_request[source_branch]=${branchNameInURL}&merge_request[target_branch]=${targetBranchInURL}&nav_source=webide`,
+ );
+ },
+ );
it('does not redirect to new merge request page when shouldCreateMR is not checked', async () => {
jest.spyOn(eventHub, '$on').mockImplementation();
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
index 8d21088bcaf..09be1e333b3 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
@@ -10,7 +10,11 @@ import {
import * as messages from '~/ide/stores/modules/terminal/messages';
import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import axios from '~/lib/utils/axios_utils';
-import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_FORBIDDEN,
+ HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
+} from '~/lib/utils/http_status';
const TEST_PROJECT_PATH = 'lorem/root';
const TEST_BRANCH_ID = 'main';
@@ -102,7 +106,7 @@ describe('IDE store terminal check actions', () => {
);
});
- [httpStatus.FORBIDDEN, httpStatus.NOT_FOUND].forEach((status) => {
+ [HTTP_STATUS_FORBIDDEN, HTTP_STATUS_NOT_FOUND].forEach((status) => {
it(`hides tab, when status is ${status}`, () => {
const payload = { response: { status } };
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
index df365442c67..9fd5f1a38d7 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js
@@ -6,7 +6,7 @@ import { STARTING, PENDING, STOPPING, STOPPED } from '~/ide/stores/modules/termi
import * as messages from '~/ide/stores/modules/terminal/messages';
import * as mutationTypes from '~/ide/stores/modules/terminal/mutation_types';
import axios from '~/lib/utils/axios_utils';
-import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -285,7 +285,7 @@ describe('IDE store terminal session controls actions', () => {
);
});
- [httpStatus.NOT_FOUND, HTTP_STATUS_UNPROCESSABLE_ENTITY].forEach((status) => {
+ [HTTP_STATUS_NOT_FOUND, HTTP_STATUS_UNPROCESSABLE_ENTITY].forEach((status) => {
it(`dispatches request and startSession on ${status}`, () => {
mock
.onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' })
diff --git a/spec/frontend/ide/stores/modules/terminal/messages_spec.js b/spec/frontend/ide/stores/modules/terminal/messages_spec.js
index 2a802d6b4af..f99496a4b98 100644
--- a/spec/frontend/ide/stores/modules/terminal/messages_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/messages_spec.js
@@ -1,7 +1,11 @@
import { escape } from 'lodash';
import { TEST_HOST } from 'spec/test_constants';
import * as messages from '~/ide/stores/modules/terminal/messages';
-import httpStatus, { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_FORBIDDEN,
+ HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
+} from '~/lib/utils/http_status';
import { sprintf } from '~/locale';
const TEST_HELP_URL = `${TEST_HOST}/help`;
@@ -26,13 +30,13 @@ describe('IDE store terminal messages', () => {
});
it('returns permission error, with status FORBIDDEN', () => {
- const result = messages.configCheckError(httpStatus.FORBIDDEN, TEST_HELP_URL);
+ const result = messages.configCheckError(HTTP_STATUS_FORBIDDEN, TEST_HELP_URL);
expect(result).toBe(messages.ERROR_PERMISSION);
});
it('returns unexpected error, with unexpected status', () => {
- const result = messages.configCheckError(httpStatus.NOT_FOUND, TEST_HELP_URL);
+ const result = messages.configCheckError(HTTP_STATUS_NOT_FOUND, TEST_HELP_URL);
expect(result).toBe(messages.UNEXPECTED_ERROR_CONFIG);
});
diff --git a/spec/frontend/import_entities/components/import_status_spec.js b/spec/frontend/import_entities/components/import_status_spec.js
index 686a21e3923..56c4ed827d7 100644
--- a/spec/frontend/import_entities/components/import_status_spec.js
+++ b/spec/frontend/import_entities/components/import_status_spec.js
@@ -18,6 +18,7 @@ describe('Import entities status component', () => {
describe('success status', () => {
const getStatusText = () => wrapper.findComponent(GlBadge).text();
+ const getStatusIcon = () => wrapper.findComponent(GlBadge).props('icon');
it('displays finished status as complete when no stats are provided', () => {
createComponent({
@@ -38,6 +39,7 @@ describe('Import entities status component', () => {
});
expect(getStatusText()).toBe('Complete');
+ expect(getStatusIcon()).toBe('status-success');
});
it('displays finished status as partial when all stats items were processed', () => {
@@ -52,6 +54,7 @@ describe('Import entities status component', () => {
});
expect(getStatusText()).toBe('Partial import');
+ expect(getStatusIcon()).toBe('status-alert');
});
});
@@ -105,9 +108,9 @@ describe('Import entities status component', () => {
const getStatusIcon = () =>
wrapper.findComponent(GlAccordionItem).findComponent(GlIcon).props().name;
- const createComponentWithStats = ({ fetched, imported }) => {
+ const createComponentWithStats = ({ fetched, imported, status = 'created' }) => {
createComponent({
- status: 'created',
+ status,
stats: {
fetched: { label: fetched },
imported: { label: imported },
@@ -124,7 +127,7 @@ describe('Import entities status component', () => {
expect(getStatusIcon()).toBe('status-scheduled');
});
- it('displays running status when imported is not equal to fetched', () => {
+ it('displays running status when imported is not equal to fetched and import is not finished', () => {
createComponentWithStats({
fetched: 100,
imported: 10,
@@ -133,6 +136,16 @@ describe('Import entities status component', () => {
expect(getStatusIcon()).toBe('status-running');
});
+ it('displays alert status when imported is not equal to fetched and import is finished', () => {
+ createComponentWithStats({
+ fetched: 100,
+ imported: 10,
+ status: STATUSES.FINISHED,
+ });
+
+ expect(getStatusIcon()).toBe('status-alert');
+ });
+
it('displays success status when imported is equal to fetched', () => {
createComponentWithStats({
fetched: 100,
diff --git a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
index cd56f573011..da7fb4e060d 100644
--- a/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlIcon } from '@gitlab/ui';
+import { GlButton, GlIcon, GlDropdown, GlDropdownItem } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import ImportActionsCell from '~/import_entities/import_groups/components/import_actions_cell.vue';
@@ -8,6 +8,7 @@ describe('import actions cell', () => {
const createComponent = (props) => {
wrapper = shallowMount(ImportActionsCell, {
propsData: {
+ isProjectsImportEnabled: false,
isFinished: false,
isAvailableForImport: false,
isInvalid: false,
@@ -78,4 +79,39 @@ describe('import actions cell', () => {
expect(wrapper.emitted('import-group')).toHaveLength(1);
});
+
+ describe.each`
+ isFinished | expectedAction
+ ${false} | ${'Import'}
+ ${true} | ${'Re-import'}
+ `(
+ 'when import projects is enabled, group is available for import and finish status is $status',
+ ({ isFinished, expectedAction }) => {
+ beforeEach(() => {
+ createComponent({ isProjectsImportEnabled: true, isAvailableForImport: true, isFinished });
+ });
+
+ it('render import dropdown', () => {
+ const dropdown = wrapper.findComponent(GlDropdown);
+ expect(dropdown.props('text')).toBe(`${expectedAction} with projects`);
+ expect(dropdown.findComponent(GlDropdownItem).text()).toBe(
+ `${expectedAction} without projects`,
+ );
+ });
+
+ it('request migrate projects by default', async () => {
+ const dropdown = wrapper.findComponent(GlDropdown);
+ dropdown.vm.$emit('click');
+
+ expect(wrapper.emitted('import-group')[0]).toStrictEqual([{ migrateProjects: true }]);
+ });
+
+ it('request not to migrate projects via dropdown option', async () => {
+ const dropdown = wrapper.findComponent(GlDropdown);
+ dropdown.findComponent(GlDropdownItem).vm.$emit('click');
+
+ expect(wrapper.emitted('import-group')[0]).toStrictEqual([{ migrateProjects: false }]);
+ });
+ },
+ );
});
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 f7a97f22d44..bd79e20e698 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
@@ -7,7 +7,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { createAlert } from '~/flash';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status';
import axios from '~/lib/utils/axios_utils';
import { STATUSES } from '~/import_entities/constants';
import { i18n, ROOT_NAMESPACE } from '~/import_entities/import_groups/constants';
@@ -49,6 +49,8 @@ describe('import table', () => {
const findImportSelectedButton = () =>
wrapper.findAll('button').wrappers.find((w) => w.text() === 'Import selected');
+ const findImportSelectedDropdown = () =>
+ wrapper.findAll('.gl-dropdown').wrappers.find((w) => w.text().includes('Import with projects'));
const findImportButtons = () =>
wrapper.findAll('button').wrappers.filter((w) => w.text() === 'Import');
const findPaginationDropdown = () => wrapper.find('[data-testid="page-size"]');
@@ -64,7 +66,12 @@ describe('import table', () => {
const selectRow = (idx) =>
wrapper.findAll('tbody td input[type=checkbox]').at(idx).setChecked(true);
- const createComponent = ({ bulkImportSourceGroups, importGroups, defaultTargetNamespace }) => {
+ const createComponent = ({
+ bulkImportSourceGroups,
+ importGroups,
+ defaultTargetNamespace,
+ glFeatures = {},
+ }) => {
apolloProvider = createMockApollo(
[
[
@@ -93,6 +100,9 @@ describe('import table', () => {
directives: {
GlTooltip: createMockDirective(),
},
+ provide: {
+ glFeatures,
+ },
apolloProvider,
});
};
@@ -258,7 +268,7 @@ describe('import table', () => {
},
});
- axiosMock.onPost('/import/bulk_imports.json').reply(httpStatus.BAD_REQUEST);
+ axiosMock.onPost('/import/bulk_imports.json').reply(HTTP_STATUS_BAD_REQUEST);
await waitForPromises();
await findImportButtons()[0].trigger('click');
@@ -530,16 +540,16 @@ describe('import table', () => {
mutation: importGroupsMutation,
variables: {
importRequests: [
- {
+ expect.objectContaining({
targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
newName: NEW_GROUPS[0].lastImportTarget.newName,
sourceGroupId: NEW_GROUPS[0].id,
- },
- {
+ }),
+ expect.objectContaining({
targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
newName: NEW_GROUPS[1].lastImportTarget.newName,
sourceGroupId: NEW_GROUPS[1].id,
- },
+ }),
],
},
});
@@ -610,4 +620,83 @@ describe('import table', () => {
expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
});
});
+
+ describe('when import projects is enabled', () => {
+ const NEW_GROUPS = [
+ generateFakeEntry({ id: 1, status: STATUSES.NONE }),
+ generateFakeEntry({ id: 2, status: STATUSES.NONE }),
+ generateFakeEntry({ id: 3, status: STATUSES.FINISHED }),
+ ];
+
+ beforeEach(() => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: NEW_GROUPS,
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ glFeatures: {
+ bulkImportProjects: true,
+ },
+ });
+ jest.spyOn(apolloProvider.defaultClient, 'mutate');
+ return waitForPromises();
+ });
+
+ it('renders import all dropdown', async () => {
+ expect(findImportSelectedDropdown().exists()).toBe(true);
+ });
+
+ it('includes migrateProjects: true when dropdown is clicked', async () => {
+ await selectRow(0);
+ await selectRow(1);
+ await nextTick();
+ await findImportSelectedDropdown().find('button').trigger('click');
+ expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
+ mutation: importGroupsMutation,
+ variables: {
+ importRequests: [
+ expect.objectContaining({
+ targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
+ newName: NEW_GROUPS[0].lastImportTarget.newName,
+ sourceGroupId: NEW_GROUPS[0].id,
+ migrateProjects: true,
+ }),
+ expect.objectContaining({
+ targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
+ newName: NEW_GROUPS[1].lastImportTarget.newName,
+ sourceGroupId: NEW_GROUPS[1].id,
+ migrateProjects: true,
+ }),
+ ],
+ },
+ });
+ });
+
+ it('includes migrateProjects: false when dropdown item is clicked', async () => {
+ await selectRow(0);
+ await selectRow(1);
+ await nextTick();
+ await findImportSelectedDropdown().find('.gl-dropdown-item button').trigger('click');
+ expect(apolloProvider.defaultClient.mutate).toHaveBeenCalledWith({
+ mutation: importGroupsMutation,
+ variables: {
+ importRequests: [
+ expect.objectContaining({
+ targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
+ newName: NEW_GROUPS[0].lastImportTarget.newName,
+ sourceGroupId: NEW_GROUPS[0].id,
+ migrateProjects: false,
+ }),
+ expect.objectContaining({
+ targetNamespace: AVAILABLE_NAMESPACES[0].fullPath,
+ newName: NEW_GROUPS[1].lastImportTarget.newName,
+ sourceGroupId: NEW_GROUPS[1].id,
+ migrateProjects: false,
+ }),
+ ],
+ },
+ });
+ });
+ });
});
diff --git a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
index adc4ebcffb8..ce111a0c10c 100644
--- a/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
+++ b/spec/frontend/import_entities/import_groups/graphql/client_factory_spec.js
@@ -13,7 +13,7 @@ import updateImportStatusMutation from '~/import_entities/import_groups/graphql/
import bulkImportSourceGroupsQuery from '~/import_entities/import_groups/graphql/queries/bulk_import_source_groups.query.graphql';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { statusEndpointFixture } from './fixtures';
jest.mock('~/flash');
@@ -52,7 +52,7 @@ describe('Bulk import resolvers', () => {
axiosMockAdapter = new MockAdapter(axios);
client = createClient();
- axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(httpStatus.OK, statusEndpointFixture);
+ axiosMockAdapter.onGet(FAKE_ENDPOINTS.status).reply(HTTP_STATUS_OK, statusEndpointFixture);
client.watchQuery({ query: bulkImportSourceGroupsQuery }).subscribe(({ data }) => {
results = data.bulkImportSourceGroups.nodes;
});
@@ -143,7 +143,7 @@ describe('Bulk import resolvers', () => {
it('sets import status to CREATED for successful groups when request completes', async () => {
axiosMockAdapter
.onPost(FAKE_ENDPOINTS.createBulkImport)
- .reply(httpStatus.OK, [{ success: true, id: 1 }]);
+ .reply(HTTP_STATUS_OK, [{ success: true, id: 1 }]);
await client.mutate({
mutation: importGroupsMutation,
@@ -163,7 +163,7 @@ describe('Bulk import resolvers', () => {
});
it('sets import status to CREATED for successful groups when request completes with legacy response', async () => {
- axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(httpStatus.OK, { id: 1 });
+ axiosMockAdapter.onPost(FAKE_ENDPOINTS.createBulkImport).reply(HTTP_STATUS_OK, { id: 1 });
await client.mutate({
mutation: importGroupsMutation,
@@ -186,7 +186,7 @@ describe('Bulk import resolvers', () => {
const FAKE_ERROR_MESSAGE = 'foo';
axiosMockAdapter
.onPost(FAKE_ENDPOINTS.createBulkImport)
- .reply(httpStatus.OK, [{ success: false, id: 1, message: FAKE_ERROR_MESSAGE }]);
+ .reply(HTTP_STATUS_OK, [{ success: false, id: 1, message: FAKE_ERROR_MESSAGE }]);
await client.mutate({
mutation: importGroupsMutation,
@@ -210,7 +210,7 @@ describe('Bulk import resolvers', () => {
it('updateImportStatus updates status', async () => {
axiosMockAdapter
.onPost(FAKE_ENDPOINTS.createBulkImport)
- .reply(httpStatus.OK, [{ success: true, id: 1 }]);
+ .reply(HTTP_STATUS_OK, [{ success: true, id: 1 }]);
const NEW_STATUS = 'dummy';
await client.mutate({
diff --git a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
index 08c407cc4b4..1d1b285c1b6 100644
--- a/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
+++ b/spec/frontend/incidents_settings/components/incidents_settings_service_spec.js
@@ -3,7 +3,7 @@ import { createAlert } from '~/flash';
import { ERROR_MSG } from '~/incidents_settings/constants';
import IncidentsSettingsService from '~/incidents_settings/incidents_settings_service';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
jest.mock('~/flash');
@@ -26,7 +26,7 @@ describe('IncidentsSettingsService', () => {
describe('updateSettings', () => {
it('should refresh the page on successful update', () => {
- mock.onPatch().reply(httpStatusCodes.OK);
+ mock.onPatch().reply(HTTP_STATUS_OK);
return service.updateSettings({}).then(() => {
expect(refreshCurrentPage).toHaveBeenCalled();
@@ -34,7 +34,7 @@ describe('IncidentsSettingsService', () => {
});
it('should display a flash message on update error', () => {
- mock.onPatch().reply(httpStatusCodes.BAD_REQUEST);
+ mock.onPatch().reply(HTTP_STATUS_BAD_REQUEST);
return service.updateSettings({}).then(() => {
expect(createAlert).toHaveBeenCalledWith({
@@ -47,7 +47,7 @@ describe('IncidentsSettingsService', () => {
describe('resetWebhookUrl', () => {
it('should make a call for webhook update', () => {
jest.spyOn(axios, 'post');
- mock.onPost().reply(httpStatusCodes.OK);
+ mock.onPost().reply(HTTP_STATUS_OK);
return service.resetWebhookUrl().then(() => {
expect(axios.post).toHaveBeenCalledWith(webhookUpdateEndpoint);
diff --git a/spec/frontend/integrations/edit/components/integration_form_spec.js b/spec/frontend/integrations/edit/components/integration_form_spec.js
index 4b49e492880..383dfb36aa5 100644
--- a/spec/frontend/integrations/edit/components/integration_form_spec.js
+++ b/spec/frontend/integrations/edit/components/integration_form_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlBadge, GlForm } from '@gitlab/ui';
+import { GlAlert, GlForm } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
@@ -11,18 +11,16 @@ import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
import IntegrationForm from '~/integrations/edit/components/integration_form.vue';
import OverrideDropdown from '~/integrations/edit/components/override_dropdown.vue';
import TriggerFields from '~/integrations/edit/components/trigger_fields.vue';
-import IntegrationSectionConnection from '~/integrations/edit/components/sections/connection.vue';
import IntegrationFormActions from '~/integrations/edit/components/integration_form_actions.vue';
+import IntegrationFormSection from '~/integrations/edit/components/integration_forms/section.vue';
import {
I18N_SUCCESSFUL_CONNECTION_MESSAGE,
I18N_DEFAULT_ERROR_MESSAGE,
INTEGRATION_FORM_TYPE_SLACK,
- billingPlans,
- billingPlanNames,
} from '~/integrations/constants';
import { createStore } from '~/integrations/edit/store';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import {
mockIntegrationProps,
@@ -73,15 +71,11 @@ describe('IntegrationForm', () => {
const findActiveCheckbox = () => wrapper.findComponent(ActiveCheckbox);
const findTriggerFields = () => wrapper.findComponent(TriggerFields);
const findAlert = () => wrapper.findComponent(GlAlert);
- const findGlBadge = () => wrapper.findComponent(GlBadge);
const findGlForm = () => wrapper.findComponent(GlForm);
const findRedirectToField = () => wrapper.findByTestId('redirect-to-field');
const findDynamicField = () => wrapper.findComponent(DynamicField);
const findAllDynamicFields = () => wrapper.findAllComponents(DynamicField);
- const findAllSections = () => wrapper.findAllByTestId('integration-section');
- const findConnectionSection = () => findAllSections().at(0);
- const findConnectionSectionComponent = () =>
- findConnectionSection().findComponent(IntegrationSectionConnection);
+ const findAllSections = () => wrapper.findAllComponents(IntegrationFormSection);
const findHelpHtml = () => wrapper.findByTestId('help-html');
const findFormActions = () => wrapper.findComponent(IntegrationFormActions);
@@ -215,54 +209,13 @@ describe('IntegrationForm', () => {
beforeEach(() => {
createComponent({
customStateProps: {
- sections: [mockSectionConnection],
- },
- });
- });
-
- it('renders the expected number of sections', () => {
- expect(findAllSections().length).toBe(1);
- });
-
- it('renders title, description and the correct dynamic component', () => {
- const connectionSection = findConnectionSection();
-
- expect(connectionSection.find('h4').text()).toBe(mockSectionConnection.title);
- expect(connectionSection.find('p').text()).toBe(mockSectionConnection.description);
- expect(findGlBadge().exists()).toBe(false);
- expect(findConnectionSectionComponent().exists()).toBe(true);
- });
-
- it('renders GlBadge when `plan` is present', () => {
- createComponent({
- customStateProps: {
sections: [mockSectionConnection, mockSectionJiraIssues],
},
});
-
- expect(findGlBadge().exists()).toBe(true);
- expect(findGlBadge().text()).toMatchInterpolatedText(billingPlanNames[billingPlans.PREMIUM]);
});
- it('passes only fields with section type', () => {
- const sectionFields = [
- { name: 'username', type: 'text', section: mockSectionConnection.type },
- { name: 'API token', type: 'password', section: mockSectionConnection.type },
- ];
-
- const nonSectionFields = [
- { name: 'branch', type: 'text' },
- { name: 'labels', type: 'select' },
- ];
-
- createComponent({
- customStateProps: {
- sections: [mockSectionConnection],
- fields: [...sectionFields, ...nonSectionFields],
- },
- });
-
- expect(findConnectionSectionComponent().props('fields')).toEqual(sectionFields);
+ it('renders the expected number of sections', () => {
+ expect(findAllSections()).toHaveLength(2);
});
describe.each`
@@ -281,7 +234,8 @@ describe('IntegrationForm', () => {
},
});
- findConnectionSectionComponent().vm.$emit('toggle-integration-active', formActive);
+ const section = findAllSections().at(0);
+ section.vm.$emit('toggle-integration-active', formActive);
});
it(`sets noValidate to ${novalidate}`, () => {
@@ -290,7 +244,7 @@ describe('IntegrationForm', () => {
},
);
- describe('when IntegrationSectionConnection emits `request-jira-issue-types` event', () => {
+ describe('when section emits `request-jira-issue-types` event', () => {
beforeEach(() => {
jest.spyOn(document, 'querySelector').mockReturnValue(document.createElement('form'));
@@ -302,7 +256,8 @@ describe('IntegrationForm', () => {
mountFn: mountExtended,
});
- findConnectionSectionComponent().vm.$emit('request-jira-issue-types');
+ const section = findAllSections().at(0);
+ section.vm.$emit('request-jira-issue-types');
});
it('dispatches `requestJiraIssueTypes` action', () => {
@@ -456,11 +411,11 @@ describe('IntegrationForm', () => {
});
describe.each`
- scenario | replyStatus | errorMessage | serviceResponse | expectToast | expectSentry
- ${'when "test settings" request fails'} | ${httpStatus.INTERNAL_SERVER_ERROR} | ${undefined} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true}
- ${'when "test settings" returns an error'} | ${httpStatus.OK} | ${'an error'} | ${undefined} | ${'an error'} | ${false}
- ${'when "test settings" returns an error with details'} | ${httpStatus.OK} | ${'an error.'} | ${'extra info'} | ${'an error. extra info'} | ${false}
- ${'when "test settings" succeeds'} | ${httpStatus.OK} | ${undefined} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false}
+ scenario | replyStatus | errorMessage | serviceResponse | expectToast | expectSentry
+ ${'when "test settings" request fails'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR} | ${undefined} | ${undefined} | ${I18N_DEFAULT_ERROR_MESSAGE} | ${true}
+ ${'when "test settings" returns an error'} | ${HTTP_STATUS_OK} | ${'an error'} | ${undefined} | ${'an error'} | ${false}
+ ${'when "test settings" returns an error with details'} | ${HTTP_STATUS_OK} | ${'an error.'} | ${'extra info'} | ${'an error. extra info'} | ${false}
+ ${'when "test settings" succeeds'} | ${HTTP_STATUS_OK} | ${undefined} | ${undefined} | ${I18N_SUCCESSFUL_CONNECTION_MESSAGE} | ${false}
`(
'$scenario',
({ replyStatus, errorMessage, serviceResponse, expectToast, expectSentry }) => {
@@ -491,7 +446,7 @@ describe('IntegrationForm', () => {
const mockResetPath = '/reset';
beforeEach(async () => {
- mockAxios.onPost(mockResetPath).replyOnce(httpStatus.INTERNAL_SERVER_ERROR);
+ mockAxios.onPost(mockResetPath).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent({
customStateProps: {
resetPath: mockResetPath,
@@ -526,7 +481,7 @@ describe('IntegrationForm', () => {
describe('when "reset settings" succeeds', () => {
beforeEach(async () => {
- mockAxios.onPost(mockResetPath).replyOnce(httpStatus.OK);
+ mockAxios.onPost(mockResetPath).replyOnce(HTTP_STATUS_OK);
createComponent({
customStateProps: {
resetPath: mockResetPath,
diff --git a/spec/frontend/integrations/edit/components/integration_forms/section_spec.js b/spec/frontend/integrations/edit/components/integration_forms/section_spec.js
new file mode 100644
index 00000000000..5f82941778e
--- /dev/null
+++ b/spec/frontend/integrations/edit/components/integration_forms/section_spec.js
@@ -0,0 +1,109 @@
+import { GlBadge } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { billingPlans, billingPlanNames } from '~/integrations/constants';
+import DynamicField from '~/integrations/edit/components/dynamic_field.vue';
+import IntegrationFormSection from '~/integrations/edit/components/integration_forms/section.vue';
+import IntegrationSectionConnection from '~/integrations/edit/components/sections/connection.vue';
+import { createStore } from '~/integrations/edit/store';
+import {
+ mockIntegrationProps,
+ mockSectionConnection,
+ mockSectionJiraIssues,
+} from '../../mock_data';
+
+describe('Integration Form Section', () => {
+ let wrapper;
+
+ const defaultProps = {
+ section: mockSectionConnection,
+ isValidated: false,
+ };
+
+ const createComponent = ({
+ customStateProps = {},
+ props = {},
+ mountFn = shallowMountExtended,
+ } = {}) => {
+ const store = createStore({
+ customState: {
+ ...mockIntegrationProps,
+ ...customStateProps,
+ },
+ });
+
+ wrapper = mountFn(IntegrationFormSection, {
+ store,
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ stubs: {
+ IntegrationSectionConnection,
+ },
+ });
+ };
+
+ const findGlBadge = () => wrapper.findComponent(GlBadge);
+ const findFieldsComponent = () => wrapper.findComponent(IntegrationSectionConnection);
+ const findAllDynamicFields = () => wrapper.findAllComponents(DynamicField);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders title, description and the correct dynamic component', () => {
+ expect(wrapper.findByText(mockSectionConnection.title).exists()).toBe(true);
+ expect(wrapper.findByText(mockSectionConnection.description).exists()).toBe(true);
+ expect(findGlBadge().exists()).toBe(false);
+ });
+
+ it('renders GlBadge when `plan` is present', () => {
+ createComponent({
+ props: {
+ section: mockSectionJiraIssues,
+ },
+ });
+
+ expect(findGlBadge().exists()).toBe(true);
+ expect(findGlBadge().text()).toMatchInterpolatedText(billingPlanNames[billingPlans.PREMIUM]);
+ });
+
+ it('renders only fields for this section type', () => {
+ const sectionFields = [
+ { name: 'username', type: 'text', section: mockSectionConnection.type },
+ { name: 'API token', type: 'password', section: mockSectionConnection.type },
+ ];
+
+ const nonSectionFields = [{ name: 'branch', type: 'text' }];
+
+ createComponent({
+ customStateProps: {
+ fields: [...sectionFields, ...nonSectionFields],
+ },
+ });
+
+ expect(findAllDynamicFields()).toHaveLength(2);
+ sectionFields.forEach((field, index) => {
+ expect(findAllDynamicFields().at(index).props('name')).toBe(field.name);
+ });
+ });
+
+ describe('events proxy from the section', () => {
+ let section;
+ const dummyPayload = 'foo';
+
+ beforeEach(() => {
+ section = findFieldsComponent();
+ });
+
+ it('toggle-integration-active', () => {
+ section.vm.$emit('toggle-integration-active', dummyPayload);
+ expect(wrapper.emitted('toggle-integration-active')).toEqual([[dummyPayload]]);
+ });
+
+ it('request-jira-issue-types', () => {
+ section.vm.$emit('request-jira-issue-types', dummyPayload);
+ expect(wrapper.emitted('request-jira-issue-types')).toEqual([[dummyPayload]]);
+ });
+ });
+});
diff --git a/spec/frontend/integrations/edit/components/trigger_field_spec.js b/spec/frontend/integrations/edit/components/trigger_field_spec.js
index 6a68337813e..ed0b3324708 100644
--- a/spec/frontend/integrations/edit/components/trigger_field_spec.js
+++ b/spec/frontend/integrations/edit/components/trigger_field_spec.js
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
import { shallowMount } from '@vue/test-utils';
-import { GlFormCheckbox } from '@gitlab/ui';
+import { GlFormCheckbox, GlFormInput } from '@gitlab/ui';
import TriggerField from '~/integrations/edit/components/trigger_field.vue';
import { integrationTriggerEventTitles } from '~/integrations/constants';
@@ -10,7 +10,9 @@ describe('TriggerField', () => {
const defaultProps = {
event: { name: 'push_events' },
+ type: 'gitlab_slack_application',
};
+ const mockField = { name: 'push_channel' };
const createComponent = ({ props = {}, isInheriting = false } = {}) => {
wrapper = shallowMount(TriggerField, {
@@ -26,6 +28,7 @@ describe('TriggerField', () => {
});
const findGlFormCheckbox = () => wrapper.findComponent(GlFormCheckbox);
+ const findGlFormInput = () => wrapper.findComponent(GlFormInput);
const findHiddenInput = () => wrapper.find('input[type="hidden"]');
describe('template', () => {
@@ -55,6 +58,32 @@ describe('TriggerField', () => {
expect(findHiddenInput().attributes('value')).toBe('false');
});
+ it('renders hidden GlFormInput', () => {
+ createComponent({
+ props: {
+ event: { name: 'push_events', field: mockField },
+ },
+ });
+
+ expect(findGlFormInput().exists()).toBe(true);
+ expect(findGlFormInput().isVisible()).toBe(false);
+ });
+
+ describe('checkbox is selected', () => {
+ it('renders visible GlFormInput', async () => {
+ createComponent({
+ props: {
+ event: { name: 'push_events', field: mockField },
+ },
+ });
+
+ await findGlFormCheckbox().vm.$emit('input', true);
+
+ expect(findGlFormInput().exists()).toBe(true);
+ expect(findGlFormInput().isVisible()).toBe(true);
+ });
+ });
+
it('toggles value of hidden input on checkbox input', async () => {
createComponent({
props: { event: { name: 'push_events', value: true } },
diff --git a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
index fd60d7f817f..fdb728281b5 100644
--- a/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
+++ b/spec/frontend/integrations/overrides/components/integration_overrides_spec.js
@@ -8,7 +8,7 @@ import IntegrationOverrides from '~/integrations/overrides/components/integratio
import IntegrationTabs from '~/integrations/overrides/components/integration_tabs.vue';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ProjectAvatar from '~/vue_shared/components/project_avatar.vue';
import UrlSync from '~/vue_shared/components/url_sync.vue';
@@ -39,7 +39,7 @@ describe('IntegrationOverrides', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
- mockAxios.onGet(defaultProps.overridesPath).reply(httpStatus.OK, mockOverrides, {
+ mockAxios.onGet(defaultProps.overridesPath).reply(HTTP_STATUS_OK, mockOverrides, {
'X-TOTAL': mockOverrides.length,
'X-PAGE': 1,
});
@@ -125,7 +125,7 @@ describe('IntegrationOverrides', () => {
describe('when request fails', () => {
beforeEach(async () => {
jest.spyOn(Sentry, 'captureException');
- mockAxios.onGet(defaultProps.overridesPath).reply(httpStatus.INTERNAL_SERVER_ERROR);
+ mockAxios.onGet(defaultProps.overridesPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
await waitForPromises();
@@ -150,7 +150,7 @@ describe('IntegrationOverrides', () => {
describe('pagination', () => {
describe('when total items does not exceed the page limit', () => {
it('does not render', async () => {
- mockAxios.onGet(defaultProps.overridesPath).reply(httpStatus.OK, [mockOverrides[0]], {
+ mockAxios.onGet(defaultProps.overridesPath).reply(HTTP_STATUS_OK, [mockOverrides[0]], {
'X-TOTAL': DEFAULT_PER_PAGE - 1,
'X-PAGE': 1,
});
@@ -169,7 +169,7 @@ describe('IntegrationOverrides', () => {
beforeEach(async () => {
createComponent({ stubs: { UrlSync } });
- mockAxios.onGet(defaultProps.overridesPath).reply(httpStatus.OK, [mockOverrides[0]], {
+ mockAxios.onGet(defaultProps.overridesPath).reply(HTTP_STATUS_OK, [mockOverrides[0]], {
'X-TOTAL': DEFAULT_PER_PAGE * 2,
'X-PAGE': mockPage,
});
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 22fcedb2eaf..b6b34e1063b 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -24,7 +24,11 @@ import {
import eventHub from '~/invite_members/event_hub';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
import axios from '~/lib/utils/axios_utils';
-import httpStatus, { HTTP_STATUS_CREATED } from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_BAD_REQUEST,
+ HTTP_STATUS_CREATED,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+} from '~/lib/utils/http_status';
import { getParameterValues } from '~/lib/utils/url_utility';
import {
displaySuccessfulInvitationAlert,
@@ -361,7 +365,7 @@ describe('InviteMembersModal', () => {
describe('rendering the user limit notification', () => {
it('shows the user limit notification alert when reached limit', () => {
- const usersLimitDataset = { reachedLimit: true };
+ const usersLimitDataset = { alertVariant: 'reached' };
createInviteMembersToProjectWrapper(usersLimitDataset);
@@ -369,7 +373,15 @@ describe('InviteMembersModal', () => {
});
it('shows the user limit notification alert when close to dashboard limit', () => {
- const usersLimitDataset = { closeToDashboardLimit: true };
+ const usersLimitDataset = { alertVariant: 'close' };
+
+ createInviteMembersToProjectWrapper(usersLimitDataset);
+
+ expect(findUserLimitAlert().exists()).toBe(true);
+ });
+
+ it('shows the user limit notification alert when :preview_free_user_cap is enabled', () => {
+ const usersLimitDataset = { alertVariant: 'notification' };
createInviteMembersToProjectWrapper(usersLimitDataset);
@@ -549,7 +561,7 @@ describe('InviteMembersModal', () => {
it('displays the generic error for http server error', async () => {
mockInvitationsApi(
- httpStatus.INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
'Request failed with status code 500',
);
@@ -648,7 +660,7 @@ describe('InviteMembersModal', () => {
});
it('displays the api error for invalid email syntax', async () => {
- mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
+ mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
clickInviteButton();
@@ -660,7 +672,7 @@ describe('InviteMembersModal', () => {
});
it('clears the error when the modal is hidden', async () => {
- mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
+ mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.EMAIL_INVALID);
clickInviteButton();
@@ -715,7 +727,7 @@ describe('InviteMembersModal', () => {
});
it('displays the invalid syntax error for bad request', async () => {
- mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.ERROR_EMAIL_INVALID);
+ mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.ERROR_EMAIL_INVALID);
clickInviteButton();
@@ -739,7 +751,7 @@ describe('InviteMembersModal', () => {
createInviteMembersToGroupWrapper();
await triggerMembersTokenSelect([user3, user4]);
- mockInvitationsApi(httpStatus.BAD_REQUEST, invitationsApiResponse.ERROR_EMAIL_INVALID);
+ mockInvitationsApi(HTTP_STATUS_BAD_REQUEST, invitationsApiResponse.ERROR_EMAIL_INVALID);
clickInviteButton();
diff --git a/spec/frontend/invite_members/components/user_limit_notification_spec.js b/spec/frontend/invite_members/components/user_limit_notification_spec.js
index 2a780490468..490b2e8bc7c 100644
--- a/spec/frontend/invite_members/components/user_limit_notification_spec.js
+++ b/spec/frontend/invite_members/components/user_limit_notification_spec.js
@@ -1,9 +1,14 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import UserLimitNotification from '~/invite_members/components/user_limit_notification.vue';
-import { REACHED_LIMIT_VARIANT, CLOSE_TO_LIMIT_VARIANT } from '~/invite_members/constants';
+import {
+ NOTIFICATION_LIMIT_VARIANT,
+ REACHED_LIMIT_VARIANT,
+ CLOSE_TO_LIMIT_VARIANT,
+} from '~/invite_members/constants';
import { freeUsersLimit, remainingSeats } from '../mock_data/member_modal';
+const INFO_ALERT_TITLE = 'Your top-level group name is over the 5 user limit.';
const WARNING_ALERT_TITLE = 'You only have space for 2 more members in name';
describe('UserLimitNotification', () => {
@@ -31,6 +36,17 @@ describe('UserLimitNotification', () => {
});
};
+ describe('when previewing free user cap', () => {
+ it("renders user's preview limit notification", () => {
+ createComponent(NOTIFICATION_LIMIT_VARIANT);
+
+ const alert = findAlert();
+
+ expect(alert.attributes('title')).toEqual(INFO_ALERT_TITLE);
+ expect(alert.text()).toContain('GitLab will enforce this limit in the future.');
+ });
+ });
+
describe('when close to limit within a group', () => {
it("renders user's limit notification", () => {
createComponent(CLOSE_TO_LIMIT_VARIANT);
@@ -51,7 +67,7 @@ describe('UserLimitNotification', () => {
expect(alert.attributes('title')).toEqual("You've reached your 5 members limit for name");
expect(alert.text()).toContain(
- 'To invite new users to this namespace, you must remove existing users.',
+ 'To invite new users to this top-level group, you must remove existing users.',
);
});
});
diff --git a/spec/frontend/issuable/components/issuable_by_email_spec.js b/spec/frontend/issuable/components/issuable_by_email_spec.js
index 01abf239e57..b04a6c0b8fd 100644
--- a/spec/frontend/issuable/components/issuable_by_email_spec.js
+++ b/spec/frontend/issuable/components/issuable_by_email_spec.js
@@ -5,7 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
const initialEmail = 'user@gitlab.com';
@@ -130,7 +130,7 @@ describe('IssuableByEmail', () => {
});
it('should update the email when the request succeeds', async () => {
- mockAxios.onPut(resetPath).reply(httpStatus.OK, { new_address: 'foo@bar.com' });
+ mockAxios.onPut(resetPath).reply(HTTP_STATUS_OK, { new_address: 'foo@bar.com' });
wrapper = createComponent({
issuableType: 'issue',
@@ -144,7 +144,7 @@ describe('IssuableByEmail', () => {
});
it('should show a toast message when the request fails', async () => {
- mockAxios.onPut(resetPath).reply(httpStatus.NOT_FOUND, {});
+ mockAxios.onPut(resetPath).reply(HTTP_STATUS_NOT_FOUND, {});
wrapper = createComponent({
issuableType: 'issue',
diff --git a/spec/frontend/issuable/components/issuable_header_warnings_spec.js b/spec/frontend/issuable/components/issuable_header_warnings_spec.js
index e3a36dc8820..99aa6778e1e 100644
--- a/spec/frontend/issuable/components/issuable_header_warnings_spec.js
+++ b/spec/frontend/issuable/components/issuable_header_warnings_spec.js
@@ -7,7 +7,7 @@ import createIssueStore from '~/notes/stores';
import IssuableHeaderWarnings from '~/issuable/components/issuable_header_warnings.vue';
const ISSUABLE_TYPE_ISSUE = 'issue';
-const ISSUABLE_TYPE_MR = 'merge request';
+const ISSUABLE_TYPE_MR = 'merge_request';
Vue.use(Vuex);
@@ -57,6 +57,7 @@ describe('IssuableHeaderWarnings', () => {
beforeEach(() => {
store.getters.getNoteableData.confidential = confidentialStatus;
store.getters.getNoteableData.discussion_locked = lockStatus;
+ store.getters.getNoteableData.targetType = issuableType;
createComponent({ store, provide: { hidden: hiddenStatus } });
});
@@ -84,7 +85,7 @@ describe('IssuableHeaderWarnings', () => {
if (hiddenStatus) {
expect(hiddenIcon.attributes('title')).toBe(
- 'This issue is hidden because its author has been banned',
+ `This ${issuableType.replace('_', ' ')} is hidden because its author has been banned`,
);
expect(getBinding(hiddenIcon.element, 'gl-tooltip')).not.toBeUndefined();
}
diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js
index 5e67ea42b87..28ec0e22d8b 100644
--- a/spec/frontend/issuable/issuable_form_spec.js
+++ b/spec/frontend/issuable/issuable_form_spec.js
@@ -35,8 +35,8 @@ describe('IssuableForm', () => {
let $description;
beforeEach(() => {
- $title = $form.find('input[name*="[title]"]');
- $description = $form.find('textarea[name*="[description]"]');
+ $title = $form.find('input[name*="[title]"]').get(0);
+ $description = $form.find('textarea[name*="[description]"]').get(0);
});
afterEach(() => {
@@ -103,7 +103,11 @@ describe('IssuableForm', () => {
createIssuable($form);
expect(Autosave).toHaveBeenCalledTimes(totalAutosaveFormFields);
- expect(Autosave).toHaveBeenLastCalledWith($input, ['/', '', id], `autosave///=${id}`);
+ expect(Autosave).toHaveBeenLastCalledWith(
+ $input.get(0),
+ ['/', '', id],
+ `autosave///=${id}`,
+ );
});
});
diff --git a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
index 3f40772f7fc..841cea28ffc 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -27,6 +27,9 @@ import { scrollUp } from '~/lib/utils/scroll_utils';
import {
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_LABEL,
+ TOKEN_TYPE_MILESTONE,
+ TOKEN_TYPE_MY_REACTION,
} from '~/vue_shared/components/filtered_search_bar/constants';
import IssuableList from '~/vue_shared/issuable/list/components/issuable_list_root.vue';
import { IssuableStates } from '~/vue_shared/issuable/list/constants';
@@ -42,8 +45,12 @@ describe('IssuesDashboardApp component', () => {
Vue.use(VueApollo);
const defaultProvide = {
+ autocompleteAwardEmojisPath: 'autocomplete/award/emojis/path',
calendarPath: 'calendar/path',
- emptyStateSvgPath: 'empty-state.svg',
+ dashboardLabelsPath: 'dashboard/labels/path',
+ dashboardMilestonesPath: 'dashboard/milestones/path',
+ emptyStateWithFilterSvgPath: 'empty/state/with/filter/svg/path.svg',
+ emptyStateWithoutFilterSvgPath: 'empty/state/with/filter/svg/path.svg',
hasBlockedIssuesFeature: true,
hasIssuableHealthStatusFeature: true,
hasIssueWeightsFeature: true,
@@ -97,74 +104,122 @@ describe('IssuesDashboardApp component', () => {
axiosMock.reset();
});
- it('renders IssuableList component', async () => {
- mountComponent();
- jest.runOnlyPendingTimers();
- await waitForPromises();
-
- expect(findIssuableList().props()).toMatchObject({
- currentTab: IssuableStates.Opened,
- hasNextPage: true,
- hasPreviousPage: false,
- hasScopedLabelsFeature: defaultProvide.hasScopedLabelsFeature,
- initialSortBy: CREATED_DESC,
- issuables: issuesQueryResponse.data.issues.nodes,
- issuablesLoading: false,
- namespace: 'dashboard',
- recentSearchesStorageKey: 'issues',
- searchInputPlaceholder: IssuesDashboardApp.i18n.searchInputPlaceholder,
- showPaginationControls: true,
- sortOptions: getSortOptions({
- hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
- hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
- hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature,
- }),
- tabs: IssuesDashboardApp.IssuableListTabs,
- urlParams: {
- sort: urlSortParams[CREATED_DESC],
- state: IssuableStates.Opened,
- },
- useKeysetPagination: true,
+ describe('UI components', () => {
+ beforeEach(() => {
+ setWindowLocation(locationSearch);
+ mountComponent();
+ jest.runOnlyPendingTimers();
+ return waitForPromises();
});
- });
- it('renders RSS button link', () => {
- mountComponent();
+ it('renders IssuableList component', () => {
+ expect(findIssuableList().props()).toMatchObject({
+ currentTab: IssuableStates.Opened,
+ hasNextPage: true,
+ hasPreviousPage: false,
+ hasScopedLabelsFeature: defaultProvide.hasScopedLabelsFeature,
+ initialSortBy: CREATED_DESC,
+ issuables: issuesQueryResponse.data.issues.nodes,
+ issuablesLoading: false,
+ namespace: 'dashboard',
+ recentSearchesStorageKey: 'issues',
+ searchInputPlaceholder: IssuesDashboardApp.i18n.searchInputPlaceholder,
+ showPaginationControls: true,
+ sortOptions: getSortOptions({
+ hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
+ hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
+ hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature,
+ }),
+ tabs: IssuesDashboardApp.IssuableListTabs,
+ urlParams: {
+ sort: urlSortParams[CREATED_DESC],
+ state: IssuableStates.Opened,
+ },
+ useKeysetPagination: true,
+ });
+ });
- expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
- expect(findRssButton().props('icon')).toBe('rss');
- });
+ it('renders RSS button link', () => {
+ expect(findRssButton().attributes('href')).toBe(defaultProvide.rssPath);
+ });
- it('renders calendar button link', () => {
- mountComponent();
+ it('renders calendar button link', () => {
+ expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
+ });
+
+ it('renders issue time information', () => {
+ expect(findIssueCardTimeInfo().exists()).toBe(true);
+ });
- expect(findCalendarButton().attributes('href')).toBe(defaultProvide.calendarPath);
- expect(findCalendarButton().props('icon')).toBe('calendar');
+ it('renders issue statistics', () => {
+ expect(findIssueCardStatistics().exists()).toBe(true);
+ });
});
- it('renders issue time information', async () => {
- mountComponent();
- jest.runOnlyPendingTimers();
- await waitForPromises();
+ describe('fetching issues', () => {
+ describe('with a search query', () => {
+ describe('when there are issues returned', () => {
+ beforeEach(() => {
+ setWindowLocation(locationSearch);
+ mountComponent();
+ jest.runOnlyPendingTimers();
+ return waitForPromises();
+ });
- expect(findIssueCardTimeInfo().exists()).toBe(true);
- });
+ it('renders the issues', () => {
+ expect(findIssuableList().props('issuables')).toEqual(
+ defaultQueryResponse.data.issues.nodes,
+ );
+ });
- it('renders issue statistics', async () => {
- mountComponent();
- jest.runOnlyPendingTimers();
- await waitForPromises();
+ it('does not render empty state', () => {
+ expect(findEmptyState().exists()).toBe(false);
+ });
+ });
- expect(findIssueCardStatistics().exists()).toBe(true);
- });
+ describe('when there are no issues returned', () => {
+ beforeEach(() => {
+ setWindowLocation(locationSearch);
+ mountComponent({
+ issuesQueryHandler: jest.fn().mockResolvedValue(emptyIssuesQueryResponse),
+ });
+ return waitForPromises();
+ });
+
+ it('renders no issues', () => {
+ expect(findIssuableList().props('issuables')).toEqual([]);
+ });
+
+ it('renders empty state', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ description: IssuesDashboardApp.i18n.emptyStateWithFilterDescription,
+ svgPath: defaultProvide.emptyStateWithFilterSvgPath,
+ title: IssuesDashboardApp.i18n.emptyStateWithFilterTitle,
+ });
+ });
+ });
+ });
+
+ describe('with no search query', () => {
+ let issuesQueryHandler;
+
+ beforeEach(() => {
+ issuesQueryHandler = jest.fn().mockResolvedValue(defaultQueryResponse);
+ mountComponent({ issuesQueryHandler });
+ return waitForPromises();
+ });
- it('renders empty state', async () => {
- mountComponent({ issuesQueryHandler: jest.fn().mockResolvedValue(emptyIssuesQueryResponse) });
- await waitForPromises();
+ it('does not call issues query', () => {
+ expect(issuesQueryHandler).not.toHaveBeenCalled();
+ });
- expect(findEmptyState().props()).toMatchObject({
- svgPath: defaultProvide.emptyStateSvgPath,
- title: IssuesDashboardApp.i18n.emptyStateTitle,
+ it('renders empty state', () => {
+ expect(findEmptyState().props()).toMatchObject({
+ description: null,
+ svgPath: defaultProvide.emptyStateWithoutFilterSvgPath,
+ title: IssuesDashboardApp.i18n.emptyStateWithoutFilterTitle,
+ });
+ });
});
});
@@ -233,6 +288,7 @@ describe('IssuesDashboardApp component', () => {
describe('when there is an error fetching issues', () => {
beforeEach(() => {
+ setWindowLocation(locationSearch);
mountComponent({ issuesQueryHandler: jest.fn().mockRejectedValue(new Error('ERROR')) });
jest.runOnlyPendingTimers();
return waitForPromises();
@@ -281,6 +337,9 @@ describe('IssuesDashboardApp component', () => {
expect(findIssuableList().props('searchTokens')).toMatchObject([
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers },
+ { type: TOKEN_TYPE_LABEL },
+ { type: TOKEN_TYPE_MILESTONE },
+ { type: TOKEN_TYPE_MY_REACTION },
]);
});
});
diff --git a/spec/frontend/issues/dashboard/utils_spec.js b/spec/frontend/issues/dashboard/utils_spec.js
new file mode 100644
index 00000000000..08d00eee3e3
--- /dev/null
+++ b/spec/frontend/issues/dashboard/utils_spec.js
@@ -0,0 +1,88 @@
+import AxiosMockAdapter from 'axios-mock-adapter';
+import fuzzaldrinPlus from 'fuzzaldrin-plus';
+import { AutocompleteCache } from '~/issues/dashboard/utils';
+import { MAX_LIST_SIZE } from '~/issues/list/constants';
+import axios from '~/lib/utils/axios_utils';
+
+describe('AutocompleteCache', () => {
+ let autocompleteCache;
+ let axiosMock;
+ const cacheName = 'name';
+ const searchProperty = 'property';
+ const url = 'url';
+
+ const data = [
+ { [searchProperty]: 'one' },
+ { [searchProperty]: 'two' },
+ { [searchProperty]: 'three' },
+ { [searchProperty]: 'four' },
+ { [searchProperty]: 'five' },
+ { [searchProperty]: 'six' },
+ { [searchProperty]: 'seven' },
+ { [searchProperty]: 'eight' },
+ { [searchProperty]: 'nine' },
+ { [searchProperty]: 'ten' },
+ { [searchProperty]: 'eleven' },
+ { [searchProperty]: 'twelve' },
+ { [searchProperty]: 'thirteen' },
+ { [searchProperty]: 'fourteen' },
+ { [searchProperty]: 'fifteen' },
+ ];
+
+ beforeEach(() => {
+ autocompleteCache = new AutocompleteCache();
+ axiosMock = new AxiosMockAdapter(axios);
+ });
+
+ afterEach(() => {
+ axiosMock.reset();
+ });
+
+ describe('when there is no cached data', () => {
+ let response;
+
+ beforeEach(async () => {
+ axiosMock.onGet(url).replyOnce(200, data);
+ response = await autocompleteCache.fetch({ url, cacheName, searchProperty });
+ });
+
+ it('fetches items via the API', () => {
+ expect(axiosMock.history.get[0].url).toBe(url);
+ });
+
+ it('returns a maximum of 10 items', () => {
+ expect(response).toHaveLength(MAX_LIST_SIZE);
+ });
+ });
+
+ describe('when there is cached data', () => {
+ let response;
+
+ beforeEach(async () => {
+ axiosMock.onGet(url).replyOnce(200, data);
+ jest.spyOn(fuzzaldrinPlus, 'filter');
+ // Populate cache
+ await autocompleteCache.fetch({ url, cacheName, searchProperty });
+ // Execute filtering on cache data
+ response = await autocompleteCache.fetch({ url, cacheName, searchProperty, search: 'een' });
+ });
+
+ it('returns filtered items based on search characters', () => {
+ expect(response).toEqual([
+ { [searchProperty]: 'fifteen' },
+ { [searchProperty]: 'thirteen' },
+ { [searchProperty]: 'fourteen' },
+ { [searchProperty]: 'eleven' },
+ { [searchProperty]: 'seven' },
+ ]);
+ });
+
+ it('filters using fuzzaldrinPlus', () => {
+ expect(fuzzaldrinPlus.filter).toHaveBeenCalled();
+ });
+
+ it('does not call the API', () => {
+ expect(axiosMock.history.get[1]).toBeUndefined();
+ });
+ });
+});
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 0690501dee9..70b1521ff70 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -16,6 +16,7 @@ import {
TOKEN_TYPE_RELEASE,
TOKEN_TYPE_TYPE,
TOKEN_TYPE_WEIGHT,
+ TOKEN_TYPE_HEALTH,
} from '~/vue_shared/components/filtered_search_bar/constants';
export const getIssuesQueryResponse = {
@@ -149,6 +150,8 @@ export const locationSearch = [
'label_name[]=tv',
'not[label_name][]=live action',
'not[label_name][]=drama',
+ 'or[label_name][]=comedy',
+ 'or[label_name][]=sitcom',
'release_tag=v3',
'release_tag=v4',
'not[release_tag]=v20',
@@ -170,6 +173,8 @@ export const locationSearch = [
'not[weight]=3',
'crm_contact_id=123',
'crm_organization_id=456',
+ 'health_status=atRisk',
+ 'not[health_status]=onTrack',
].join('&');
export const locationSearchWithSpecialValues = [
@@ -182,6 +187,7 @@ export const locationSearchWithSpecialValues = [
'milestone_title=Upcoming',
'epic_id=None',
'weight=None',
+ 'health_status=None',
].join('&');
export const filteredTokens = [
@@ -204,6 +210,8 @@ export const filteredTokens = [
{ type: TOKEN_TYPE_LABEL, value: { data: 'tv', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'live action', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_LABEL, value: { data: 'drama', operator: OPERATOR_NOT } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'comedy', operator: OPERATOR_OR } },
+ { type: TOKEN_TYPE_LABEL, value: { data: 'sitcom', operator: OPERATOR_OR } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v3', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v4', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_RELEASE, value: { data: 'v20', operator: OPERATOR_NOT } },
@@ -225,6 +233,8 @@ export const filteredTokens = [
{ type: TOKEN_TYPE_WEIGHT, value: { data: '3', operator: OPERATOR_NOT } },
{ type: TOKEN_TYPE_CONTACT, value: { data: '123', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_ORGANIZATION, value: { data: '456', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_HEALTH, value: { data: 'atRisk', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_HEALTH, value: { data: 'onTrack', operator: OPERATOR_NOT } },
{ type: FILTERED_SEARCH_TERM, value: { data: 'find' } },
{ type: FILTERED_SEARCH_TERM, value: { data: 'issues' } },
];
@@ -239,6 +249,7 @@ export const filteredTokensWithSpecialValues = [
{ type: TOKEN_TYPE_MILESTONE, value: { data: 'Upcoming', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_EPIC, value: { data: 'None', operator: OPERATOR_IS } },
{ type: TOKEN_TYPE_WEIGHT, value: { data: 'None', operator: OPERATOR_IS } },
+ { type: TOKEN_TYPE_HEALTH, value: { data: 'None', operator: OPERATOR_IS } },
];
export const apiParams = {
@@ -255,6 +266,7 @@ export const apiParams = {
weight: '1',
crmContactId: '123',
crmOrganizationId: '456',
+ healthStatusFilter: 'atRisk',
not: {
authorUsername: 'marge',
assigneeUsernames: ['patty', 'selma'],
@@ -266,10 +278,12 @@ export const apiParams = {
iterationId: ['20', '42'],
epicId: '34',
weight: '3',
+ healthStatusFilter: 'onTrack',
},
or: {
authorUsernames: ['burns', 'smithers'],
assigneeUsernames: ['carl', 'lenny'],
+ labelNames: ['comedy', 'sitcom'],
},
};
@@ -283,6 +297,7 @@ export const apiParamsWithSpecialValues = {
milestoneWildcardId: 'UPCOMING',
epicId: 'None',
weight: 'None',
+ healthStatusFilter: 'NONE',
};
export const urlParams = {
@@ -296,6 +311,7 @@ export const urlParams = {
'not[milestone_title]': ['season 20', 'season 30'],
'label_name[]': ['cartoon', 'tv'],
'not[label_name][]': ['live action', 'drama'],
+ 'or[label_name][]': ['comedy', 'sitcom'],
release_tag: ['v3', 'v4'],
'not[release_tag]': ['v20', 'v30'],
'type[]': ['issue', 'feature'],
@@ -311,6 +327,8 @@ export const urlParams = {
'not[weight]': '3',
crm_contact_id: '123',
crm_organization_id: '456',
+ health_status: 'atRisk',
+ 'not[health_status]': 'onTrack',
};
export const urlParamsWithSpecialValues = {
@@ -323,6 +341,7 @@ export const urlParamsWithSpecialValues = {
milestone_title: 'Upcoming',
epic_id: 'None',
weight: 'None',
+ health_status: 'None',
};
export const project1 = {
diff --git a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
index d30a8c081cc..8413b8463c1 100644
--- a/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
+++ b/spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js
@@ -1,4 +1,4 @@
-import { mount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import mockData from 'test_fixtures/issues/related_merge_requests.json';
import axios from '~/lib/utils/axios_utils';
@@ -20,7 +20,7 @@ describe('RelatedMergeRequests', () => {
mock = new MockAdapter(axios);
mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 });
- wrapper = mount(RelatedMergeRequests, {
+ wrapper = shallowMount(RelatedMergeRequests, {
store: createStore(),
propsData: {
endpoint: API_ENDPOINT,
@@ -49,7 +49,7 @@ describe('RelatedMergeRequests', () => {
});
});
- it('should return an array with single assingee', () => {
+ it('should return an array with single assignee', () => {
const mr = { assignee: assignees[0] };
expect(wrapper.vm.getAssignees(mr)).toEqual([assignees[0]]);
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index 7d6ca44e679..aaf228ae181 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -6,6 +6,7 @@ import { mockTracking } from 'helpers/tracking_helper';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
import { IssuableStatus, IssueType } from '~/issues/constants';
import DeleteIssueModal from '~/issues/show/components/delete_issue_modal.vue';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import HeaderActions from '~/issues/show/components/header_actions.vue';
import { ISSUE_STATE_EVENT_CLOSE, ISSUE_STATE_EVENT_REOPEN } from '~/issues/show/constants';
import promoteToEpicMutation from '~/issues/show/queries/promote_to_epic.mutation.graphql';
@@ -38,8 +39,9 @@ describe('HeaderActions component', () => {
issueType: IssueType.Issue,
newIssuePath: 'gitlab-org/gitlab-test/-/issues/new',
projectPath: 'gitlab-org/gitlab-test',
- reportAbusePath:
- '-/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%2Fgitlab-org%2Fgitlab-test%2F-%2Fissues%2F32&user_id=1',
+ reportAbusePath: '-/abuse_reports/add_category',
+ reportedUserId: '1',
+ reportedFromUrl: 'http://localhost:/gitlab-org/-/issues/32',
submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam',
};
@@ -401,4 +403,31 @@ describe('HeaderActions component', () => {
});
});
});
+
+ describe('abuse category selector', () => {
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+
+ beforeEach(() => {
+ wrapper = mountComponent({ props: { isIssueAuthor: false } });
+ });
+
+ it('renders', () => {
+ expect(findAbuseCategorySelector().exists()).toBe(true);
+ expect(findAbuseCategorySelector().props('showDrawer')).toEqual(false);
+ });
+
+ it('opens the drawer', async () => {
+ findDesktopDropdownItems().at(2).vm.$emit('click');
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().props('showDrawer')).toEqual(true);
+ });
+
+ it('closes the drawer', async () => {
+ await findAbuseCategorySelector().vm.$emit('close-drawer');
+
+ expect(findAbuseCategorySelector().props('showDrawer')).toEqual(false);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
index 1286617d64a..6c923cae0cc 100644
--- a/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/create_timeline_events_form_spec.js
@@ -1,6 +1,6 @@
import VueApollo from 'vue-apollo';
import Vue from 'vue';
-import { GlDatepicker } from '@gitlab/ui';
+import { GlDatepicker, GlListboxItem } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import CreateTimelineEvent from '~/issues/show/components/incidents/create_timeline_event.vue';
@@ -27,6 +27,7 @@ const mockInputData = {
incidentId: 'gid://gitlab/Issue/1',
note: 'test',
occurredAt: '2020-07-08T00:00:00.000Z',
+ timelineEventTagNames: ['Start time'],
};
describe('Create Timeline events', () => {
@@ -51,9 +52,14 @@ describe('Create Timeline events', () => {
findHourInput().setValue(inputDate.getHours());
findMinuteInput().setValue(inputDate.getMinutes());
};
+ const findListboxItems = () => wrapper.findAllComponents(GlListboxItem);
+ const setEventTags = () => {
+ findListboxItems().at(0).vm.$emit('select', true);
+ };
const fillForm = () => {
setDatetime();
setNoteInput();
+ setEventTags();
};
function createMockApolloProvider() {
@@ -80,6 +86,7 @@ describe('Create Timeline events', () => {
provide: {
fullPath: 'group/project',
issuableId: '1',
+ glFeatures: { incidentEventTags: true },
},
apolloProvider,
});
diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js
index 9accfcea791..6606bed1567 100644
--- a/spec/frontend/issues/show/components/incidents/mock_data.js
+++ b/spec/frontend/issues/show/components/incidents/mock_data.js
@@ -74,6 +74,7 @@ const mockUpdatedEvent = {
action: 'comment',
occurredAt: '2022-07-01T12:47:00Z',
createdAt: '2022-07-20T12:47:40Z',
+ timelineEventTags: [],
};
export const timelineEventsQueryListResponse = {
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
index d5b199cc790..f06d968a4c5 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js
@@ -1,11 +1,15 @@
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
-import { GlDatepicker } from '@gitlab/ui';
-import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
+import { GlDatepicker, GlListbox } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import TimelineEventsForm from '~/issues/show/components/incidents/timeline_events_form.vue';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
-import { timelineFormI18n } from '~/issues/show/components/incidents/constants';
+import {
+ timelineFormI18n,
+ TIMELINE_EVENT_TAGS,
+ timelineEventTagsI18n,
+} from '~/issues/show/components/incidents/constants';
import { createAlert } from '~/flash';
import { useFakeDate } from 'helpers/fake_date';
@@ -17,17 +21,23 @@ const fakeDate = '2020-07-08T00:00:00.000Z';
const mockInputDate = new Date('2021-08-12');
+const mockTags = TIMELINE_EVENT_TAGS;
+
describe('Timeline events form', () => {
// July 8 2020
useFakeDate(fakeDate);
let wrapper;
- const mountComponent = ({ mountMethod = shallowMountExtended } = {}, props = {}) => {
+ const mountComponent = ({ mountMethod = mountExtended } = {}, props = {}, glFeatures = {}) => {
wrapper = mountMethod(TimelineEventsForm, {
+ provide: {
+ glFeatures,
+ },
propsData: {
showSaveAndAdd: true,
isEventProcessed: false,
...props,
+ tags: mockTags,
},
stubs: {
GlButton: true,
@@ -35,6 +45,10 @@ describe('Timeline events form', () => {
});
};
+ beforeEach(() => {
+ mountComponent();
+ });
+
afterEach(() => {
createAlert.mockReset();
wrapper.destroy();
@@ -48,16 +62,26 @@ describe('Timeline events form', () => {
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
const findHourInput = () => wrapper.findByTestId('input-hours');
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
- const setDatetime = () => {
- findDatePicker().vm.$emit('input', mockInputDate);
- findHourInput().setValue(5);
- findMinuteInput().setValue(45);
- };
+ const findTagDropdown = () => wrapper.findComponent(GlListbox);
const findTextarea = () => wrapper.findByTestId('input-note');
+ const findTextareaValue = () => findTextarea().element.value;
const findCountNumeric = (count) => wrapper.findByText(count);
const findCountVerbose = (count) => wrapper.findByText(`${count} characters remaining`);
const findCountHint = () => wrapper.findByText(timelineFormI18n.hint);
+ const setDatetime = () => {
+ findDatePicker().vm.$emit('input', mockInputDate);
+ findHourInput().setValue(5);
+ findMinuteInput().setValue(45);
+ };
+ const selectTags = async (tags) => {
+ findTagDropdown().vm.$emit(
+ 'select',
+ tags.map((x) => x.value),
+ );
+ await nextTick();
+ };
+ const selectOneTag = () => selectTags([mockTags[0]]);
const submitForm = async () => {
findSubmitButton().vm.$emit('click');
await waitForPromises();
@@ -90,23 +114,97 @@ describe('Timeline events form', () => {
]);
});
- describe('form button behaviour', () => {
+ describe('with incident_event_tag feature flag enabled', () => {
beforeEach(() => {
- mountComponent({ mountMethod: mountExtended });
+ mountComponent(
+ {},
+ {},
+ {
+ incidentEventTags: true,
+ },
+ );
+ });
+
+ describe('event tag dropdown', () => {
+ it('should render option list from provided array', () => {
+ expect(findTagDropdown().props('items')).toEqual(mockTags);
+ });
+
+ it('should allow to choose multiple tags', async () => {
+ await selectTags(mockTags);
+
+ expect(findTagDropdown().props('selected')).toEqual(mockTags.map((x) => x.value));
+ });
+
+ it('should show default option, when none is chosen', () => {
+ expect(findTagDropdown().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ });
+
+ it('should show the tag, when one is selected', async () => {
+ await selectOneTag();
+
+ expect(findTagDropdown().props('toggleText')).toBe(timelineEventTagsI18n.startTime);
+ });
+
+ it('should show the number of selected tags, when more than one is selected', async () => {
+ await selectTags(mockTags);
+
+ expect(findTagDropdown().props('toggleText')).toBe('2 tags');
+ });
+
+ it('should be cleared when clear is triggered', async () => {
+ await selectTags(mockTags);
+
+ // This component expects the parent to call `clear`, so this is the only way to trigger this
+ wrapper.vm.clear();
+ await nextTick();
+
+ expect(findTagDropdown().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ expect(findTagDropdown().props('selected')).toEqual([]);
+ });
+
+ it('should populate incident note with tags if a note was empty', async () => {
+ await selectTags(mockTags);
+
+ expect(findTextareaValue()).toBe(
+ `${timelineFormI18n.areaDefaultMessage} ${mockTags
+ .map((x) => x.value.toLowerCase())
+ .join(', ')}`,
+ );
+ });
+
+ it('should populate incident note with tag but allow to customise it', async () => {
+ await selectOneTag();
+
+ await findTextarea().setValue('my customised event note');
+
+ await nextTick();
+
+ expect(findTextareaValue()).toBe('my customised event note');
+ });
+
+ it('should not populate incident note with tag if it had a note', async () => {
+ await findTextarea().setValue('hello');
+ await selectOneTag();
+
+ expect(findTextareaValue()).toBe('hello');
+ });
});
+ });
+ describe('form button behaviour', () => {
it('should save event on submit', async () => {
await submitForm();
expect(wrapper.emitted()).toEqual({
- 'save-event': [[{ note: '', occurredAt: fakeDate }, false]],
+ 'save-event': [[{ note: '', occurredAt: fakeDate, timelineEventTags: [] }, false]],
});
});
it('should save event on "submit and add another"', async () => {
await submitFormAndAddAnother();
expect(wrapper.emitted()).toEqual({
- 'save-event': [[{ note: '', occurredAt: fakeDate }, true]],
+ 'save-event': [[{ note: '', occurredAt: fakeDate, timelineEventTags: [] }, true]],
});
});
@@ -145,10 +243,6 @@ describe('Timeline events form', () => {
});
describe('form character limit', () => {
- beforeEach(() => {
- mountComponent({ mountMethod: mountExtended });
- });
-
it('sets a character limit hint', () => {
expect(findCountHint().exists()).toBe(true);
});
@@ -172,32 +266,32 @@ describe('Timeline events form', () => {
});
describe('Delete button', () => {
- it('does not show the delete button if showDelete prop is false', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: false });
+ it('does not show the delete button if isEditing prop is false', () => {
+ mountComponent({ mountMethod: mountExtended }, { isEditing: false });
expect(findDeleteButton().exists()).toBe(false);
});
- it('shows the delete button if showDelete prop is true', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: true });
+ it('shows the delete button if isEditing prop is true', () => {
+ mountComponent({ mountMethod: mountExtended }, { isEditing: true });
expect(findDeleteButton().exists()).toBe(true);
});
it('disables the delete button if isEventProcessed prop is true', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: true });
+ mountComponent({ mountMethod: mountExtended }, { isEditing: true, isEventProcessed: true });
expect(findDeleteButton().props('disabled')).toBe(true);
});
it('does not disable the delete button if isEventProcessed prop is false', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: false });
+ mountComponent({ mountMethod: mountExtended }, { isEditing: true, isEventProcessed: false });
expect(findDeleteButton().props('disabled')).toBe(false);
});
it('emits delete event on click', () => {
- mountComponent({ mountMethod: mountExtended }, { showDelete: true, isEventProcessed: true });
+ mountComponent({ mountMethod: mountExtended }, { isEditing: true, isEventProcessed: true });
deleteForm();
diff --git a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
index b0218a9df12..944854faab3 100644
--- a/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
+++ b/spec/frontend/jira_connect/branches/components/project_dropdown_spec.js
@@ -1,10 +1,4 @@
-import {
- GlAvatarLabeled,
- GlDropdown,
- GlDropdownItem,
- GlLoadingIcon,
- GlSearchBoxByType,
-} from '@gitlab/ui';
+import { GlAvatarLabeled, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -36,12 +30,8 @@ const mockQueryLoading = jest.fn().mockReturnValue(new Promise(() => {}));
describe('ProjectDropdown', () => {
let wrapper;
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findDropdownItemByProjectId = (projectId) =>
- wrapper.find(`[data-testid="test-project-${projectId}"]`);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAllGlListboxItems = () => wrapper.findAllComponents(GlListboxItem);
function createMockApolloProvider({ mockGetProjectsQuery = mockGetProjectsQuerySuccess } = {}) {
Vue.use(VueApollo);
@@ -55,6 +45,7 @@ describe('ProjectDropdown', () => {
wrapper = mountFn(ProjectDropdown, {
apolloProvider: mockApollo || createMockApolloProvider(),
propsData: props,
+ stubs: { GlCollapsibleListbox },
});
}
@@ -72,16 +63,11 @@ describe('ProjectDropdown', () => {
it('sets dropdown `loading` prop to `true`', () => {
expect(findDropdown().props('loading')).toBe(true);
});
-
- it('renders loading icon in dropdown', () => {
- expect(findLoadingIcon().isVisible()).toBe(true);
- });
});
describe('when projects query succeeds', () => {
beforeEach(async () => {
createComponent();
- await waitForPromises();
await nextTick();
});
@@ -90,12 +76,19 @@ describe('ProjectDropdown', () => {
});
it('renders dropdown items with correct props', () => {
- const dropdownItems = findAllDropdownItems();
- const avatars = dropdownItems.wrappers.map((item) => item.findComponent(GlAvatarLabeled));
+ const dropdownItems = findDropdown().props('items');
+ expect(dropdownItems).toHaveLength(mockProjects.length);
+ expect(dropdownItems).toMatchObject(mockProjects);
+ });
+
+ it('renders dropdown items with correct template', () => {
+ expect(findAllGlListboxItems()).toHaveLength(mockProjects.length);
+ const avatars = findAllGlListboxItems().wrappers.map((item) =>
+ item.findComponent(GlAvatarLabeled),
+ );
const avatarAttributes = avatars.map((avatar) => avatar.attributes());
const avatarProps = avatars.map((avatar) => avatar.props());
- expect(dropdownItems.wrappers).toHaveLength(mockProjects.length);
expect(avatarProps).toMatchObject(
mockProjects.map((project) => ({
label: project.name,
@@ -113,8 +106,7 @@ describe('ProjectDropdown', () => {
describe('when selecting a dropdown item', () => {
it('emits `change` event with the selected project', async () => {
const mockProject = mockProjects[0];
- const itemToSelect = findDropdownItemByProjectId(mockProject.id);
- await itemToSelect.vm.$emit('click');
+ await findDropdown().vm.$emit('select', mockProject.id);
expect(wrapper.emitted('change')[0]).toEqual([mockProject]);
});
@@ -124,17 +116,11 @@ describe('ProjectDropdown', () => {
const mockProject = mockProjects[0];
beforeEach(() => {
- wrapper.setProps({
- selectedProject: mockProject,
- });
- });
-
- it('sets `isChecked` prop of the corresponding dropdown item to `true`', () => {
- expect(findDropdownItemByProjectId(mockProject.id).props('isChecked')).toBe(true);
+ createComponent({ props: { selectedProject: mockProject } });
});
- it('sets dropdown text to `selectedBranchName` value', () => {
- expect(findDropdown().props('text')).toBe(mockProject.nameWithNamespace);
+ it('selects the specified item', () => {
+ expect(findDropdown().props('selected')).toBe(mockProject.id);
});
});
});
@@ -155,11 +141,10 @@ describe('ProjectDropdown', () => {
describe('when searching branches', () => {
it('triggers a refetch', async () => {
createComponent({ mountFn: mount });
- await waitForPromises();
jest.clearAllMocks();
const mockSearchTerm = 'gitl';
- await findSearchBox().vm.$emit('input', mockSearchTerm);
+ await findDropdown().vm.$emit('search', mockSearchTerm);
expect(mockGetProjectsQuerySuccess).toHaveBeenCalledWith({
after: '',
diff --git a/spec/frontend/jira_connect/subscriptions/api_spec.js b/spec/frontend/jira_connect/subscriptions/api_spec.js
index cf496d5836a..21636017f10 100644
--- a/spec/frontend/jira_connect/subscriptions/api_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/api_spec.js
@@ -9,7 +9,7 @@ import {
updateInstallation,
} from '~/jira_connect/subscriptions/api';
import { getJwt } from '~/jira_connect/subscriptions/utils';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/jira_connect/subscriptions/utils', () => ({
getJwt: jest.fn().mockResolvedValue('jwt'),
@@ -49,7 +49,7 @@ describe('JiraConnect API', () => {
jwt: mockJwt,
namespace_path: mockNamespace,
})
- .replyOnce(httpStatus.OK, mockResponse);
+ .replyOnce(HTTP_STATUS_OK, mockResponse);
response = await makeRequest();
@@ -67,7 +67,7 @@ describe('JiraConnect API', () => {
it('returns success response', async () => {
jest.spyOn(axiosInstance, 'delete');
- axiosMock.onDelete(mockRemovePath).replyOnce(httpStatus.OK, mockResponse);
+ axiosMock.onDelete(mockRemovePath).replyOnce(HTTP_STATUS_OK, mockResponse);
response = await makeRequest();
@@ -99,7 +99,7 @@ describe('JiraConnect API', () => {
page: mockPage,
per_page: mockPerPage,
})
- .replyOnce(httpStatus.OK, mockResponse);
+ .replyOnce(HTTP_STATUS_OK, mockResponse);
response = await makeRequest();
@@ -121,7 +121,7 @@ describe('JiraConnect API', () => {
jest.spyOn(axiosInstance, 'get');
- axiosMock.onGet(expectedUrl).replyOnce(httpStatus.OK, mockResponse);
+ axiosMock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, mockResponse);
response = await makeRequest();
@@ -139,7 +139,7 @@ describe('JiraConnect API', () => {
jest.spyOn(axiosInstance, 'post');
- axiosMock.onPost(expectedUrl).replyOnce(httpStatus.OK, mockResponse);
+ axiosMock.onPost(expectedUrl).replyOnce(HTTP_STATUS_OK, mockResponse);
response = await makeRequest();
@@ -175,7 +175,7 @@ describe('JiraConnect API', () => {
instance_url: expectedInstanceUrl,
},
})
- .replyOnce(httpStatus.OK, mockResponse);
+ .replyOnce(HTTP_STATUS_OK, mockResponse);
response = await makeRequest();
diff --git a/spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js b/spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js
deleted file mode 100644
index 5f38a0acb9d..00000000000
--- a/spec/frontend/jira_connect/subscriptions/components/compatibility_alert_spec.js
+++ /dev/null
@@ -1,56 +0,0 @@
-import { GlAlert, GlLink } from '@gitlab/ui';
-import { shallowMount, mount } from '@vue/test-utils';
-import CompatibilityAlert from '~/jira_connect/subscriptions/components/compatibility_alert.vue';
-
-import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
-
-describe('CompatibilityAlert', () => {
- let wrapper;
-
- const createComponent = ({ mountFn = shallowMount } = {}) => {
- wrapper = mountFn(CompatibilityAlert);
- };
-
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findLink = () => wrapper.findComponent(GlLink);
- const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('displays an alert', () => {
- createComponent();
-
- expect(findAlert().exists()).toBe(true);
- });
-
- it('renders help link with target="_blank" and rel="noopener noreferrer"', () => {
- createComponent({ mountFn: mount });
- expect(findLink().attributes()).toMatchObject({
- target: '_blank',
- rel: 'noopener',
- });
- });
-
- it('`local-storage-sync` value prop is initially false', () => {
- createComponent();
-
- expect(findLocalStorageSync().props('value')).toBe(false);
- });
-
- describe('when dismissed', () => {
- beforeEach(async () => {
- createComponent();
- await findAlert().vm.$emit('dismiss');
- });
-
- it('hides alert', () => {
- expect(findAlert().exists()).toBe(false);
- });
-
- it('updates value prop of `local-storage-sync`', () => {
- expect(findLocalStorageSync().props('value')).toBe(true);
- });
- });
-});
diff --git a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
index 01317eb5dba..e20c4b62e77 100644
--- a/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/sign_in_oauth_button_spec.js
@@ -4,6 +4,7 @@ import { nextTick } from 'vue';
import SignInOauthButton from '~/jira_connect/subscriptions/components/sign_in_oauth_button.vue';
import {
+ GITLAB_COM_BASE_PATH,
I18N_DEFAULT_SIGN_IN_BUTTON_TEXT,
OAUTH_WINDOW_OPTIONS,
} from '~/jira_connect/subscriptions/constants';
@@ -36,6 +37,9 @@ describe('SignInOauthButton', () => {
},
state: 'good-state',
};
+ const defaultProps = {
+ gitlabBasePath: GITLAB_COM_BASE_PATH,
+ };
const createComponent = ({ slots, props } = {}) => {
store = createStore();
@@ -48,7 +52,7 @@ describe('SignInOauthButton', () => {
provide: {
oauthMetadata: mockOauthMetadata,
},
- propsData: props,
+ propsData: { ...defaultProps, ...props },
});
};
@@ -57,16 +61,17 @@ describe('SignInOauthButton', () => {
});
const findButton = () => wrapper.findComponent(GlButton);
+ describe('when `gitlabBasePath` is GitLab.com', () => {
+ it('displays a button', () => {
+ createComponent();
- it('displays a button', () => {
- createComponent();
-
- expect(findButton().exists()).toBe(true);
- expect(findButton().text()).toBe(I18N_DEFAULT_SIGN_IN_BUTTON_TEXT);
- expect(findButton().props('category')).toBe('primary');
+ expect(findButton().exists()).toBe(true);
+ expect(findButton().text()).toBe(I18N_DEFAULT_SIGN_IN_BUTTON_TEXT);
+ expect(findButton().props('category')).toBe('primary');
+ });
});
- describe('when `gitlabBasePath` is passed', () => {
+ describe('when `gitlabBasePath` is self-managed', () => {
const mockBasePath = 'https://gitlab.mycompany.com';
it('uses custom text for button', () => {
diff --git a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
index 748e151f31b..40e627262db 100644
--- a/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
+++ b/spec/frontend/jira_import/components/__snapshots__/jira_import_form_spec.js.snap
@@ -150,18 +150,12 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<input
aria-label="Search"
- class="gl-form-input gl-search-box-by-type-input form-control"
+ class="gl-form-input form-control gl-search-box-by-type-input"
placeholder="Search"
type="search"
/>
- <div
- class="gl-search-box-by-type-right-icons"
- >
- <!---->
-
- <!---->
- </div>
+ <!---->
</div>
<li
@@ -281,18 +275,12 @@ exports[`JiraImportForm table body shows correct information in each cell 1`] =
<input
aria-label="Search"
- class="gl-form-input gl-search-box-by-type-input form-control"
+ class="gl-form-input form-control gl-search-box-by-type-input"
placeholder="Search"
type="search"
/>
- <div
- class="gl-search-box-by-type-right-icons"
- >
- <!---->
-
- <!---->
- </div>
+ <!---->
</div>
<li
diff --git a/spec/frontend/jobs/components/job/manual_variables_form_spec.js b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
index 45a1e9dca76..3040570df19 100644
--- a/spec/frontend/jobs/components/job/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
@@ -212,9 +212,30 @@ describe('Manual Variables Form', () => {
expect(findDeleteVarBtn().exists()).toBe(true);
});
+ });
+
+ describe('variable delete button placeholder', () => {
+ beforeEach(async () => {
+ getJobQueryResponse.mockResolvedValue(mockJobResponse);
+ await createComponentWithApollo();
+ });
it('delete variable button placeholder should only exist when a user cannot remove', async () => {
expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
});
+
+ it('does not show the placeholder button', () => {
+ expect(findDeleteVarBtnPlaceholder().classes('gl-opacity-0')).toBe(true);
+ });
+
+ it('placeholder button will not delete the row on click', async () => {
+ expect(findAllCiVariableKeys()).toHaveLength(1);
+ expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
+
+ await findDeleteVarBtnPlaceholder().trigger('click');
+
+ expect(findAllCiVariableKeys()).toHaveLength(1);
+ expect(findDeleteVarBtnPlaceholder().exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/jobs/components/job/sidebar_spec.js b/spec/frontend/jobs/components/job/sidebar_spec.js
index 27911eb76eb..aa9ca932023 100644
--- a/spec/frontend/jobs/components/job/sidebar_spec.js
+++ b/spec/frontend/jobs/components/job/sidebar_spec.js
@@ -3,7 +3,7 @@ import { nextTick } from 'vue';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ArtifactsBlock from '~/jobs/components/job/sidebar/artifacts_block.vue';
import JobRetryForwardDeploymentModal from '~/jobs/components/job/sidebar/job_retry_forward_deployment_modal.vue';
import JobsContainer from '~/jobs/components/job/sidebar/jobs_container.vue';
@@ -43,7 +43,7 @@ describe('Sidebar details block', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet().reply(httpStatus.OK, {
+ mock.onGet().reply(HTTP_STATUS_OK, {
name: job.stage,
});
});
diff --git a/spec/frontend/jobs/components/table/jobs_table_spec.js b/spec/frontend/jobs/components/table/jobs_table_spec.js
index 803df3df37f..3c4f2d624fe 100644
--- a/spec/frontend/jobs/components/table/jobs_table_spec.js
+++ b/spec/frontend/jobs/components/table/jobs_table_spec.js
@@ -2,14 +2,14 @@ import { GlTable } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import JobsTable from '~/jobs/components/table/jobs_table.vue';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import { mockJobsNodes } from '../../mock_data';
describe('Jobs Table', () => {
let wrapper;
const findTable = () => wrapper.findComponent(GlTable);
- const findStatusBadge = () => wrapper.findComponent(CiBadge);
+ const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
const findTableRows = () => wrapper.findAllByTestId('jobs-table-row');
const findJobStage = () => wrapper.findByTestId('job-stage-name');
const findJobName = () => wrapper.findByTestId('job-name');
@@ -43,7 +43,7 @@ describe('Jobs Table', () => {
});
it('displays job status', () => {
- expect(findStatusBadge().exists()).toBe(true);
+ expect(findCiBadgeLink().exists()).toBe(true);
});
it('displays the job stage and name', () => {
diff --git a/spec/frontend/language_switcher/components/app_spec.js b/spec/frontend/language_switcher/components/app_spec.js
index 6a1b94cd813..effb71c2775 100644
--- a/spec/frontend/language_switcher/components/app_spec.js
+++ b/spec/frontend/language_switcher/components/app_spec.js
@@ -1,3 +1,4 @@
+import { GlLink } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import LanguageSwitcherApp from '~/language_switcher/components/app.vue';
import { PREFERRED_LANGUAGE_COOKIE_KEY } from '~/language_switcher/constants';
@@ -29,6 +30,7 @@ describe('<LanguageSwitcher />', () => {
const getPreferredLanguage = () => wrapper.find('.gl-dropdown-button-text').text();
const findLanguageDropdownItem = (code) => wrapper.findByTestId(`language_switcher_lang_${code}`);
+ const findFooter = () => wrapper.findByTestId('footer');
it('preferred language', () => {
expect(getPreferredLanguage()).toBe(EN.text);
@@ -59,4 +61,12 @@ describe('<LanguageSwitcher />', () => {
expect(utils.setCookie).toHaveBeenCalledWith(PREFERRED_LANGUAGE_COOKIE_KEY, ES.value);
window.location = originalLocation;
});
+
+ it('renders footer link', () => {
+ const link = findFooter().findComponent(GlLink);
+
+ // Assert against actual value so we can implicitly test `helpPagePath` call
+ expect(link.attributes('href')).toBe('/help/development/i18n/translation.md');
+ expect(link.text()).toBe(LanguageSwitcherApp.HELP_TRANSLATE_MSG);
+ });
});
diff --git a/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
index 055d57d6ada..8d6ace165ab 100644
--- a/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/date_calculation_utility_spec.js
@@ -3,7 +3,9 @@ import {
newDateAsLocaleTime,
nSecondsAfter,
nSecondsBefore,
+ isToday,
} from '~/lib/utils/datetime/date_calculation_utility';
+import { useFakeDate } from 'helpers/fake_date';
describe('newDateAsLocaleTime', () => {
it.each`
@@ -66,3 +68,19 @@ describe('nSecondsBefore', () => {
expect(nSecondsBefore(date, seconds)).toEqual(expected);
});
});
+
+describe('isToday', () => {
+ useFakeDate(2022, 11, 5);
+
+ describe('when date is today', () => {
+ it('returns `true`', () => {
+ expect(isToday(new Date(2022, 11, 5))).toBe(true);
+ });
+ });
+
+ describe('when date is not today', () => {
+ it('returns `false`', () => {
+ expect(isToday(new Date(2022, 11, 6))).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
index 2e0bb6a8dcd..a83b0ed9fbe 100644
--- a/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
+++ b/spec/frontend/lib/utils/datetime/date_format_utility_spec.js
@@ -149,17 +149,17 @@ describe('durationTimeFormatted', () => {
describe('formatUtcOffset', () => {
it.each`
offset | expected
- ${-32400} | ${'- 9'}
- ${'-12600'} | ${'- 3.5'}
- ${0} | ${'0'}
- ${'10800'} | ${'+ 3'}
- ${19800} | ${'+ 5.5'}
- ${0} | ${'0'}
- ${[]} | ${'0'}
- ${{}} | ${'0'}
- ${true} | ${'0'}
- ${null} | ${'0'}
- ${undefined} | ${'0'}
+ ${-32400} | ${'-9'}
+ ${'-12600'} | ${'-3.5'}
+ ${0} | ${' 0'}
+ ${'10800'} | ${'+3'}
+ ${19800} | ${'+5.5'}
+ ${0} | ${' 0'}
+ ${[]} | ${' 0'}
+ ${{}} | ${' 0'}
+ ${true} | ${' 0'}
+ ${null} | ${' 0'}
+ ${undefined} | ${' 0'}
`('returns $expected given $offset', ({ offset, expected }) => {
expect(utils.formatUtcOffset(offset)).toEqual(expected);
});
diff --git a/spec/frontend/lib/utils/poll_until_complete_spec.js b/spec/frontend/lib/utils/poll_until_complete_spec.js
index 3ce17ecfc8c..309e0cc540b 100644
--- a/spec/frontend/lib/utils/poll_until_complete_spec.js
+++ b/spec/frontend/lib/utils/poll_until_complete_spec.js
@@ -1,7 +1,11 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import pollUntilComplete from '~/lib/utils/poll_until_complete';
const endpoint = `${TEST_HOST}/foo`;
@@ -24,7 +28,7 @@ describe('pollUntilComplete', () => {
describe('given an immediate success response', () => {
beforeEach(() => {
- mock.onGet(endpoint).replyOnce(httpStatusCodes.OK, mockData);
+ mock.onGet(endpoint).replyOnce(HTTP_STATUS_OK, mockData);
});
it('resolves with the response', () =>
@@ -39,7 +43,7 @@ describe('pollUntilComplete', () => {
.onGet(endpoint)
.replyOnce(HTTP_STATUS_NO_CONTENT, undefined, pollIntervalHeader)
.onGet(endpoint)
- .replyOnce(httpStatusCodes.OK, mockData);
+ .replyOnce(HTTP_STATUS_OK, mockData);
});
it('calls the endpoint until it succeeds, and resolves with the response', () =>
@@ -66,7 +70,7 @@ describe('pollUntilComplete', () => {
const errorMessage = 'error message';
beforeEach(() => {
- mock.onGet(endpoint).replyOnce(httpStatusCodes.NOT_FOUND, errorMessage);
+ mock.onGet(endpoint).replyOnce(HTTP_STATUS_NOT_FOUND, errorMessage);
});
it('rejects with the error response', () =>
@@ -78,7 +82,7 @@ describe('pollUntilComplete', () => {
describe('given params', () => {
const params = { foo: 'bar' };
beforeEach(() => {
- mock.onGet(endpoint, { params }).replyOnce(httpStatusCodes.OK, mockData);
+ mock.onGet(endpoint, { params }).replyOnce(HTTP_STATUS_OK, mockData);
});
it('requests the expected URL', () =>
diff --git a/spec/frontend/locale/ensure_single_line_spec.js b/spec/frontend/locale/ensure_single_line_spec.js
index 20b04cab9c8..ca3d57015af 100644
--- a/spec/frontend/locale/ensure_single_line_spec.js
+++ b/spec/frontend/locale/ensure_single_line_spec.js
@@ -1,4 +1,4 @@
-import ensureSingleLine from '~/locale/ensure_single_line';
+import ensureSingleLine from '~/locale/ensure_single_line.cjs';
describe('locale', () => {
describe('ensureSingleLine', () => {
diff --git a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
index df5c884f42e..b94964dc482 100644
--- a/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/access_request_action_buttons_spec.js
@@ -38,7 +38,6 @@ describe('AccessRequestActionButtons', () => {
title: 'Deny access',
isAccessRequest: true,
isInvite: false,
- icon: 'close',
});
});
diff --git a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
index ea819b4fb83..68009708c99 100644
--- a/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
+++ b/spec/frontend/members/components/action_buttons/invite_action_buttons_spec.js
@@ -39,12 +39,10 @@ describe('InviteActionButtons', () => {
it('sets props correctly', () => {
expect(findRemoveMemberButton().props()).toMatchObject({
memberId: member.id,
- memberType: null,
message: `Are you sure you want to revoke the invitation for ${member.invite.email} to join "${member.source.fullName}"`,
title: 'Revoke invite',
isAccessRequest: false,
isInvite: true,
- icon: 'remove',
});
});
});
diff --git a/spec/frontend/members/components/action_buttons/leave_button_spec.js b/spec/frontend/members/components/action_buttons/leave_button_spec.js
deleted file mode 100644
index ecfbf4460a6..00000000000
--- a/spec/frontend/members/components/action_buttons/leave_button_spec.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import { GlButton } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
-import LeaveButton from '~/members/components/action_buttons/leave_button.vue';
-import LeaveModal from '~/members/components/modals/leave_modal.vue';
-import { LEAVE_MODAL_ID } from '~/members/constants';
-import { member } from '../../mock_data';
-
-describe('LeaveButton', () => {
- let wrapper;
-
- const createComponent = (propsData = {}) => {
- wrapper = shallowMount(LeaveButton, {
- propsData: {
- member,
- ...propsData,
- },
- directives: {
- GlTooltip: createMockDirective(),
- GlModal: createMockDirective(),
- },
- });
- };
-
- const findButton = () => wrapper.findComponent(GlButton);
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('displays a tooltip', () => {
- const button = findButton();
-
- expect(getBinding(button.element, 'gl-tooltip')).not.toBeUndefined();
- expect(button.attributes('title')).toBe('Leave');
- });
-
- it('sets `aria-label` attribute', () => {
- expect(findButton().attributes('aria-label')).toBe('Leave');
- });
-
- it('renders leave modal', () => {
- const leaveModal = wrapper.findComponent(LeaveModal);
-
- expect(leaveModal.exists()).toBe(true);
- expect(leaveModal.props('member')).toEqual(member);
- });
-
- it('triggers leave modal', () => {
- const binding = getBinding(findButton().element, 'gl-modal');
-
- expect(binding).not.toBeUndefined();
- expect(binding.value).toBe(LEAVE_MODAL_ID);
- });
-});
diff --git a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
index 0e5b667eb9b..cca340169b7 100644
--- a/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
+++ b/spec/frontend/members/components/action_buttons/remove_member_button_spec.js
@@ -39,7 +39,6 @@ describe('RemoveMemberButton', () => {
},
propsData: {
memberId: 1,
- memberType: 'GroupMember',
message: 'Are you sure you want to remove John Smith?',
title: 'Remove member',
isAccessRequest: true,
@@ -77,20 +76,9 @@ describe('RemoveMemberButton', () => {
it('calls Vuex action to show `remove member` modal when clicked', () => {
findButton().vm.$emit('click');
- expect(actions.showRemoveMemberModal).toHaveBeenCalledWith(expect.any(Object), modalData);
- });
-
- describe('button optional properties', () => {
- it('has default value for category and text', () => {
- createComponent();
- expect(findButton().props('category')).toBe('secondary');
- expect(findButton().text()).toBe('');
- });
-
- it('allow changing value of button category and text', () => {
- createComponent({ buttonCategory: 'primary', buttonText: 'Decline request' });
- expect(findButton().props('category')).toBe('primary');
- expect(findButton().text()).toBe('Decline request');
+ expect(actions.showRemoveMemberModal).toHaveBeenCalledWith(expect.any(Object), {
+ ...modalData,
+ memberModelType: undefined,
});
});
});
diff --git a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js b/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
deleted file mode 100644
index 6ac46619bc9..00000000000
--- a/spec/frontend/members/components/action_buttons/user_action_buttons_spec.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import LeaveButton from '~/members/components/action_buttons/leave_button.vue';
-import RemoveMemberButton from '~/members/components/action_buttons/remove_member_button.vue';
-import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
-import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
-import { member, orphanedMember } from '../../mock_data';
-
-describe('UserActionButtons', () => {
- let wrapper;
-
- const createComponent = (propsData = {}) => {
- wrapper = shallowMount(UserActionButtons, {
- propsData: {
- member,
- isCurrentUser: false,
- isInvitedUser: false,
- ...propsData,
- },
- });
- };
-
- const findRemoveMemberButton = () => wrapper.findComponent(RemoveMemberButton);
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('when user has `canRemove` permissions', () => {
- beforeEach(() => {
- createComponent({
- permissions: {
- canRemove: true,
- },
- });
- });
-
- it('renders remove member button', () => {
- expect(findRemoveMemberButton().exists()).toBe(true);
- });
-
- it('sets props correctly', () => {
- expect(findRemoveMemberButton().props()).toEqual({
- memberId: member.id,
- memberType: 'GroupMember',
- message: `Are you sure you want to remove ${member.user.name} from "${member.source.fullName}"?`,
- title: null,
- isAccessRequest: false,
- isInvite: false,
- icon: '',
- buttonCategory: 'secondary',
- buttonText: 'Remove member',
- userDeletionObstacles: {
- name: member.user.name,
- obstacles: parseUserDeletionObstacles(member.user),
- },
- });
- });
-
- describe('when member is orphaned', () => {
- it('sets `message` prop correctly', () => {
- createComponent({
- member: orphanedMember,
- permissions: {
- canRemove: true,
- },
- });
-
- expect(findRemoveMemberButton().props('message')).toBe(
- `Are you sure you want to remove this orphaned member from "${orphanedMember.source.fullName}"?`,
- );
- });
- });
-
- describe('when member is the current user', () => {
- it('renders leave button', () => {
- createComponent({
- isCurrentUser: true,
- permissions: {
- canRemove: true,
- },
- });
-
- expect(wrapper.findComponent(LeaveButton).exists()).toBe(true);
- });
- });
- });
-
- describe('when user does not have `canRemove` permissions', () => {
- it('does not render remove member button', () => {
- createComponent({
- permissions: {
- canRemove: false,
- },
- });
-
- expect(findRemoveMemberButton().exists()).toBe(false);
- });
- });
-
- describe('when group member', () => {
- beforeEach(() => {
- createComponent({
- member: {
- ...member,
- type: 'GroupMember',
- },
- permissions: {
- canRemove: true,
- },
- });
- });
-
- it('sets member type correctly', () => {
- expect(findRemoveMemberButton().props().memberType).toBe('GroupMember');
- });
- });
-
- describe('when project member', () => {
- beforeEach(() => {
- createComponent({
- member: {
- ...member,
- type: 'ProjectMember',
- },
- permissions: {
- canRemove: true,
- },
- });
- });
-
- it('sets member type correctly', () => {
- expect(findRemoveMemberButton().props().memberType).toBe('ProjectMember');
- });
- });
-
- describe('isInvitedUser', () => {
- it.each`
- isInvitedUser | icon | buttonText | buttonCategory
- ${true} | ${'remove'} | ${null} | ${'primary'}
- ${false} | ${''} | ${'Remove member'} | ${'secondary'}
- `(
- 'passes the correct props to remove-member-button when isInvitedUser is $isInvitedUser',
- ({ isInvitedUser, icon, buttonText, buttonCategory }) => {
- createComponent({
- isInvitedUser,
- permissions: {
- canRemove: true,
- },
- });
-
- expect(findRemoveMemberButton().props()).toEqual(
- expect.objectContaining({
- icon,
- buttonText,
- buttonCategory,
- }),
- );
- },
- );
- });
-});
diff --git a/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js b/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js
new file mode 100644
index 00000000000..90f5b217007
--- /dev/null
+++ b/spec/frontend/members/components/action_dropdowns/leave_group_dropdown_item_spec.js
@@ -0,0 +1,54 @@
+import { GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import LeaveGroupDropdownItem from '~/members/components/action_dropdowns/leave_group_dropdown_item.vue';
+import LeaveModal from '~/members/components/modals/leave_modal.vue';
+import { LEAVE_MODAL_ID } from '~/members/constants';
+import { member, permissions } from '../../mock_data';
+
+describe('LeaveGroupDropdownItem', () => {
+ let wrapper;
+ const text = 'dummy';
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(LeaveGroupDropdownItem, {
+ propsData: {
+ member,
+ permissions,
+ ...propsData,
+ },
+ directives: {
+ GlModal: createMockDirective(),
+ },
+ slots: {
+ default: text,
+ },
+ });
+ };
+
+ const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a slot with red text', () => {
+ expect(findDropdownItem().html()).toContain(`<span class="gl-text-red-500">${text}</span>`);
+ });
+
+ it('contains LeaveModal component', () => {
+ const leaveModal = wrapper.findComponent(LeaveModal);
+
+ expect(leaveModal.props()).toEqual({ member, permissions });
+ });
+
+ it('binds to the LeaveModal component', () => {
+ const binding = getBinding(findDropdownItem().element, 'gl-modal');
+
+ expect(binding.value).toBe(LEAVE_MODAL_ID);
+ });
+});
diff --git a/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js b/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js
new file mode 100644
index 00000000000..e1c498249d7
--- /dev/null
+++ b/spec/frontend/members/components/action_dropdowns/remove_member_dropdown_item_spec.js
@@ -0,0 +1,77 @@
+import { GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { modalData } from 'jest/members/mock_data';
+import RemoveMemberDropdownItem from '~/members/components/action_dropdowns/remove_member_dropdown_item.vue';
+import { MEMBER_TYPES, MEMBER_MODEL_TYPE_GROUP_MEMBER } from '~/members/constants';
+
+Vue.use(Vuex);
+
+describe('RemoveMemberDropdownItem', () => {
+ let wrapper;
+ const text = 'dummy';
+
+ const actions = {
+ showRemoveMemberModal: jest.fn(),
+ };
+
+ const createStore = (state = {}) => {
+ return new Vuex.Store({
+ modules: {
+ [MEMBER_TYPES.user]: {
+ namespaced: true,
+ state: {
+ memberPath: '/groups/foo-bar/-/group_members/:id',
+ ...state,
+ },
+ actions,
+ },
+ },
+ });
+ };
+
+ const createComponent = (propsData = {}, state) => {
+ wrapper = shallowMount(RemoveMemberDropdownItem, {
+ store: createStore(state),
+ provide: {
+ namespace: MEMBER_TYPES.user,
+ },
+ propsData: {
+ memberId: 1,
+ memberModelType: MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ modalMessage: 'Are you sure you want to remove John Smith?',
+ isAccessRequest: true,
+ isInvite: true,
+ userDeletionObstacles: { name: 'user', obstacles: [] },
+ ...propsData,
+ },
+ slots: {
+ default: text,
+ },
+ });
+ };
+
+ const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders a slot with red text', () => {
+ expect(findDropdownItem().html()).toContain(`<span class="gl-text-red-500">${text}</span>`);
+ });
+
+ it('calls Vuex action to show `remove member` modal when clicked', () => {
+ findDropdownItem().vm.$emit('click');
+
+ expect(actions.showRemoveMemberModal).toHaveBeenCalledWith(expect.any(Object), {
+ ...modalData,
+ preventRemoval: false,
+ });
+ });
+});
diff --git a/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js b/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js
new file mode 100644
index 00000000000..5a2de1cac80
--- /dev/null
+++ b/spec/frontend/members/components/action_dropdowns/user_action_dropdown_spec.js
@@ -0,0 +1,220 @@
+import { shallowMount } from '@vue/test-utils';
+import { sprintf } from '~/locale';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import LeaveGroupDropdownItem from '~/members/components/action_dropdowns/leave_group_dropdown_item.vue';
+import RemoveMemberDropdownItem from '~/members/components/action_dropdowns/remove_member_dropdown_item.vue';
+import UserActionDropdown from '~/members/components/action_dropdowns/user_action_dropdown.vue';
+import { I18N } from '~/members/components/action_dropdowns/constants';
+import {
+ MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ MEMBER_MODEL_TYPE_PROJECT_MEMBER,
+} from '~/members/constants';
+import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
+import { member, orphanedMember } from '../../mock_data';
+
+describe('UserActionDropdown', () => {
+ let wrapper;
+
+ const createComponent = (propsData = {}) => {
+ wrapper = shallowMount(UserActionDropdown, {
+ propsData: {
+ member,
+ isCurrentUser: false,
+ isInvitedUser: false,
+ ...propsData,
+ },
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
+ });
+ };
+
+ const findRemoveMemberDropdownItem = () => wrapper.findComponent(RemoveMemberDropdownItem);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when user has `canRemove` permissions', () => {
+ beforeEach(() => {
+ createComponent({
+ permissions: {
+ canRemove: true,
+ },
+ });
+ });
+
+ it('renders remove member dropdown with correct text', () => {
+ const removeMemberDropdownItem = findRemoveMemberDropdownItem();
+ expect(removeMemberDropdownItem.exists()).toBe(true);
+ expect(removeMemberDropdownItem.html()).toContain(I18N.removeMember);
+ });
+
+ it('displays a tooltip', () => {
+ const tooltip = getBinding(wrapper.element, 'gl-tooltip');
+ expect(tooltip).not.toBeUndefined();
+ expect(tooltip.value).toBe(I18N.actions);
+ });
+
+ it('sets props correctly', () => {
+ expect(findRemoveMemberDropdownItem().props()).toEqual({
+ memberId: member.id,
+ memberModelType: MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ modalMessage: sprintf(
+ I18N.confirmNormalUserRemoval,
+ {
+ userName: member.user.name,
+ group: member.source.fullName,
+ },
+ false,
+ ),
+ isAccessRequest: false,
+ isInvite: false,
+ userDeletionObstacles: {
+ name: member.user.name,
+ obstacles: parseUserDeletionObstacles(member.user),
+ },
+ preventRemoval: false,
+ });
+ });
+
+ describe('when member is orphaned', () => {
+ it('sets `message` prop correctly', () => {
+ createComponent({
+ member: orphanedMember,
+ permissions: {
+ canRemove: true,
+ },
+ });
+
+ expect(findRemoveMemberDropdownItem().props('modalMessage')).toBe(
+ sprintf(I18N.confirmOrphanedUserRemoval, { group: orphanedMember.source.fullName }),
+ );
+ });
+ });
+
+ describe('when member is the current user', () => {
+ it('renders leave dropdown with correct text', () => {
+ createComponent({
+ isCurrentUser: true,
+ permissions: {
+ canRemove: true,
+ },
+ });
+
+ const leaveGroupDropdownItem = wrapper.findComponent(LeaveGroupDropdownItem);
+ expect(leaveGroupDropdownItem.exists()).toBe(true);
+ expect(leaveGroupDropdownItem.html()).toContain(I18N.leaveGroup);
+ });
+ });
+ });
+
+ describe('when user does not have `canRemove` permissions', () => {
+ it('does not render remove member dropdown', () => {
+ createComponent({
+ permissions: {
+ canRemove: false,
+ },
+ });
+
+ expect(findRemoveMemberDropdownItem().exists()).toBe(false);
+ });
+ });
+
+ describe('when user can remove but it is blocked by last owner', () => {
+ const permissions = {
+ canRemove: false,
+ canRemoveBlockedByLastOwner: true,
+ };
+
+ it('renders remove member dropdown', () => {
+ createComponent({
+ permissions,
+ });
+
+ expect(findRemoveMemberDropdownItem().exists()).toBe(true);
+ });
+
+ describe('when member model type is `GroupMember`', () => {
+ it('passes correct message to the modal', () => {
+ createComponent({
+ permissions,
+ });
+
+ expect(findRemoveMemberDropdownItem().props('modalMessage')).toBe(
+ I18N.lastGroupOwnerCannotBeRemoved,
+ );
+ });
+ });
+
+ describe('when member model type is `ProjectMember`', () => {
+ it('passes correct message to the modal', () => {
+ createComponent({
+ member: {
+ ...member,
+ type: MEMBER_MODEL_TYPE_PROJECT_MEMBER,
+ },
+ permissions,
+ });
+
+ expect(findRemoveMemberDropdownItem().props('modalMessage')).toBe(
+ I18N.personalProjectOwnerCannotBeRemoved,
+ );
+ });
+ });
+
+ describe('when member is the current user', () => {
+ it('renders leave dropdown with correct props', () => {
+ createComponent({
+ isCurrentUser: true,
+ permissions,
+ });
+
+ expect(wrapper.findComponent(LeaveGroupDropdownItem).props()).toEqual({
+ member,
+ permissions,
+ });
+ });
+ });
+ });
+
+ describe('when group member', () => {
+ beforeEach(() => {
+ createComponent({
+ member: {
+ ...member,
+ type: MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ },
+ permissions: {
+ canRemove: true,
+ },
+ });
+ });
+
+ it('sets member type correctly', () => {
+ expect(findRemoveMemberDropdownItem().props().memberModelType).toBe(
+ MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ );
+ });
+ });
+
+ describe('when project member', () => {
+ beforeEach(() => {
+ createComponent({
+ member: {
+ ...member,
+ type: MEMBER_MODEL_TYPE_PROJECT_MEMBER,
+ },
+ permissions: {
+ canRemove: true,
+ },
+ });
+ });
+
+ it('sets member type correctly', () => {
+ expect(findRemoveMemberDropdownItem().props().memberModelType).toBe(
+ MEMBER_MODEL_TYPE_PROJECT_MEMBER,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/members/components/modals/leave_modal_spec.js b/spec/frontend/members/components/modals/leave_modal_spec.js
index cdbabb2f646..ba587c6f0b3 100644
--- a/spec/frontend/members/components/modals/leave_modal_spec.js
+++ b/spec/frontend/members/components/modals/leave_modal_spec.js
@@ -1,11 +1,14 @@
import { GlModal, GlForm } from '@gitlab/ui';
-import { within } from '@testing-library/dom';
-import { mount, createWrapper } from '@vue/test-utils';
import { cloneDeep } from 'lodash';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import LeaveModal from '~/members/components/modals/leave_modal.vue';
-import { LEAVE_MODAL_ID, MEMBER_TYPES } from '~/members/constants';
+import {
+ LEAVE_MODAL_ID,
+ MEMBER_TYPES,
+ MEMBER_MODEL_TYPE_PROJECT_MEMBER,
+} from '~/members/constants';
import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
import { parseUserDeletionObstacles } from '~/vue_shared/components/user_deletion_obstacles/utils';
import { member } from '../../mock_data';
@@ -31,14 +34,17 @@ describe('LeaveModal', () => {
});
};
- const createComponent = (propsData = {}, state) => {
- wrapper = mount(LeaveModal, {
+ const createComponent = async (propsData = {}, state) => {
+ wrapper = mountExtended(LeaveModal, {
store: createStore(state),
provide: {
namespace: MEMBER_TYPES.user,
},
propsData: {
member,
+ permissions: {
+ canRemove: true,
+ },
...propsData,
},
attrs: {
@@ -46,39 +52,98 @@ describe('LeaveModal', () => {
visible: true,
},
});
+
+ await nextTick();
};
- const findModal = () => wrapper.findComponent(GlModal);
+ const findModal = () => extendedWrapper(wrapper.findComponent(GlModal));
const findForm = () => findModal().findComponent(GlForm);
const findUserDeletionObstaclesList = () => findModal().findComponent(UserDeletionObstaclesList);
- const getByText = (text, options) =>
- createWrapper(within(findModal().element).getByText(text, options));
-
- beforeEach(async () => {
- createComponent();
- await nextTick();
- });
-
afterEach(() => {
wrapper.destroy();
});
- it('sets modal ID', () => {
+ it('sets modal ID', async () => {
+ await createComponent();
+
expect(findModal().props('modalId')).toBe(LEAVE_MODAL_ID);
});
- it('displays modal title', () => {
- expect(getByText(`Leave "${member.source.fullName}"`).exists()).toBe(true);
+ describe('when leave is allowed', () => {
+ it('displays modal title', async () => {
+ await createComponent();
+
+ expect(findModal().findByText(`Leave "${member.source.fullName}"`).exists()).toBe(true);
+ });
+
+ it('displays modal body', async () => {
+ await createComponent();
+
+ expect(
+ findModal()
+ .findByText(`Are you sure you want to leave "${member.source.fullName}"?`)
+ .exists(),
+ ).toBe(true);
+ });
});
- it('displays modal body', () => {
- expect(getByText(`Are you sure you want to leave "${member.source.fullName}"?`).exists()).toBe(
- true,
- );
+ describe('when leave is blocked by last owner', () => {
+ const permissions = {
+ canRemove: false,
+ canRemoveBlockedByLastOwner: true,
+ };
+
+ it('does not show primary action button', async () => {
+ await createComponent({
+ permissions,
+ });
+
+ expect(findModal().props('actionPrimary')).toBe(null);
+ });
+
+ it('displays modal title', async () => {
+ await createComponent({
+ permissions,
+ });
+
+ expect(findModal().findByText(`Cannot leave "${member.source.fullName}"`).exists()).toBe(
+ true,
+ );
+ });
+
+ describe('when member model type is `GroupMember`', () => {
+ it('displays modal body', async () => {
+ await createComponent({
+ permissions,
+ });
+
+ expect(
+ findModal().findByText(LeaveModal.i18n.preventedBodyGroupMemberModelType).exists(),
+ ).toBe(true);
+ });
+ });
+
+ describe('when member model type is `ProjectMember`', () => {
+ it('displays modal body', async () => {
+ await createComponent({
+ member: {
+ ...member,
+ type: MEMBER_MODEL_TYPE_PROJECT_MEMBER,
+ },
+ permissions,
+ });
+
+ expect(
+ findModal().findByText(LeaveModal.i18n.preventedBodyProjectMemberModelType).exists(),
+ ).toBe(true);
+ });
+ });
});
- it('displays form with correct action and inputs', () => {
+ it('displays form with correct action and inputs', async () => {
+ await createComponent();
+
const form = findForm();
expect(form.attributes('action')).toBe('/groups/foo-bar/-/group_members/leave');
@@ -89,7 +154,9 @@ describe('LeaveModal', () => {
});
describe('User deletion obstacles list', () => {
- it("displays obstacles list when member's user is part of on-call management", () => {
+ it("displays obstacles list when member's user is part of on-call management", async () => {
+ await createComponent();
+
const obstaclesList = findUserDeletionObstaclesList();
expect(obstaclesList.exists()).toBe(true);
expect(obstaclesList.props()).toMatchObject({
@@ -105,17 +172,18 @@ describe('LeaveModal', () => {
delete memberWithoutOncall.user.oncallSchedules;
delete memberWithoutOncall.user.escalationPolicies;
- createComponent({ member: memberWithoutOncall });
- await nextTick();
+ await createComponent({ member: memberWithoutOncall });
expect(findUserDeletionObstaclesList().exists()).toBe(false);
});
});
- it('submits the form when "Leave" button is clicked', () => {
+ it('submits the form when "Leave" button is clicked', async () => {
+ await createComponent();
+
const submitSpy = jest.spyOn(findForm().element, 'submit');
- getByText('Leave').trigger('click');
+ findModal().findByText('Leave').trigger('click');
expect(submitSpy).toHaveBeenCalled();
diff --git a/spec/frontend/members/components/modals/remove_member_modal_spec.js b/spec/frontend/members/components/modals/remove_member_modal_spec.js
index 59b112492b8..47a03b5083a 100644
--- a/spec/frontend/members/components/modals/remove_member_modal_spec.js
+++ b/spec/frontend/members/components/modals/remove_member_modal_spec.js
@@ -3,7 +3,11 @@ import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
import RemoveMemberModal from '~/members/components/modals/remove_member_modal.vue';
-import { MEMBER_TYPES } from '~/members/constants';
+import {
+ MEMBER_TYPES,
+ MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ MEMBER_MODEL_TYPE_PROJECT_MEMBER,
+} from '~/members/constants';
import { OBSTACLE_TYPES } from '~/vue_shared/components/user_deletion_obstacles/constants';
import UserDeletionObstaclesList from '~/vue_shared/components/user_deletion_obstacles/user_deletion_obstacles_list.vue';
@@ -55,16 +59,16 @@ describe('RemoveMemberModal', () => {
});
describe.each`
- state | memberType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | userDeletionObstacles | isPartOfOncall
- ${'removing a group member'} | ${'GroupMember'} | ${false} | ${false} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${{}} | ${false}
- ${'removing a project member'} | ${'ProjectMember'} | ${false} | ${false} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockObstacles} | ${true}
- ${'denying an access request'} | ${'ProjectMember'} | ${true} | ${false} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${{}} | ${false}
- ${'revoking invite'} | ${'ProjectMember'} | ${false} | ${true} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockObstacles} | ${false}
+ state | memberModelType | isAccessRequest | isInvite | actionText | removeSubMembershipsCheckboxExpected | unassignIssuablesCheckboxExpected | message | userDeletionObstacles | isPartOfOncall
+ ${'removing a group member'} | ${MEMBER_MODEL_TYPE_GROUP_MEMBER} | ${false} | ${false} | ${'Remove member'} | ${true} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${{}} | ${false}
+ ${'removing a project member'} | ${MEMBER_MODEL_TYPE_PROJECT_MEMBER} | ${false} | ${false} | ${'Remove member'} | ${false} | ${true} | ${'Are you sure you want to remove Jane Doe from the Gitlab Org / Gitlab Test project?'} | ${mockObstacles} | ${true}
+ ${'denying an access request'} | ${MEMBER_MODEL_TYPE_PROJECT_MEMBER} | ${true} | ${false} | ${'Deny access request'} | ${false} | ${false} | ${"Are you sure you want to deny Jane Doe's request to join the Gitlab Org / Gitlab Test project?"} | ${{}} | ${false}
+ ${'revoking invite'} | ${MEMBER_MODEL_TYPE_PROJECT_MEMBER} | ${false} | ${true} | ${'Revoke invite'} | ${false} | ${false} | ${'Are you sure you want to revoke the invitation for foo@bar.com to join the Gitlab Org / Gitlab Test project?'} | ${mockObstacles} | ${false}
`(
'when $state',
({
actionText,
- memberType,
+ memberModelType,
isAccessRequest,
isInvite,
message,
@@ -79,7 +83,7 @@ describe('RemoveMemberModal', () => {
isInvite,
message,
memberPath,
- memberType,
+ memberModelType,
userDeletionObstacles,
});
});
@@ -133,4 +137,28 @@ describe('RemoveMemberModal', () => {
});
},
);
+
+ describe('when removal is prevented', () => {
+ const message =
+ 'A group must have at least one owner. To remove the member, assign a new owner.';
+
+ beforeEach(() => {
+ createComponent({
+ actionText: 'Remove member',
+ memberModelType: MEMBER_MODEL_TYPE_GROUP_MEMBER,
+ isAccessRequest: false,
+ isInvite: false,
+ message,
+ preventRemoval: true,
+ });
+ });
+
+ it('does not show primary action button', () => {
+ expect(findGlModal().props('actionPrimary')).toBe(null);
+ });
+
+ it('only shows the message', () => {
+ expect(findGlModal().text()).toBe(message);
+ });
+ });
});
diff --git a/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap b/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap
new file mode 100644
index 00000000000..a0d9bae8a0b
--- /dev/null
+++ b/spec/frontend/members/components/table/__snapshots__/member_activity_spec.js.snap
@@ -0,0 +1,61 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`MemberActivity with a member that does not have all of the fields renders \`User created\` field 1`] = `
+<div>
+ <!---->
+
+ <div>
+ <strong>
+ Access granted:
+ </strong>
+
+ <span>
+
+ Aug 06, 2020
+
+ </span>
+ </div>
+
+ <!---->
+</div>
+`;
+
+exports[`MemberActivity with a member that has all fields renders \`User created\`, \`Access granted\`, and \`Last activity\` fields 1`] = `
+<div>
+ <div>
+ <strong>
+ User created:
+ </strong>
+
+ <span>
+
+ Mar 10, 2022
+
+ </span>
+ </div>
+
+ <div>
+ <strong>
+ Access granted:
+ </strong>
+
+ <span>
+
+ Jul 17, 2020
+
+ </span>
+ </div>
+
+ <div>
+ <strong>
+ Last activity:
+ </strong>
+
+ <span>
+
+ Mar 15, 2022
+
+ </span>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/members/components/table/created_at_spec.js b/spec/frontend/members/components/table/created_at_spec.js
index 793c122587d..fa31177564b 100644
--- a/spec/frontend/members/components/table/created_at_spec.js
+++ b/spec/frontend/members/components/table/created_at_spec.js
@@ -1,20 +1,18 @@
-import { within } from '@testing-library/dom';
-import { mount, createWrapper } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { useFakeDate } from 'helpers/fake_date';
import CreatedAt from '~/members/components/table/created_at.vue';
-import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
describe('CreatedAt', () => {
// March 15th, 2020
useFakeDate(2020, 2, 15);
const date = '2020-03-01T00:00:00.000';
- const dateTimeAgo = '2 weeks ago';
+ const formattedDate = 'Mar 01, 2020';
let wrapper;
const createComponent = (propsData) => {
- wrapper = mount(CreatedAt, {
+ wrapper = mountExtended(CreatedAt, {
propsData: {
date,
...propsData,
@@ -22,9 +20,6 @@ describe('CreatedAt', () => {
});
};
- const getByText = (text, options) =>
- createWrapper(within(wrapper.element).getByText(text, options));
-
afterEach(() => {
wrapper.destroy();
});
@@ -35,11 +30,7 @@ describe('CreatedAt', () => {
});
it('displays created at text', () => {
- expect(getByText(dateTimeAgo).exists()).toBe(true);
- });
-
- it('uses `TimeAgoTooltip` component to display tooltip', () => {
- expect(wrapper.findComponent(TimeAgoTooltip).exists()).toBe(true);
+ expect(wrapper.findByText(formattedDate).exists()).toBe(true);
});
});
@@ -52,7 +43,7 @@ describe('CreatedAt', () => {
},
});
- const link = getByText('Administrator');
+ const link = wrapper.findByRole('link', { name: 'Administrator' });
expect(link.exists()).toBe(true);
expect(link.attributes('href')).toBe('https://gitlab.com/root');
diff --git a/spec/frontend/members/components/table/member_action_buttons_spec.js b/spec/frontend/members/components/table/member_action_buttons_spec.js
index 03cfc6ca0f6..402a5e9db27 100644
--- a/spec/frontend/members/components/table/member_action_buttons_spec.js
+++ b/spec/frontend/members/components/table/member_action_buttons_spec.js
@@ -2,7 +2,7 @@ import { shallowMount } from '@vue/test-utils';
import AccessRequestActionButtons from '~/members/components/action_buttons/access_request_action_buttons.vue';
import GroupActionButtons from '~/members/components/action_buttons/group_action_buttons.vue';
import InviteActionButtons from '~/members/components/action_buttons/invite_action_buttons.vue';
-import UserActionButtons from '~/members/components/action_buttons/user_action_buttons.vue';
+import UserActionDropdown from '~/members/components/action_dropdowns/user_action_dropdown.vue';
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
@@ -29,7 +29,7 @@ describe('MemberActionButtons', () => {
it.each`
memberType | member | expectedComponent | expectedComponentName
- ${MEMBER_TYPES.user} | ${memberMock} | ${UserActionButtons} | ${'UserActionButtons'}
+ ${MEMBER_TYPES.user} | ${memberMock} | ${UserActionDropdown} | ${'UserActionDropdown'}
${MEMBER_TYPES.group} | ${group} | ${GroupActionButtons} | ${'GroupActionButtons'}
${MEMBER_TYPES.invite} | ${invite} | ${InviteActionButtons} | ${'InviteActionButtons'}
${MEMBER_TYPES.accessRequest} | ${accessRequest} | ${AccessRequestActionButtons} | ${'AccessRequestActionButtons'}
diff --git a/spec/frontend/members/components/table/member_activity_spec.js b/spec/frontend/members/components/table/member_activity_spec.js
new file mode 100644
index 00000000000..a372b40fd1f
--- /dev/null
+++ b/spec/frontend/members/components/table/member_activity_spec.js
@@ -0,0 +1,40 @@
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import MemberActivity from '~/members/components/table/member_activity.vue';
+import { member as memberMock, group as groupLinkMock } from '../../mock_data';
+
+describe('MemberActivity', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ member: memberMock,
+ };
+
+ const createComponent = ({ propsData = {} } = {}) => {
+ wrapper = mountExtended(MemberActivity, {
+ propsData: {
+ ...defaultPropsData,
+ ...propsData,
+ },
+ });
+ };
+
+ describe('with a member that has all fields', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders `User created`, `Access granted`, and `Last activity` fields', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('with a member that does not have all of the fields', () => {
+ beforeEach(() => {
+ createComponent({ propsData: { member: groupLinkMock } });
+ });
+
+ it('renders `User created` field', () => {
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+});
diff --git a/spec/frontend/members/components/table/member_source_spec.js b/spec/frontend/members/components/table/member_source_spec.js
index 2cd888207b1..fbfd0ca7ae7 100644
--- a/spec/frontend/members/components/table/member_source_spec.js
+++ b/spec/frontend/members/components/table/member_source_spec.js
@@ -1,19 +1,25 @@
-import { getByText as getByTextHelper } from '@testing-library/dom';
-import { mount, createWrapper } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import MemberSource from '~/members/components/table/member_source.vue';
describe('MemberSource', () => {
let wrapper;
+ const memberSource = {
+ id: 102,
+ fullName: 'Foo bar',
+ webUrl: 'https://gitlab.com/groups/foo-bar',
+ };
+
+ const createdBy = {
+ name: 'Administrator',
+ webUrl: 'https://gitlab.com/root',
+ };
+
const createComponent = (propsData) => {
- wrapper = mount(MemberSource, {
+ wrapper = mountExtended(MemberSource, {
propsData: {
- memberSource: {
- id: 102,
- fullName: 'Foo bar',
- webUrl: 'https://gitlab.com/groups/foo-bar',
- },
+ memberSource,
...propsData,
},
directives: {
@@ -22,9 +28,6 @@ describe('MemberSource', () => {
});
};
- const getByText = (text, options) =>
- createWrapper(getByTextHelper(wrapper.element, text, options));
-
const getTooltipDirective = (elementWrapper) => getBinding(elementWrapper.element, 'gl-tooltip');
afterEach(() => {
@@ -32,40 +35,69 @@ describe('MemberSource', () => {
});
describe('direct member', () => {
- it('displays "Direct member"', () => {
- createComponent({
- isDirectMember: true,
+ describe('when created by is available', () => {
+ it('displays "Direct member by <user name>"', () => {
+ createComponent({
+ isDirectMember: true,
+ createdBy,
+ });
+
+ expect(wrapper.text()).toBe('Direct member by Administrator');
+ expect(wrapper.findByRole('link', { name: createdBy.name }).attributes('href')).toBe(
+ createdBy.webUrl,
+ );
});
+ });
- expect(getByText('Direct member').exists()).toBe(true);
+ describe('when created by is not available', () => {
+ it('displays "Direct member"', () => {
+ createComponent({
+ isDirectMember: true,
+ });
+
+ expect(wrapper.text()).toBe('Direct member');
+ });
});
});
describe('inherited member', () => {
- let sourceGroupLink;
-
- beforeEach(() => {
- createComponent({
- isDirectMember: false,
+ describe('when created by is available', () => {
+ beforeEach(() => {
+ createComponent({
+ isDirectMember: false,
+ createdBy,
+ });
});
- sourceGroupLink = getByText('Foo bar');
+ it('displays "<group name> by <user name>"', () => {
+ expect(wrapper.text()).toBe('Foo bar by Administrator');
+ expect(wrapper.findByRole('link', { name: memberSource.fullName }).attributes('href')).toBe(
+ memberSource.webUrl,
+ );
+ expect(wrapper.findByRole('link', { name: createdBy.name }).attributes('href')).toBe(
+ createdBy.webUrl,
+ );
+ });
});
- it('displays a link to source group', () => {
- createComponent({
- isDirectMember: false,
+ describe('when created by is not available', () => {
+ beforeEach(() => {
+ createComponent({
+ isDirectMember: false,
+ });
});
- expect(sourceGroupLink.exists()).toBe(true);
- expect(sourceGroupLink.attributes('href')).toBe('https://gitlab.com/groups/foo-bar');
- });
+ it('displays a link to source group', () => {
+ expect(wrapper.text()).toBe(memberSource.fullName);
+ expect(wrapper.attributes('href')).toBe(memberSource.webUrl);
+ });
- it('displays tooltip with "Inherited"', () => {
- const tooltipDirective = getTooltipDirective(sourceGroupLink);
+ it('displays tooltip with "Inherited"', () => {
+ const tooltipDirective = getTooltipDirective(wrapper);
- expect(tooltipDirective).not.toBeUndefined();
- expect(sourceGroupLink.attributes('title')).toBe('Inherited');
+ expect(tooltipDirective).not.toBeUndefined();
+ expect(tooltipDirective.value).toBe('Inherited');
+ });
});
});
});
diff --git a/spec/frontend/members/components/table/members_table_cell_spec.js b/spec/frontend/members/components/table/members_table_cell_spec.js
index 0b0140b0cdb..ac5d83d028d 100644
--- a/spec/frontend/members/components/table/members_table_cell_spec.js
+++ b/spec/frontend/members/components/table/members_table_cell_spec.js
@@ -3,6 +3,7 @@ import Vue from 'vue';
import Vuex from 'vuex';
import MembersTableCell from '~/members/components/table/members_table_cell.vue';
import { MEMBER_TYPES } from '~/members/constants';
+import { canRemoveBlockedByLastOwner } from '~/members/utils';
import {
member as memberMock,
directMember,
@@ -12,6 +13,11 @@ import {
accessRequest,
} from '../../mock_data';
+jest.mock('~/members/utils', () => ({
+ ...jest.requireActual('~/members/utils'),
+ canRemoveBlockedByLastOwner: jest.fn().mockImplementation(() => true),
+}));
+
describe('MembersTableCell', () => {
const WrappedComponent = {
props: {
@@ -55,6 +61,7 @@ describe('MembersTableCell', () => {
provide: {
sourceId: 1,
currentUserId: 1,
+ canManageMembers: true,
},
scopedSlots: {
default: `
@@ -179,6 +186,15 @@ describe('MembersTableCell', () => {
});
});
+ describe('canRemoveBlockedByLastOwner', () => {
+ it('calls util and returns value', () => {
+ createComponentWithDirectMember();
+
+ expect(canRemoveBlockedByLastOwner).toHaveBeenCalledWith(directMember, true);
+ expect(findWrappedComponent().props('permissions').canRemoveBlockedByLastOwner).toBe(true);
+ });
+ });
+
describe('canResend', () => {
describe('when member type is `invite`', () => {
it('returns `true` when `canResend` is `true`', () => {
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 0ed01396fcb..1d18026a410 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -8,9 +8,9 @@ import ExpirationDatepicker from '~/members/components/table/expiration_datepick
import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
import MemberAvatar from '~/members/components/table/member_avatar.vue';
import MemberSource from '~/members/components/table/member_source.vue';
+import MemberActivity from '~/members/components/table/member_activity.vue';
import MembersTable from '~/members/components/table/members_table.vue';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
-import UserDate from '~/vue_shared/components/user_date.vue';
import {
MEMBER_TYPES,
MEMBER_STATE_CREATED,
@@ -63,6 +63,7 @@ describe('MembersTable', () => {
provide: {
sourceId: 1,
currentUserId: 1,
+ canManageMembers: true,
namespace: MEMBER_TYPES.invite,
...provide,
},
@@ -106,16 +107,14 @@ describe('MembersTable', () => {
};
it.each`
- field | label | member | expectedComponent
- ${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
- ${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
- ${'granted'} | ${'Access granted'} | ${memberMock} | ${CreatedAt}
- ${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
- ${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
- ${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
- ${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
- ${'userCreatedAt'} | ${'Created on'} | ${memberMock} | ${UserDate}
- ${'lastActivityOn'} | ${'Last activity'} | ${memberMock} | ${UserDate}
+ field | label | member | expectedComponent
+ ${'account'} | ${'Account'} | ${memberMock} | ${MemberAvatar}
+ ${'source'} | ${'Source'} | ${memberMock} | ${MemberSource}
+ ${'invited'} | ${'Invited'} | ${invite} | ${CreatedAt}
+ ${'requested'} | ${'Requested'} | ${accessRequest} | ${CreatedAt}
+ ${'maxRole'} | ${'Max role'} | ${memberCanUpdate} | ${RoleDropdown}
+ ${'expiration'} | ${'Expiration'} | ${memberMock} | ${ExpirationDatepicker}
+ ${'activity'} | ${'Activity'} | ${memberMock} | ${MemberActivity}
`('renders the $label field', ({ field, label, member, expectedComponent }) => {
createComponent({
members: [member],
@@ -202,16 +201,23 @@ describe('MembersTable', () => {
canRemove: true,
};
+ const memberCanRemoveBlockedLastOwner = {
+ ...directMember,
+ canRemove: false,
+ isLastOwner: true,
+ };
+
const memberNoPermissions = {
...memberMock,
id: 2,
};
describe.each`
- permission | members
- ${'canUpdate'} | ${[memberNoPermissions, memberCanUpdate]}
- ${'canRemove'} | ${[memberNoPermissions, memberCanRemove]}
- ${'canResend'} | ${[memberNoPermissions, invite]}
+ permission | members
+ ${'canUpdate'} | ${[memberNoPermissions, memberCanUpdate]}
+ ${'canRemove'} | ${[memberNoPermissions, memberCanRemove]}
+ ${'canRemoveBlockedByLastOwner'} | ${[memberNoPermissions, memberCanRemoveBlockedLastOwner]}
+ ${'canResend'} | ${[memberNoPermissions, invite]}
`('when one of the members has $permission permissions', ({ members }) => {
it('renders the "Actions" field', () => {
createComponent({ members, tableFields: ['actions'] });
@@ -230,10 +236,11 @@ describe('MembersTable', () => {
});
describe.each`
- permission | members
- ${'canUpdate'} | ${[memberMock]}
- ${'canRemove'} | ${[memberMock]}
- ${'canResend'} | ${[{ ...invite, invite: { ...invite.invite, canResend: false } }]}
+ permission | members
+ ${'canUpdate'} | ${[memberMock]}
+ ${'canRemove'} | ${[memberMock]}
+ ${'canRemoveBlockedByLastOwner'} | ${[memberMock]}
+ ${'canResend'} | ${[{ ...invite, invite: { ...invite.invite, canResend: false } }]}
`('when none of the members have $permission permissions', ({ members }) => {
it('does not render the "Actions" field', () => {
createComponent({ members, tableFields: ['actions'] });
diff --git a/spec/frontend/members/components/table/role_dropdown_spec.js b/spec/frontend/members/components/table/role_dropdown_spec.js
index b254cce4d72..a11f67be8f5 100644
--- a/spec/frontend/members/components/table/role_dropdown_spec.js
+++ b/spec/frontend/members/components/table/role_dropdown_spec.js
@@ -4,11 +4,14 @@ import { within } from '@testing-library/dom';
import { mount, createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import waitForPromises from 'helpers/wait_for_promises';
import RoleDropdown from '~/members/components/table/role_dropdown.vue';
import { MEMBER_TYPES } from '~/members/constants';
+import { guestOverageConfirmAction } from 'ee_else_ce/members/guest_overage_confirm_action';
import { member } from '../../mock_data';
Vue.use(Vuex);
+jest.mock('ee_else_ce/members/guest_overage_confirm_action');
describe('RoleDropdown', () => {
let wrapper;
@@ -33,6 +36,10 @@ describe('RoleDropdown', () => {
wrapper = mount(RoleDropdown, {
provide: {
namespace: MEMBER_TYPES.user,
+ group: {
+ name: 'groupname',
+ path: '/grouppath/',
+ },
},
propsData: {
member,
@@ -63,12 +70,21 @@ describe('RoleDropdown', () => {
const findDropdownToggle = () => wrapper.find('button[aria-haspopup="true"]');
const findDropdown = () => wrapper.findComponent(GlDropdown);
+ let originalGon;
+
+ beforeEach(() => {
+ originalGon = window.gon;
+ gon.features = { showOverageOnRolePromotion: true };
+ });
+
afterEach(() => {
+ window.gon = originalGon;
wrapper.destroy();
});
describe('when dropdown is open', () => {
beforeEach(() => {
+ guestOverageConfirmAction.mockReturnValue(true);
createComponent();
return findDropdownToggle().trigger('click');
@@ -113,12 +129,16 @@ describe('RoleDropdown', () => {
expect($toast.show).toHaveBeenCalledWith('Role updated successfully.');
});
- it('disables dropdown while waiting for `updateMemberRole` to resolve', async () => {
+ it('puts dropdown in loading state while waiting for `updateMemberRole` to resolve', async () => {
await getDropdownItemByText('Developer').trigger('click');
- expect(findDropdown().props('disabled')).toBe(true);
+ expect(findDropdown().props('loading')).toBe(true);
+ });
+
+ it('enables dropdown after `updateMemberRole` resolves', async () => {
+ await getDropdownItemByText('Developer').trigger('click');
- await nextTick();
+ await waitForPromises();
expect(findDropdown().props('disabled')).toBe(false);
});
@@ -148,4 +168,44 @@ describe('RoleDropdown', () => {
expect(findDropdown().props('right')).toBe(false);
});
+
+ describe('guestOverageConfirmAction', () => {
+ const mockConfirmAction = ({ confirmed }) => {
+ guestOverageConfirmAction.mockResolvedValueOnce(confirmed);
+ };
+
+ beforeEach(() => {
+ createComponent();
+
+ findDropdownToggle().trigger('click');
+ });
+
+ afterEach(() => {
+ guestOverageConfirmAction.mockReset();
+ });
+
+ describe('when guestOverageConfirmAction returns true', () => {
+ beforeEach(() => {
+ mockConfirmAction({ confirmed: true });
+
+ getDropdownItemByText('Reporter').trigger('click');
+ });
+
+ it('calls updateMemberRole', () => {
+ expect(actions.updateMemberRole).toHaveBeenCalled();
+ });
+ });
+
+ describe('when guestOverageConfirmAction returns false', () => {
+ beforeEach(() => {
+ mockConfirmAction({ confirmed: false });
+
+ getDropdownItemByText('Reporter').trigger('click');
+ });
+
+ it('does not call updateMemberRole', () => {
+ expect(actions.updateMemberRole).not.toHaveBeenCalled();
+ });
+ });
+ });
});
diff --git a/spec/frontend/members/guest_overage_confirm_action_spec.js b/spec/frontend/members/guest_overage_confirm_action_spec.js
new file mode 100644
index 00000000000..d7ab54fa13b
--- /dev/null
+++ b/spec/frontend/members/guest_overage_confirm_action_spec.js
@@ -0,0 +1,7 @@
+import { guestOverageConfirmAction } from '~/members/guest_overage_confirm_action';
+
+describe('guestOverageConfirmAction', () => {
+ it('returns true', () => {
+ expect(guestOverageConfirmAction()).toBe(true);
+ });
+});
diff --git a/spec/frontend/members/mock_data.js b/spec/frontend/members/mock_data.js
index 49c4c46c3ac..161e96c0c48 100644
--- a/spec/frontend/members/mock_data.js
+++ b/spec/frontend/members/mock_data.js
@@ -1,4 +1,8 @@
-import { MEMBER_TYPES, MEMBER_STATE_CREATED } from '~/members/constants';
+import {
+ MEMBER_TYPES,
+ MEMBER_STATE_CREATED,
+ MEMBER_MODEL_TYPE_GROUP_MEMBER,
+} from '~/members/constants';
export const member = {
requestedAt: null,
@@ -13,7 +17,7 @@ export const member = {
fullName: 'Foo Bar',
webUrl: 'https://gitlab.com/groups/foo-bar',
},
- type: 'GroupMember',
+ type: MEMBER_MODEL_TYPE_GROUP_MEMBER,
state: MEMBER_STATE_CREATED,
user: {
id: 123,
@@ -69,7 +73,7 @@ export const modalData = {
isAccessRequest: true,
isInvite: true,
memberPath: '/groups/foo-bar/-/group_members/1',
- memberType: 'GroupMember',
+ memberModelType: MEMBER_MODEL_TYPE_GROUP_MEMBER,
message: 'Are you sure you want to remove John Smith?',
userDeletionObstacles: { name: 'user', obstacles: [] },
};
@@ -123,7 +127,15 @@ export const dataAttribute = JSON.stringify({
pagination: paginationData,
member_path: '/groups/foo-bar/-/group_members/:id',
ldap_override_path: '/groups/ldap-group/-/group_members/:id/override',
+ disable_two_factor_path: '/groups/ldap-group/-/two_factor_auth',
},
source_id: 234,
can_manage_members: true,
});
+
+export const permissions = {
+ canRemove: true,
+ canRemoveBlockedByLastOwner: false,
+ canResend: true,
+ canUpdate: true,
+};
diff --git a/spec/frontend/members/store/actions_spec.js b/spec/frontend/members/store/actions_spec.js
index 20dce639177..38214048b23 100644
--- a/spec/frontend/members/store/actions_spec.js
+++ b/spec/frontend/members/store/actions_spec.js
@@ -4,7 +4,7 @@ import { noop } from 'lodash';
import { useFakeDate } from 'helpers/fake_date';
import testAction from 'helpers/vuex_action_helper';
import { members, group, modalData } from 'jest/members/mock_data';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
updateMemberRole,
showRemoveGroupLinkModal,
@@ -44,7 +44,7 @@ describe('Vuex members actions', () => {
describe('successful request', () => {
it(`commits ${types.RECEIVE_MEMBER_ROLE_SUCCESS} mutation`, async () => {
- mock.onPut().replyOnce(httpStatusCodes.OK);
+ mock.onPut().replyOnce(HTTP_STATUS_OK);
await testAction(updateMemberRole, payload, state, [
{
@@ -83,7 +83,7 @@ describe('Vuex members actions', () => {
describe('successful request', () => {
describe('changing expiration date', () => {
it(`commits ${types.RECEIVE_MEMBER_EXPIRATION_SUCCESS} mutation`, async () => {
- mock.onPut().replyOnce(httpStatusCodes.OK);
+ mock.onPut().replyOnce(HTTP_STATUS_OK);
await testAction(updateMemberExpiration, { memberId, expiresAt }, state, [
{
@@ -98,7 +98,7 @@ describe('Vuex members actions', () => {
describe('removing the expiration date', () => {
it(`commits ${types.RECEIVE_MEMBER_EXPIRATION_SUCCESS} mutation`, async () => {
- mock.onPut().replyOnce(httpStatusCodes.OK);
+ mock.onPut().replyOnce(HTTP_STATUS_OK);
await testAction(updateMemberExpiration, { memberId, expiresAt: null }, state, [
{
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index 8bef2096a2a..9f200324c02 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -13,8 +13,10 @@ import {
isDirectMember,
isCurrentUser,
canRemove,
+ canRemoveBlockedByLastOwner,
canResend,
canUpdate,
+ canDisableTwoFactor,
canOverride,
parseSortParam,
buildSortHref,
@@ -129,6 +131,17 @@ describe('Members Utils', () => {
});
});
+ describe('canRemoveBlockedByLastOwner', () => {
+ it.each`
+ member | canManageMembers | expected
+ ${{ ...directMember, isLastOwner: true }} | ${true} | ${true}
+ ${{ ...inheritedMember, isLastOwner: false }} | ${true} | ${false}
+ ${{ ...directMember, isLastOwner: true }} | ${false} | ${false}
+ `('returns $expected', ({ member, canManageMembers, expected }) => {
+ expect(canRemoveBlockedByLastOwner(member, canManageMembers)).toBe(expected);
+ });
+ });
+
describe('canResend', () => {
it.each`
member | expected
@@ -151,6 +164,19 @@ describe('Members Utils', () => {
});
});
+ describe('canDisableTwoFactor', () => {
+ it.each`
+ member | expected
+ ${{ ...memberMock, canGetTwoFactorDisabled: true }} | ${false}
+ ${{ ...memberMock, canGetTwoFactorDisabled: false }} | ${false}
+ `(
+ 'returns $expected for members whose two factor authentication can be disabled',
+ ({ member, expected }) => {
+ expect(canDisableTwoFactor(member)).toBe(expected);
+ },
+ );
+ });
+
describe('canOverride', () => {
it('returns `false`', () => {
expect(canOverride(memberMock)).toBe(false);
diff --git a/spec/frontend/merge_request_tabs_spec.js b/spec/frontend/merge_request_tabs_spec.js
index 69ff5e47689..6d434d7e654 100644
--- a/spec/frontend/merge_request_tabs_spec.js
+++ b/spec/frontend/merge_request_tabs_spec.js
@@ -5,6 +5,7 @@ import initMrPage from 'helpers/init_vue_mr_page_helper';
import { stubPerformanceWebAPI } from 'helpers/performance';
import axios from '~/lib/utils/axios_utils';
import MergeRequestTabs from '~/merge_request_tabs';
+import Diff from '~/diff';
import '~/lib/utils/common_utils';
import '~/lib/utils/url_utility';
@@ -389,4 +390,73 @@ describe('MergeRequestTabs', () => {
});
});
});
+
+ describe('tabs <-> diff interactions', () => {
+ beforeEach(() => {
+ jest.spyOn(testContext.class, 'loadDiff').mockImplementation(() => {});
+ });
+
+ describe('switchViewType', () => {
+ it('marks the class as having not loaded diffs already', () => {
+ testContext.class.diffsLoaded = true;
+
+ testContext.class.switchViewType({});
+
+ expect(testContext.class.diffsLoaded).toBe(false);
+ });
+
+ it('reloads the diffs', () => {
+ testContext.class.switchViewType({ source: 'a new url' });
+
+ expect(testContext.class.loadDiff).toHaveBeenCalledWith({
+ endpoint: 'a new url',
+ strip: false,
+ });
+ });
+ });
+
+ describe('createDiff', () => {
+ it("creates a Diff if there isn't one", () => {
+ expect(testContext.class.diffsClass).toBe(null);
+
+ testContext.class.createDiff();
+
+ expect(testContext.class.diffsClass).toBeInstanceOf(Diff);
+ });
+
+ it("doesn't create a Diff if one already exists", () => {
+ testContext.class.diffsClass = 'truthy';
+
+ testContext.class.createDiff();
+
+ expect(testContext.class.diffsClass).toBe('truthy');
+ });
+
+ it('sets the available MR Tabs event hub to the new Diff', () => {
+ expect(testContext.class.diffsClass).toBe(null);
+
+ testContext.class.createDiff();
+
+ expect(testContext.class.diffsClass.mrHub).toBe(testContext.class.eventHub);
+ });
+ });
+
+ describe('setHubToDiff', () => {
+ it('sets the MR Tabs event hub to the child Diff', () => {
+ testContext.class.diffsClass = {};
+
+ testContext.class.setHubToDiff();
+
+ expect(testContext.class.diffsClass.mrHub).toBe(testContext.class.eventHub);
+ });
+
+ it('does not fatal if theres no child Diff', () => {
+ testContext.class.diffsClass = null;
+
+ expect(() => {
+ testContext.class.setHubToDiff();
+ }).not.toThrow();
+ });
+ });
+ });
});
diff --git a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap
index 8af0753f929..0c3d3e78038 100644
--- a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap
+++ b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap
@@ -163,8 +163,8 @@ exports[`MlCandidate renders correctly 1`] = `
class="gl-text-secondary gl-font-weight-bold"
>
- Parameters
-
+ Parameters
+
</td>
<td
@@ -190,7 +190,6 @@ exports[`MlCandidate renders correctly 1`] = `
3
</td>
</tr>
-
<tr
class="divider"
/>
@@ -200,8 +199,8 @@ exports[`MlCandidate renders correctly 1`] = `
class="gl-text-secondary gl-font-weight-bold"
>
- Metrics
-
+ Metrics
+
</td>
<td
@@ -227,6 +226,42 @@ exports[`MlCandidate renders correctly 1`] = `
.99
</td>
</tr>
+ <tr
+ class="divider"
+ />
+
+ <tr>
+ <td
+ class="gl-text-secondary gl-font-weight-bold"
+ >
+
+ Metadata
+
+ </td>
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ FileName
+ </td>
+
+ <td>
+ test.py
+ </td>
+ </tr>
+ <tr>
+ <td />
+
+ <td
+ class="gl-font-weight-bold"
+ >
+ ExecutionTime
+ </td>
+
+ <td>
+ .0856
+ </td>
+ </tr>
</tbody>
</table>
</div>
diff --git a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap
index e253a0afc6c..3ee2c1cc075 100644
--- a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap
+++ b/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap
@@ -95,8 +95,8 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
<table
aria-busy="false"
- aria-colcount="6"
- class="table b-table gl-table gl-mt-0!"
+ aria-colcount="9"
+ class="table b-table gl-table gl-mt-0! ml-candidate-table table-sm"
role="table"
>
<!---->
@@ -117,7 +117,7 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
scope="col"
>
<div>
- L1 Ratio
+ Name
</div>
</th>
<th
@@ -127,7 +127,7 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
scope="col"
>
<div>
- Rmse
+ Created at
</div>
</th>
<th
@@ -137,7 +137,7 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
scope="col"
>
<div>
- Auc
+ User
</div>
</th>
<th
@@ -147,11 +147,41 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
scope="col"
>
<div>
- Mae
+ L1 Ratio
</div>
</th>
<th
aria-colindex="5"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Rmse
+ </div>
+ </th>
+ <th
+ aria-colindex="6"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Auc
+ </div>
+ </th>
+ <th
+ aria-colindex="7"
+ class=""
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Mae
+ </div>
+ </th>
+ <th
+ aria-colindex="8"
aria-label="Details"
class=""
role="columnheader"
@@ -160,7 +190,7 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
<div />
</th>
<th
- aria-colindex="6"
+ aria-colindex="9"
aria-label="Artifact"
class=""
role="columnheader"
@@ -183,39 +213,97 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
class=""
role="cell"
>
- 0.4
+ <div
+ title="aCandidate"
+ >
+ aCandidate
+ </div>
</td>
<td
aria-colindex="2"
class=""
role="cell"
>
- 1
+ <time
+ class=""
+ datetime="2023-01-05T14:07:02.975Z"
+ title="2023-01-05T14:07:02.975Z"
+ >
+ in 2 years
+ </time>
</td>
<td
aria-colindex="3"
class=""
role="cell"
- />
+ >
+ <a
+ class="gl-link"
+ href="/root"
+ title="root"
+ >
+ @root
+ </a>
+ </td>
<td
aria-colindex="4"
class=""
role="cell"
- />
+ >
+ <div
+ title="0.4"
+ >
+ 0.4
+ </div>
+ </td>
<td
aria-colindex="5"
class=""
role="cell"
>
+ <div
+ title="1"
+ >
+ 1
+ </div>
+ </td>
+ <td
+ aria-colindex="6"
+ class=""
+ role="cell"
+ >
+ <div
+ title=""
+ >
+
+ </div>
+ </td>
+ <td
+ aria-colindex="7"
+ class=""
+ role="cell"
+ >
+ <div
+ title=""
+ >
+
+ </div>
+ </td>
+ <td
+ aria-colindex="8"
+ class=""
+ role="cell"
+ >
<a
class="gl-link"
href="link_to_candidate1"
+ title="Details"
>
Details
</a>
</td>
<td
- aria-colindex="6"
+ aria-colindex="9"
class=""
role="cell"
>
@@ -224,6 +312,7 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
href="link_to_artifact"
rel="noopener"
target="_blank"
+ title="Artifacts"
>
Artifacts
</a>
@@ -238,47 +327,435 @@ exports[`MlExperiment with candidates renders correctly 1`] = `
class=""
role="cell"
>
- 0.5
+ <div
+ title=""
+ >
+
+ </div>
</td>
<td
aria-colindex="2"
class=""
role="cell"
- />
+ >
+ <time
+ class=""
+ datetime="2023-01-05T14:07:02.975Z"
+ title="2023-01-05T14:07:02.975Z"
+ >
+ in 2 years
+ </time>
+ </td>
<td
aria-colindex="3"
class=""
role="cell"
>
- 0.3
+ <div>
+ -
+ </div>
</td>
<td
aria-colindex="4"
class=""
role="cell"
- />
+ >
+ <div
+ title="0.5"
+ >
+ 0.5
+ </div>
+ </td>
<td
aria-colindex="5"
class=""
role="cell"
>
+ <div
+ title=""
+ >
+
+ </div>
+ </td>
+ <td
+ aria-colindex="6"
+ class=""
+ role="cell"
+ >
+ <div
+ title="0.3"
+ >
+ 0.3
+ </div>
+ </td>
+ <td
+ aria-colindex="7"
+ class=""
+ role="cell"
+ >
+ <div
+ title=""
+ >
+
+ </div>
+ </td>
+ <td
+ aria-colindex="8"
+ class=""
+ role="cell"
+ >
<a
class="gl-link"
href="link_to_candidate2"
+ title="Details"
>
Details
</a>
</td>
<td
+ aria-colindex="9"
+ class=""
+ role="cell"
+ >
+ <div
+ title="Artifacts"
+ >
+
+ -
+
+ </div>
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <div
+ title=""
+ >
+
+ </div>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+ <time
+ class=""
+ datetime="2023-01-05T14:07:02.975Z"
+ title="2023-01-05T14:07:02.975Z"
+ >
+ in 2 years
+ </time>
+ </td>
+ <td
+ aria-colindex="3"
+ class=""
+ role="cell"
+ >
+ <div>
+ -
+ </div>
+ </td>
+ <td
+ aria-colindex="4"
+ class=""
+ role="cell"
+ >
+ <div
+ title="0.5"
+ >
+ 0.5
+ </div>
+ </td>
+ <td
+ aria-colindex="5"
+ class=""
+ role="cell"
+ >
+ <div
+ title=""
+ >
+
+ </div>
+ </td>
+ <td
aria-colindex="6"
class=""
role="cell"
- />
+ >
+ <div
+ title="0.3"
+ >
+ 0.3
+ </div>
+ </td>
+ <td
+ aria-colindex="7"
+ class=""
+ role="cell"
+ >
+ <div
+ title=""
+ >
+
+ </div>
+ </td>
+ <td
+ aria-colindex="8"
+ class=""
+ role="cell"
+ >
+ <a
+ class="gl-link"
+ href="link_to_candidate3"
+ title="Details"
+ >
+ Details
+ </a>
+ </td>
+ <td
+ aria-colindex="9"
+ class=""
+ role="cell"
+ >
+ <div
+ title="Artifacts"
+ >
+
+ -
+
+ </div>
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <div
+ title=""
+ >
+
+ </div>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+ <time
+ class=""
+ datetime="2023-01-05T14:07:02.975Z"
+ title="2023-01-05T14:07:02.975Z"
+ >
+ in 2 years
+ </time>
+ </td>
+ <td
+ aria-colindex="3"
+ class=""
+ role="cell"
+ >
+ <div>
+ -
+ </div>
+ </td>
+ <td
+ aria-colindex="4"
+ class=""
+ role="cell"
+ >
+ <div
+ title="0.5"
+ >
+ 0.5
+ </div>
+ </td>
+ <td
+ aria-colindex="5"
+ class=""
+ role="cell"
+ >
+ <div
+ title=""
+ >
+
+ </div>
+ </td>
+ <td
+ aria-colindex="6"
+ class=""
+ role="cell"
+ >
+ <div
+ title="0.3"
+ >
+ 0.3
+ </div>
+ </td>
+ <td
+ aria-colindex="7"
+ class=""
+ role="cell"
+ >
+ <div
+ title=""
+ >
+
+ </div>
+ </td>
+ <td
+ aria-colindex="8"
+ class=""
+ role="cell"
+ >
+ <a
+ class="gl-link"
+ href="link_to_candidate4"
+ title="Details"
+ >
+ Details
+ </a>
+ </td>
+ <td
+ aria-colindex="9"
+ class=""
+ role="cell"
+ >
+ <div
+ title="Artifacts"
+ >
+
+ -
+
+ </div>
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <div
+ title=""
+ >
+
+ </div>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+ <time
+ class=""
+ datetime="2023-01-05T14:07:02.975Z"
+ title="2023-01-05T14:07:02.975Z"
+ >
+ in 2 years
+ </time>
+ </td>
+ <td
+ aria-colindex="3"
+ class=""
+ role="cell"
+ >
+ <div>
+ -
+ </div>
+ </td>
+ <td
+ aria-colindex="4"
+ class=""
+ role="cell"
+ >
+ <div
+ title="0.5"
+ >
+ 0.5
+ </div>
+ </td>
+ <td
+ aria-colindex="5"
+ class=""
+ role="cell"
+ >
+ <div
+ title=""
+ >
+
+ </div>
+ </td>
+ <td
+ aria-colindex="6"
+ class=""
+ role="cell"
+ >
+ <div
+ title="0.3"
+ >
+ 0.3
+ </div>
+ </td>
+ <td
+ aria-colindex="7"
+ class=""
+ role="cell"
+ >
+ <div
+ title=""
+ >
+
+ </div>
+ </td>
+ <td
+ aria-colindex="8"
+ class=""
+ role="cell"
+ >
+ <a
+ class="gl-link"
+ href="link_to_candidate5"
+ title="Details"
+ >
+ Details
+ </a>
+ </td>
+ <td
+ aria-colindex="9"
+ class=""
+ role="cell"
+ >
+ <div
+ title="Artifacts"
+ >
+
+ -
+
+ </div>
+ </td>
</tr>
<!---->
<!---->
</tbody>
<!---->
</table>
+
+ <!---->
</div>
`;
diff --git a/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js b/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
index 4b16312815a..fb45c4b07a4 100644
--- a/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
+++ b/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
@@ -15,6 +15,10 @@ describe('MlCandidate', () => {
{ name: 'AUC', value: '.55' },
{ name: 'Accuracy', value: '.99' },
],
+ metadata: [
+ { name: 'FileName', value: 'test.py' },
+ { name: 'ExecutionTime', value: '.0856' },
+ ],
info: {
iid: 'candidate_iid',
artifact_link: 'path_to_artifact',
diff --git a/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js b/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
index 50539440f25..abcaf17303f 100644
--- a/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
+++ b/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
@@ -1,12 +1,19 @@
-import { GlAlert } from '@gitlab/ui';
+import { GlAlert, GlPagination } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
describe('MlExperiment', () => {
let wrapper;
- const createWrapper = (candidates = [], metricNames = [], paramNames = []) => {
- return mountExtended(MlExperiment, { provide: { candidates, metricNames, paramNames } });
+ const createWrapper = (
+ candidates = [],
+ metricNames = [],
+ paramNames = [],
+ pagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 0 },
+ ) => {
+ return mountExtended(MlExperiment, {
+ provide: { candidates, metricNames, paramNames, pagination },
+ });
};
const findAlert = () => wrapper.findComponent(GlAlert);
@@ -25,20 +32,110 @@ describe('MlExperiment', () => {
expect(findEmptyState().exists()).toBe(true);
});
+
+ it('does not show pagination', () => {
+ wrapper = createWrapper();
+
+ expect(wrapper.findComponent(GlPagination).exists()).toBe(false);
+ });
});
describe('with candidates', () => {
- it('renders correctly', () => {
- wrapper = createWrapper(
+ const defaultPagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 5 };
+
+ const createWrapperWithCandidates = (pagination = defaultPagination) => {
+ return createWrapper(
[
- { rmse: 1, l1_ratio: 0.4, details: 'link_to_candidate1', artifact: 'link_to_artifact' },
- { auc: 0.3, l1_ratio: 0.5, details: 'link_to_candidate2' },
+ {
+ rmse: 1,
+ l1_ratio: 0.4,
+ details: 'link_to_candidate1',
+ artifact: 'link_to_artifact',
+ name: 'aCandidate',
+ created_at: '2023-01-05T14:07:02.975Z',
+ user: { username: 'root', path: '/root' },
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate2',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate3',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate4',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
+ {
+ auc: 0.3,
+ l1_ratio: 0.5,
+ details: 'link_to_candidate5',
+ created_at: '2023-01-05T14:07:02.975Z',
+ name: null,
+ user: null,
+ },
],
['rmse', 'auc', 'mae'],
['l1_ratio'],
+ pagination,
);
+ };
+
+ it('renders correctly', () => {
+ wrapper = createWrapperWithCandidates();
expect(wrapper.element).toMatchSnapshot();
});
+
+ describe('Pagination behaviour', () => {
+ it('should show', () => {
+ wrapper = createWrapperWithCandidates();
+
+ expect(wrapper.findComponent(GlPagination).exists()).toBe(true);
+ });
+
+ it('should get the page number from the URL', () => {
+ wrapper = createWrapperWithCandidates({ ...defaultPagination, page: 2 });
+
+ expect(wrapper.findComponent(GlPagination).props().value).toBe(2);
+ });
+
+ it('should not have a prevPage if the page is 1', () => {
+ wrapper = createWrapperWithCandidates();
+
+ expect(wrapper.findComponent(GlPagination).props().prevPage).toBe(null);
+ });
+
+ it('should set the prevPage to 1 if the page is 2', () => {
+ wrapper = createWrapperWithCandidates({ ...defaultPagination, page: 2 });
+
+ expect(wrapper.findComponent(GlPagination).props().prevPage).toBe(1);
+ });
+
+ it('should not have a nextPage if isLastPage is true', async () => {
+ wrapper = createWrapperWithCandidates({ ...defaultPagination, isLastPage: true });
+
+ expect(wrapper.findComponent(GlPagination).props().nextPage).toBe(null);
+ });
+
+ it('should set the nextPage to 2 if the page is 1', () => {
+ wrapper = createWrapperWithCandidates();
+
+ expect(wrapper.findComponent(GlPagination).props().nextPage).toBe(2);
+ });
+ });
});
});
diff --git a/spec/frontend/monitoring/requests/index_spec.js b/spec/frontend/monitoring/requests/index_spec.js
index def4bfe9443..cf7df3dd9d5 100644
--- a/spec/frontend/monitoring/requests/index_spec.js
+++ b/spec/frontend/monitoring/requests/index_spec.js
@@ -2,8 +2,12 @@ import MockAdapter from 'axios-mock-adapter';
import { backoffMockImplementation } from 'helpers/backoff_helper';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
-import statusCodes, {
+import {
+ HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_OK,
+ HTTP_STATUS_SERVICE_UNAVAILABLE,
+ HTTP_STATUS_UNAUTHORIZED,
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
import { getDashboard, getPrometheusQueryData } from '~/monitoring/requests';
@@ -32,7 +36,7 @@ describe('monitoring metrics_requests', () => {
};
it('returns a dashboard response', () => {
- mock.onGet(dashboardEndpoint).reply(statusCodes.OK, response);
+ mock.onGet(dashboardEndpoint).reply(HTTP_STATUS_OK, response);
return getDashboard(dashboardEndpoint, params).then((data) => {
expect(data).toEqual(metricsDashboardResponse);
@@ -42,7 +46,7 @@ describe('monitoring metrics_requests', () => {
it('returns a dashboard response after retrying twice', () => {
mock.onGet(dashboardEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(dashboardEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(dashboardEndpoint).reply(statusCodes.OK, response);
+ mock.onGet(dashboardEndpoint).reply(HTTP_STATUS_OK, response);
return getDashboard(dashboardEndpoint, params).then((data) => {
expect(data).toEqual(metricsDashboardResponse);
@@ -75,7 +79,7 @@ describe('monitoring metrics_requests', () => {
};
it('returns a dashboard response', () => {
- mock.onGet(prometheusEndpoint).reply(statusCodes.OK, response);
+ mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_OK, response);
return getPrometheusQueryData(prometheusEndpoint, params).then((data) => {
expect(data).toEqual(response.data);
@@ -86,7 +90,7 @@ describe('monitoring metrics_requests', () => {
// Mock multiple attempts while the cache is filling up
mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(prometheusEndpoint).reply(statusCodes.OK, response); // 3rd attempt
+ mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_OK, response); // 3rd attempt
return getPrometheusQueryData(prometheusEndpoint, params).then((data) => {
expect(data).toEqual(response.data);
@@ -107,7 +111,7 @@ describe('monitoring metrics_requests', () => {
it('rejects after retrying twice and getting an HTTP 401 error', () => {
// Mock multiple attempts while the cache is filling up and fails
- mock.onGet(prometheusEndpoint).reply(statusCodes.UNAUTHORIZED, {
+ mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_UNAUTHORIZED, {
status: 'error',
error: 'An error occurred',
});
@@ -134,9 +138,9 @@ describe('monitoring metrics_requests', () => {
it.each`
code | reason
- ${statusCodes.BAD_REQUEST} | ${'Parameters are missing or incorrect'}
+ ${HTTP_STATUS_BAD_REQUEST} | ${'Parameters are missing or incorrect'}
${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${"Expression can't be executed"}
- ${statusCodes.SERVICE_UNAVAILABLE} | ${'Query timed out or aborted'}
+ ${HTTP_STATUS_SERVICE_UNAVAILABLE} | ${'Query timed out or aborted'}
`('rejects with details: "$reason" after getting an HTTP $code error', ({ code, reason }) => {
mock.onGet(prometheusEndpoint).reply(code, {
status: 'error',
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index 93af6526c67..fbe030b1a7d 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -4,8 +4,10 @@ import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
-import statusCodes, {
+import {
+ HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_CREATED,
+ HTTP_STATUS_OK,
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
import { ENVIRONMENT_AVAILABLE_STATE } from '~/monitoring/constants';
@@ -983,7 +985,7 @@ describe('Monitoring store actions', () => {
});
it('Failed POST request throws an error', async () => {
- mock.onPost(state.dashboardsEndpoint).reply(statusCodes.BAD_REQUEST);
+ mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_BAD_REQUEST);
await expect(testAction(duplicateSystemDashboard, {}, state, [], [])).rejects.toEqual(
'There was an error creating the dashboard.',
@@ -994,7 +996,7 @@ describe('Monitoring store actions', () => {
it('Failed POST request throws an error with a description', async () => {
const backendErrorMsg = 'This file already exists!';
- mock.onPost(state.dashboardsEndpoint).reply(statusCodes.BAD_REQUEST, {
+ mock.onPost(state.dashboardsEndpoint).reply(HTTP_STATUS_BAD_REQUEST, {
error: backendErrorMsg,
});
@@ -1116,7 +1118,7 @@ describe('Monitoring store actions', () => {
mock
.onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
- .reply(statusCodes.OK, mockPanel);
+ .reply(HTTP_STATUS_OK, mockPanel);
testAction(
fetchPanelPreview,
diff --git a/spec/frontend/monitoring/store/mutations_spec.js b/spec/frontend/monitoring/store/mutations_spec.js
index 49e8ab9ebd4..3baef743f42 100644
--- a/spec/frontend/monitoring/store/mutations_spec.js
+++ b/spec/frontend/monitoring/store/mutations_spec.js
@@ -1,5 +1,4 @@
-import httpStatusCodes from '~/lib/utils/http_status';
-
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_SERVICE_UNAVAILABLE } from '~/lib/utils/http_status';
import { dashboardEmptyStates, metricStates } from '~/monitoring/constants';
import * as types from '~/monitoring/stores/mutation_types';
import mutations from '~/monitoring/stores/mutations';
@@ -318,7 +317,7 @@ describe('Monitoring mutations', () => {
metricId,
error: {
response: {
- status: httpStatusCodes.SERVICE_UNAVAILABLE,
+ status: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
},
});
@@ -336,7 +335,7 @@ describe('Monitoring mutations', () => {
metricId,
error: {
response: {
- status: httpStatusCodes.BAD_REQUEST,
+ status: HTTP_STATUS_BAD_REQUEST,
},
},
});
diff --git a/spec/frontend/nav/components/new_nav_toggle_spec.js b/spec/frontend/nav/components/new_nav_toggle_spec.js
index f09bdef8caa..ee75dfb70e4 100644
--- a/spec/frontend/nav/components/new_nav_toggle_spec.js
+++ b/spec/frontend/nav/components/new_nav_toggle_spec.js
@@ -14,6 +14,8 @@ jest.mock('~/flash');
const TEST_ENDPONT = 'https://example.com/toggle';
describe('NewNavToggle', () => {
+ useMockLocationHelper();
+
let wrapper;
const findToggle = () => wrapper.findComponent(GlToggle);
@@ -59,18 +61,22 @@ describe('NewNavToggle', () => {
});
});
- describe('changing the toggle', () => {
- useMockLocationHelper();
+ describe.each`
+ desc | actFn
+ ${'when toggle button is clicked'} | ${() => findToggle().trigger('click')}
+ ${'when menu item text is clicked'} | ${() => getByText('New navigation').trigger('click')}
+ `('$desc', ({ actFn }) => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
- createComponent();
+ createComponent({ enabled: false });
});
it('reloads the page on success', async () => {
mock.onPut(TEST_ENDPONT).reply(200);
- findToggle().vm.$emit('change');
+
+ actFn();
await waitForPromises();
expect(window.location.reload).toHaveBeenCalled();
@@ -78,7 +84,8 @@ describe('NewNavToggle', () => {
it('shows an alert on error', async () => {
mock.onPut(TEST_ENDPONT).reply(500);
- findToggle().vm.$emit('change');
+
+ actFn();
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith(
@@ -91,6 +98,12 @@ describe('NewNavToggle', () => {
expect(window.location.reload).not.toHaveBeenCalled();
});
+ it('changes the toggle', async () => {
+ await actFn();
+
+ expect(findToggle().props('value')).toBe(true);
+ });
+
afterEach(() => {
mock.restore();
});
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index 701ff492702..e13985ef469 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';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import Autosave from '~/autosave';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/flash';
@@ -20,6 +21,7 @@ import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock }
jest.mock('autosize');
jest.mock('~/commons/nav/user_merge_requests');
jest.mock('~/flash');
+jest.mock('~/autosave');
Vue.use(Vuex);
@@ -336,8 +338,11 @@ describe('issue_comment_form component', () => {
});
it('inits autosave', () => {
- expect(wrapper.vm.autosave).toBeDefined();
- expect(wrapper.vm.autosave.key).toBe(`autosave/Note/Issue/${noteableDataMock.id}`);
+ expect(Autosave).toHaveBeenCalledWith(expect.any(Element), [
+ 'Note',
+ 'Issue',
+ noteableDataMock.id,
+ ]);
});
});
diff --git a/spec/frontend/notes/components/note_body_spec.js b/spec/frontend/notes/components/note_body_spec.js
index 3b5313744ff..c71cf7666ab 100644
--- a/spec/frontend/notes/components/note_body_spec.js
+++ b/spec/frontend/notes/components/note_body_spec.js
@@ -7,11 +7,14 @@ import NoteAwardsList from '~/notes/components/note_awards_list.vue';
import NoteForm from '~/notes/components/note_form.vue';
import createStore from '~/notes/stores';
import notes from '~/notes/stores/modules/index';
+import Autosave from '~/autosave';
import Suggestions from '~/vue_shared/components/markdown/suggestions.vue';
import { noteableDataMock, notesDataMock, note } from '../mock_data';
+jest.mock('~/autosave');
+
const createComponent = ({
props = {},
noteableData = noteableDataMock,
@@ -84,13 +87,8 @@ describe('issue_note_body component', () => {
});
it('adds autosave', () => {
- const autosaveKey = `autosave/Note/${note.noteable_type}/${note.id}`;
-
- // While we discourage testing wrapper props
- // here we aren't testing a component prop
- // but instead an instance object property
- // which is defined in `app/assets/javascripts/notes/mixins/autosave.js`
- expect(wrapper.vm.autosave.key).toEqual(autosaveKey);
+ // passing undefined instead of an element because of shallowMount
+ expect(Autosave).toHaveBeenCalledWith(undefined, ['Note', note.noteable_type, note.id]);
});
describe('isInternalNote', () => {
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index dce2e5d370d..0b2623f3d77 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -1442,7 +1442,7 @@ describe('Actions Notes Store', () => {
return testAction(
actions.fetchDiscussions,
{},
- { noteableType: notesConstants.MERGE_REQUEST_NOTEABLE_TYPE },
+ { noteableType: notesConstants.EPIC_NOTEABLE_TYPE },
[
{ type: mutationTypes.ADD_OR_UPDATE_DISCUSSIONS, payload: { discussion } },
{ type: mutationTypes.SET_FETCHING_DISCUSSIONS, payload: false },
@@ -1472,9 +1472,7 @@ describe('Actions Notes Store', () => {
);
});
- it('dispatches `fetchDiscussionsBatch` action if `paginatedMrDiscussions` feature flag is enabled', () => {
- window.gon = { features: { paginatedMrDiscussions: true } };
-
+ it('dispatches `fetchDiscussionsBatch` action if noteable is a MergeRequest', () => {
return testAction(
actions.fetchDiscussions,
{ path: 'test-path', filter: 'test-filter', persistFilter: 'test-persist-filter' },
diff --git a/spec/frontend/notifications/components/custom_notifications_modal_spec.js b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
index cd04adac72d..70749557e61 100644
--- a/spec/frontend/notifications/components/custom_notifications_modal_spec.js
+++ b/spec/frontend/notifications/components/custom_notifications_modal_spec.js
@@ -5,7 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import CustomNotificationsModal from '~/notifications/components/custom_notifications_modal.vue';
import { i18n } from '~/notifications/constants';
@@ -138,7 +138,7 @@ describe('CustomNotificationsModal', () => {
mockAxios
.onGet(endpointUrl)
- .reply(httpStatus.OK, mockNotificationSettingsResponses.default);
+ .reply(HTTP_STATUS_OK, mockNotificationSettingsResponses.default);
wrapper = createComponent({ injectedProperties });
@@ -155,7 +155,7 @@ describe('CustomNotificationsModal', () => {
mockAxios
.onGet(endpointUrl)
- .reply(httpStatus.OK, mockNotificationSettingsResponses.default);
+ .reply(HTTP_STATUS_OK, mockNotificationSettingsResponses.default);
wrapper = createComponent();
@@ -173,7 +173,7 @@ describe('CustomNotificationsModal', () => {
});
it('shows a toast message when the request fails', async () => {
- mockAxios.onGet('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
+ mockAxios.onGet('/api/v4/notification_settings').reply(HTTP_STATUS_NOT_FOUND, {});
wrapper = createComponent();
wrapper.findComponent(GlModal).vm.$emit('show');
@@ -201,11 +201,11 @@ describe('CustomNotificationsModal', () => {
async ({ projectId, groupId, endpointUrl }) => {
mockAxios
.onGet(endpointUrl)
- .reply(httpStatus.OK, mockNotificationSettingsResponses.default);
+ .reply(HTTP_STATUS_OK, mockNotificationSettingsResponses.default);
mockAxios
.onPut(endpointUrl)
- .reply(httpStatus.OK, mockNotificationSettingsResponses.updated);
+ .reply(HTTP_STATUS_OK, mockNotificationSettingsResponses.updated);
const injectedProperties = {
projectId,
@@ -241,7 +241,7 @@ describe('CustomNotificationsModal', () => {
);
it('shows a toast message when the request fails', async () => {
- mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
+ mockAxios.onPut('/api/v4/notification_settings').reply(HTTP_STATUS_NOT_FOUND, {});
wrapper = createComponent();
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
diff --git a/spec/frontend/notifications/components/notification_email_listbox_input_spec.js b/spec/frontend/notifications/components/notification_email_listbox_input_spec.js
new file mode 100644
index 00000000000..c490c737cf1
--- /dev/null
+++ b/spec/frontend/notifications/components/notification_email_listbox_input_spec.js
@@ -0,0 +1,81 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue';
+import NotificationEmailListboxInput from '~/notifications/components/notification_email_listbox_input.vue';
+
+describe('NotificationEmailListboxInput', () => {
+ let wrapper;
+
+ // Props
+ const label = 'label';
+ const name = 'name';
+ const emails = ['test@gitlab.com'];
+ const emptyValueText = 'emptyValueText';
+ const value = 'value';
+ const disabled = false;
+
+ // Finders
+ const findListboxInput = () => wrapper.findComponent(ListboxInput);
+
+ const createComponent = (attachTo) => {
+ wrapper = shallowMount(NotificationEmailListboxInput, {
+ provide: {
+ label,
+ name,
+ emails,
+ emptyValueText,
+ value,
+ disabled,
+ },
+ attachTo,
+ });
+ };
+
+ describe('props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ propName | propValue
+ ${'label'} | ${label}
+ ${'name'} | ${name}
+ ${'selected'} | ${value}
+ ${'disabled'} | ${disabled}
+ `('passes the $propName prop to ListboxInput', ({ propName, propValue }) => {
+ expect(findListboxInput().props(propName)).toBe(propValue);
+ });
+
+ it('passes the options to ListboxInput', () => {
+ expect(findListboxInput().props('items')).toStrictEqual([
+ { text: emptyValueText, value: '' },
+ { text: emails[0], value: emails[0] },
+ ]);
+ });
+ });
+
+ describe('form', () => {
+ let form;
+
+ beforeEach(() => {
+ form = document.createElement('form');
+ const root = document.createElement('div');
+ form.appendChild(root);
+ createComponent(root);
+ });
+
+ afterEach(() => {
+ form = null;
+ });
+
+ it('submits the parent form when the value changes', async () => {
+ jest.spyOn(form, 'submit');
+ expect(form.submit).not.toHaveBeenCalled();
+
+ findListboxInput().vm.$emit('select');
+ await nextTick();
+
+ expect(form.submit).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/notifications/components/notifications_dropdown_spec.js b/spec/frontend/notifications/components/notifications_dropdown_spec.js
index 7a98b374095..0f13de0e6d8 100644
--- a/spec/frontend/notifications/components/notifications_dropdown_spec.js
+++ b/spec/frontend/notifications/components/notifications_dropdown_spec.js
@@ -4,7 +4,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import waitForPromises from 'helpers/wait_for_promises';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import CustomNotificationsModal from '~/notifications/components/custom_notifications_modal.vue';
import NotificationsDropdown from '~/notifications/components/notifications_dropdown.vue';
import NotificationsDropdownItem from '~/notifications/components/notifications_dropdown_item.vue';
@@ -98,7 +98,7 @@ describe('NotificationsDropdown', () => {
it('opens the modal when the user clicks the button', async () => {
jest.spyOn(axios, 'put');
- mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {});
+ mockAxios.onPut('/api/v4/notification_settings').reply(HTTP_STATUS_OK, {});
wrapper = createComponent({
initialNotificationLevel: 'custom',
@@ -233,7 +233,7 @@ describe('NotificationsDropdown', () => {
);
it('updates the selectedNotificationLevel and marks the item with a checkmark', async () => {
- mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {});
+ mockAxios.onPut('/api/v4/notification_settings').reply(HTTP_STATUS_OK, {});
wrapper = createComponent();
const dropdownItem = findDropdownItemAt(1);
@@ -245,7 +245,7 @@ describe('NotificationsDropdown', () => {
});
it("won't update the selectedNotificationLevel and shows a toast message when the request fails and", async () => {
- mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.NOT_FOUND, {});
+ mockAxios.onPut('/api/v4/notification_settings').reply(HTTP_STATUS_NOT_FOUND, {});
wrapper = createComponent();
await clickDropdownItemAt(1);
@@ -257,7 +257,7 @@ describe('NotificationsDropdown', () => {
});
it('opens the modal when the user clicks on the "Custom" dropdown item', async () => {
- mockAxios.onPut('/api/v4/notification_settings').reply(httpStatus.OK, {});
+ mockAxios.onPut('/api/v4/notification_settings').reply(HTTP_STATUS_OK, {});
wrapper = createComponent();
await clickDropdownItemAt(5);
diff --git a/spec/frontend/observability/observability_app_spec.js b/spec/frontend/observability/observability_app_spec.js
index 248b0a2057c..e3bcd140d60 100644
--- a/spec/frontend/observability/observability_app_spec.js
+++ b/spec/frontend/observability/observability_app_spec.js
@@ -2,11 +2,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ObservabilityApp from '~/observability/components/observability_app.vue';
import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
-import {
- MESSAGE_EVENT_TYPE,
- OBSERVABILITY_ROUTES,
- SKELETON_VARIANT,
-} from '~/observability/constants';
+import { MESSAGE_EVENT_TYPE, SKELETON_VARIANTS_BY_ROUTE } from '~/observability/constants';
import { darkModeEnabled } from '~/lib/utils/color_utils';
@@ -20,6 +16,7 @@ describe('Observability root app', () => {
};
const $route = {
pathname: 'https://gitlab.com/gitlab-org/',
+ path: 'https://gitlab.com/gitlab-org/-/observability/dashboards',
query: { otherQuery: 100 },
};
@@ -29,6 +26,10 @@ describe('Observability root app', () => {
const TEST_IFRAME_SRC = 'https://observe.gitlab.com/9970/?groupId=14485840';
+ const OBSERVABILITY_ROUTES = Object.keys(SKELETON_VARIANTS_BY_ROUTE);
+
+ const SKELETON_VARIANTS = Object.values(SKELETON_VARIANTS_BY_ROUTE);
+
const mountComponent = (route = $route) => {
wrapper = shallowMountExtended(ObservabilityApp, {
propsData: {
@@ -139,9 +140,9 @@ describe('Observability root app', () => {
describe('on GOUI_LOADED', () => {
beforeEach(() => {
mountComponent();
- wrapper.vm.$refs.iframeSkeleton.handleSkeleton = mockHandleSkeleton;
+ wrapper.vm.$refs.observabilitySkeleton.onContentLoaded = mockHandleSkeleton;
});
- it('should call handleSkeleton method', () => {
+ it('should call onContentLoaded method', () => {
dispatchMessageEvent({
data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
origin: 'https://observe.gitlab.com',
@@ -149,7 +150,7 @@ describe('Observability root app', () => {
expect(mockHandleSkeleton).toHaveBeenCalled();
});
- it('should not call handleSkeleton method if origin is different', () => {
+ it('should not call onContentLoaded method if origin is different', () => {
dispatchMessageEvent({
data: { type: MESSAGE_EVENT_TYPE.GOUI_LOADED },
origin: 'https://example.com',
@@ -157,7 +158,7 @@ describe('Observability root app', () => {
expect(mockHandleSkeleton).not.toHaveBeenCalled();
});
- it('should not call handleSkeleton method if event type is different', () => {
+ it('should not call onContentLoaded method if event type is different', () => {
dispatchMessageEvent({
data: { type: 'UNKNOWN_EVENT' },
origin: 'https://observe.gitlab.com',
@@ -168,11 +169,11 @@ describe('Observability root app', () => {
describe('skeleton variant', () => {
it.each`
- pathDescription | path | variant
- ${'dashboards'} | ${OBSERVABILITY_ROUTES.DASHBOARDS} | ${SKELETON_VARIANT.DASHBOARDS}
- ${'explore'} | ${OBSERVABILITY_ROUTES.EXPLORE} | ${SKELETON_VARIANT.EXPLORE}
- ${'manage dashboards'} | ${OBSERVABILITY_ROUTES.MANAGE} | ${SKELETON_VARIANT.MANAGE}
- ${'any other'} | ${'unknown/route'} | ${SKELETON_VARIANT.DASHBOARDS}
+ 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', ({ path, variant }) => {
mountComponent({ ...$route, path });
const props = wrapper.findComponent(ObservabilitySkeleton).props();
diff --git a/spec/frontend/observability/skeleton_spec.js b/spec/frontend/observability/skeleton_spec.js
index 5637c0e6d70..a95597d8516 100644
--- a/spec/frontend/observability/skeleton_spec.js
+++ b/spec/frontend/observability/skeleton_spec.js
@@ -1,96 +1,127 @@
-import { GlSkeletonLoader } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { GlSkeletonLoader, GlAlert } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
-import ObservabilitySkeleton from '~/observability/components/skeleton/index.vue';
+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 { SKELETON_VARIANT } from '~/observability/constants';
+import { SKELETON_VARIANTS_BY_ROUTE, DEFAULT_TIMERS } from '~/observability/constants';
-describe('ObservabilitySkeleton component', () => {
+describe('Skeleton component', () => {
let wrapper;
+ const SKELETON_VARIANTS = Object.values(SKELETON_VARIANTS_BY_ROUTE);
+
+ const findContentWrapper = () => wrapper.findByTestId('observability-wrapper');
+
+ const findExploreSkeleton = () => wrapper.findComponent(ExploreSkeleton);
+
+ const findDashboardsSkeleton = () => wrapper.findComponent(DashboardsSkeleton);
+
+ const findManageSkeleton = () => wrapper.findComponent(ManageSkeleton);
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
const mountComponent = ({ ...props } = {}) => {
- wrapper = shallowMountExtended(ObservabilitySkeleton, {
+ wrapper = shallowMountExtended(Skeleton, {
propsData: props,
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('on mount', () => {
beforeEach(() => {
- jest.spyOn(global, 'setTimeout');
- mountComponent();
+ mountComponent({ variant: 'explore' });
});
- it('should call setTimeout on mount and show ObservabilitySkeleton if Observability UI is not loaded yet', () => {
- jest.runAllTimers();
+ describe('loading timers', () => {
+ it('show Skeleton if content is not loaded within CONTENT_WAIT_MS', async () => {
+ expect(findExploreSkeleton().exists()).toBe(false);
+ expect(findContentWrapper().isVisible()).toBe(false);
- expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 500);
- expect(wrapper.vm.loading).toBe(true);
- expect(wrapper.vm.timerId).not.toBeNull();
- });
+ jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
- it('should call setTimeout on mount and dont show ObservabilitySkeleton if Observability UI is loaded', () => {
- wrapper.vm.loading = false;
- jest.runAllTimers();
+ await nextTick();
- expect(setTimeout).toHaveBeenCalledWith(expect.any(Function), 500);
- expect(wrapper.vm.loading).toBe(false);
- expect(wrapper.vm.timerId).not.toBeNull();
- });
- });
+ expect(findExploreSkeleton().exists()).toBe(true);
+ expect(findContentWrapper().isVisible()).toBe(false);
+ });
- describe('handleSkeleton', () => {
- it('will not show the skeleton if Observability UI is loaded before', () => {
- jest.spyOn(global, 'clearTimeout');
- mountComponent();
- wrapper.vm.handleSkeleton();
- expect(clearTimeout).toHaveBeenCalledWith(wrapper.vm.timerId);
+ it('does not show the skeleton if content has loaded within CONTENT_WAIT_MS', async () => {
+ expect(findExploreSkeleton().exists()).toBe(false);
+ expect(findContentWrapper().isVisible()).toBe(false);
+
+ wrapper.vm.onContentLoaded();
+
+ await nextTick();
+
+ expect(findContentWrapper().isVisible()).toBe(true);
+ expect(findExploreSkeleton().exists()).toBe(false);
+
+ jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
+
+ await nextTick();
+
+ expect(findContentWrapper().isVisible()).toBe(true);
+ expect(findExploreSkeleton().exists()).toBe(false);
+ });
});
- it('will hide skeleton gracefully after 400ms if skeleton was present on screen before Observability UI', () => {
- jest.spyOn(global, 'setTimeout');
- mountComponent();
- jest.runAllTimers();
- wrapper.vm.handleSkeleton();
- jest.runAllTimers();
+ describe('error timeout', () => {
+ it('shows the error dialog if content has not loaded within TIMEOUT_MS', async () => {
+ expect(findAlert().exists()).toBe(false);
+ jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
+
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findContentWrapper().isVisible()).toBe(false);
+ });
+
+ it('does not show the error dialog if content has loaded within TIMEOUT_MS', async () => {
+ wrapper.vm.onContentLoaded();
+ jest.advanceTimersByTime(DEFAULT_TIMERS.TIMEOUT_MS);
- expect(setTimeout).toHaveBeenCalledWith(wrapper.vm.hideSkeleton, 400);
- expect(wrapper.vm.loading).toBe(false);
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(false);
+ expect(findContentWrapper().isVisible()).toBe(true);
+ });
});
});
describe('skeleton variant', () => {
it.each`
skeletonType | condition | variant
- ${'dashboards'} | ${'variant is dashboards'} | ${SKELETON_VARIANT.DASHBOARDS}
- ${'explore'} | ${'variant is explore'} | ${SKELETON_VARIANT.EXPLORE}
- ${'manage'} | ${'variant is manage'} | ${SKELETON_VARIANT.MANAGE}
+ ${'dashboards'} | ${'variant is dashboards'} | ${SKELETON_VARIANTS[0]}
+ ${'explore'} | ${'variant is explore'} | ${SKELETON_VARIANTS[1]}
+ ${'manage'} | ${'variant is manage'} | ${SKELETON_VARIANTS[2]}
${'default'} | ${'variant is not manage, dashboards or explore'} | ${'unknown'}
`('should render $skeletonType skeleton if $condition', async ({ skeletonType, variant }) => {
mountComponent({ variant });
- const showsDefaultSkeleton = ![
- SKELETON_VARIANT.DASHBOARDS,
- SKELETON_VARIANT.EXPLORE,
- SKELETON_VARIANT.MANAGE,
- ].includes(variant);
- expect(wrapper.findComponent(DashboardsSkeleton).exists()).toBe(
- skeletonType === SKELETON_VARIANT.DASHBOARDS,
- );
- expect(wrapper.findComponent(ExploreSkeleton).exists()).toBe(
- skeletonType === SKELETON_VARIANT.EXPLORE,
- );
- expect(wrapper.findComponent(ManageSkeleton).exists()).toBe(
- skeletonType === SKELETON_VARIANT.MANAGE,
- );
+ jest.advanceTimersByTime(DEFAULT_TIMERS.CONTENT_WAIT_MS);
+ await nextTick();
+ const showsDefaultSkeleton = !SKELETON_VARIANTS.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(wrapper.findComponent(GlSkeletonLoader).exists()).toBe(showsDefaultSkeleton);
});
});
+
+ describe('on destroy', () => {
+ it('should clear init timer and timeout timer', () => {
+ jest.spyOn(global, 'clearTimeout');
+ mountComponent();
+ wrapper.destroy();
+ expect(clearTimeout).toHaveBeenCalledTimes(2);
+ expect(clearTimeout.mock.calls).toEqual([
+ [wrapper.vm.loadingTimeout], // First call
+ [wrapper.vm.errorTimeout], // Second call
+ ]);
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
index 96c670eaad2..fa0d76762df 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/details_page/tags_list_row_spec.js
@@ -335,10 +335,10 @@ describe('tags list row', () => {
});
describe.each`
- name | finderFunction | text | icon | clipboard
- ${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the gitlab-org/gitlab-test/rails-12009 image repository at 01:29 UTC on 2020-11-03'} | ${'clock'} | ${false}
- ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:2cf3d2fdac1b04a14301d47d51cb88dcd26714c74f91440eeee99ce399089062'} | ${'log'} | ${true}
- ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:c2613843ab33aabf847965442b13a8b55a56ae28837ce182627c0716eb08c02b'} | ${'cloud-gear'} | ${true}
+ name | finderFunction | text | icon | clipboard
+ ${'published date detail'} | ${findPublishedDateDetail} | ${'Published to the gitlab-org/gitlab-test/rails-12009 image repository at 13:29:38 UTC on 2020-11-03'} | ${'clock'} | ${false}
+ ${'manifest detail'} | ${findManifestDetail} | ${'Manifest digest: sha256:2cf3d2fdac1b04a14301d47d51cb88dcd26714c74f91440eeee99ce399089062'} | ${'log'} | ${true}
+ ${'configuration detail'} | ${findConfigurationDetail} | ${'Configuration digest: sha256:c2613843ab33aabf847965442b13a8b55a56ae28837ce182627c0716eb08c02b'} | ${'cloud-gear'} | ${true}
`('$name details row', ({ finderFunction, text, icon, clipboard }) => {
it(`has ${text} as text`, async () => {
mountComponent();
diff --git a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
index 7da91c4af96..75068591007 100644
--- a/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/container_registry/explorer/components/list_page/image_list_row_spec.js
@@ -1,6 +1,6 @@
import { GlIcon, GlSprintf, GlSkeletonLoader, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { createMockDirective } from 'helpers/vue_mock_directive';
import { mockTracking } from 'helpers/tracking_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import DeleteButton from '~/packages_and_registries/container_registry/explorer/components/delete_button.vue';
@@ -59,31 +59,6 @@ describe('Image List Row', () => {
wrapper = null;
});
- describe('list item component', () => {
- describe('tooltip', () => {
- it(`the title is ${ROW_SCHEDULED_FOR_DELETION}`, () => {
- mountComponent();
-
- const tooltip = getBinding(wrapper.element, 'gl-tooltip');
- expect(tooltip).toBeDefined();
- expect(tooltip.value.title).toBe(ROW_SCHEDULED_FOR_DELETION);
- });
-
- it('is disabled when item is being deleted', () => {
- mountComponent({ item: { ...item, status: IMAGE_DELETE_SCHEDULED_STATUS } });
-
- const tooltip = getBinding(wrapper.element, 'gl-tooltip');
- expect(tooltip.value.disabled).toBe(false);
- });
- });
-
- it('is disabled when the item is in deleting status', () => {
- mountComponent({ item: { ...item, status: IMAGE_DELETE_SCHEDULED_STATUS } });
-
- expect(findListItemComponent().props('disabled')).toBe(true);
- });
- });
-
describe('image title and path', () => {
it('renders shortened name of image and contains a link to the details page', () => {
mountComponent();
@@ -158,10 +133,22 @@ describe('Image List Row', () => {
mountComponent({ item: { ...item, status: IMAGE_DELETE_SCHEDULED_STATUS } });
});
- it('the router link is disabled', () => {
- // we check the event prop as is the only workaround to disable a router link
- expect(findDetailsLink().props('event')).toBe('');
+ it('the router link does not exist', () => {
+ expect(findDetailsLink().exists()).toBe(false);
+ });
+
+ it('image name exists', () => {
+ expect(findListItemComponent().text()).toContain('gitlab-test/rails-12009');
+ });
+
+ it(`contains secondary text ${ROW_SCHEDULED_FOR_DELETION}`, () => {
+ expect(findListItemComponent().text()).toContain(ROW_SCHEDULED_FOR_DELETION);
});
+
+ it('the tags count does not exist', () => {
+ expect(findTagsCount().exists()).toBe(false);
+ });
+
it('the clipboard button is disabled', () => {
expect(findClipboardButton().attributes('disabled')).toBe('true');
});
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
index c6b5138639e..0cbe2755f7e 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/details/components/package_history_spec.js
@@ -61,14 +61,14 @@ describe('Package History', () => {
);
});
describe.each`
- name | amount | icon | text | timeAgoTooltip | link
- ${'created-on'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'clock'} | ${'Test package version 1.0.0 was first created'} | ${mavenPackage.created_at} | ${null}
- ${'first-pipeline-commit'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'commit'} | ${'Created by commit #sha-baz on branch branch-name'} | ${null} | ${mockPipelineInfo.project.commit_url}
- ${'first-pipeline-pipeline'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'pipeline'} | ${'Built by pipeline #1 triggered by foo'} | ${mockPipelineInfo.created_at} | ${mockPipelineInfo.project.pipeline_url}
- ${'published'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'package'} | ${'Published to the baz project Package Registry'} | ${mavenPackage.created_at} | ${null}
- ${'archived'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'history'} | ${'Package has 1 archived update'} | ${null} | ${null}
- ${'archived'} | ${HISTORY_PIPELINES_LIMIT + 3} | ${'history'} | ${'Package has 2 archived updates'} | ${null} | ${null}
- ${'pipeline-entry'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'pencil'} | ${'Package updated by commit #sha-baz on branch branch-name, built by pipeline #3, and published to the registry'} | ${mavenPackage.created_at} | ${mockPipelineInfo.project.commit_url}
+ name | amount | icon | text | timeAgoTooltip | link
+ ${'created-on'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'clock'} | ${'Test package version 1.0.0 was first created'} | ${mavenPackage.created_at} | ${null}
+ ${'first-pipeline-commit'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'commit'} | ${'Created by commit sha-baz on branch branch-name'} | ${null} | ${mockPipelineInfo.project.commit_url}
+ ${'first-pipeline-pipeline'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'pipeline'} | ${'Built by pipeline #1 triggered by foo'} | ${mockPipelineInfo.created_at} | ${mockPipelineInfo.project.pipeline_url}
+ ${'published'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'package'} | ${'Published to the baz project Package Registry'} | ${mavenPackage.created_at} | ${null}
+ ${'archived'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'history'} | ${'Package has 1 archived update'} | ${null} | ${null}
+ ${'archived'} | ${HISTORY_PIPELINES_LIMIT + 3} | ${'history'} | ${'Package has 2 archived updates'} | ${null} | ${null}
+ ${'pipeline-entry'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'pencil'} | ${'Package updated by commit sha-baz on branch branch-name, built by pipeline #3, and published to the registry'} | ${mavenPackage.created_at} | ${mockPipelineInfo.project.commit_url}
`(
'with $amount pipelines history element $name',
({ name, icon, text, timeAgoTooltip, link, amount }) => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
index e0e26434680..9c1ebf5a2eb 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/delete_modal_spec.js
@@ -63,6 +63,14 @@ describe('DeleteModal', () => {
expect(wrapper.emitted('confirm')).toHaveLength(1);
});
+ it('emits cancel when cancel event is emitted', () => {
+ expect(wrapper.emitted('cancel')).toBeUndefined();
+
+ findModal().vm.$emit('cancel');
+
+ expect(wrapper.emitted('cancel')).toHaveLength(1);
+ });
+
it('show calls gl-modal show', () => {
findModal().vm.show();
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 c4020eeb75f..b2375da7b11 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
@@ -114,7 +114,7 @@ exports[`PypiInstallation renders all the messages 1`] = `
aria-live="polite"
class="btn input-group-text btn-default btn-md gl-button btn-default-secondary btn-icon"
data-clipboard-handle-tooltip="false"
- data-clipboard-text="pip install @gitlab-org/package-15 --extra-index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple"
+ data-clipboard-text="pip install @gitlab-org/package-15 --index-url http://__token__:<your_personal_token>@gdk.test:3000/api/v4/projects/1/packages/pypi/simple"
id="clipboard-button-6"
title="Copy Pip command"
type="button"
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
index ec2e833552a..bb2fa9eb6f5 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_history_spec.js
@@ -131,14 +131,14 @@ describe('Package History', () => {
});
describe.each`
- name | amount | icon | text | timeAgoTooltip | link
- ${'created-on'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'clock'} | ${'@gitlab-org/package-15 version 1.0.0 was first created'} | ${packageData().createdAt} | ${null}
- ${'first-pipeline-commit'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'commit'} | ${'Created by commit #b83d6e39 on branch master'} | ${null} | ${onePipeline.commitPath}
- ${'first-pipeline-pipeline'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'pipeline'} | ${'Built by pipeline #1 triggered by Administrator'} | ${onePipeline.createdAt} | ${onePipeline.path}
- ${'published'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'package'} | ${'Published to the baz project Package Registry'} | ${packageData().createdAt} | ${null}
- ${'archived'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'history'} | ${'Package has 1 archived update'} | ${null} | ${null}
- ${'archived'} | ${HISTORY_PIPELINES_LIMIT + 3} | ${'history'} | ${'Package has 2 archived updates'} | ${null} | ${null}
- ${'pipeline-entry'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'pencil'} | ${'Package updated by commit #b83d6e39 on branch master, built by pipeline #3, and published to the registry'} | ${packageData().createdAt} | ${onePipeline.commitPath}
+ name | amount | icon | text | timeAgoTooltip | link
+ ${'created-on'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'clock'} | ${'@gitlab-org/package-15 version 1.0.0 was first created'} | ${packageData().createdAt} | ${null}
+ ${'first-pipeline-commit'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'commit'} | ${'Created by commit b83d6e39 on branch master'} | ${null} | ${onePipeline.commitPath}
+ ${'first-pipeline-pipeline'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'pipeline'} | ${'Built by pipeline #1 triggered by Administrator'} | ${onePipeline.createdAt} | ${onePipeline.path}
+ ${'published'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'package'} | ${'Published to the baz project Package Registry'} | ${packageData().createdAt} | ${null}
+ ${'archived'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'history'} | ${'Package has 1 archived update'} | ${null} | ${null}
+ ${'archived'} | ${HISTORY_PIPELINES_LIMIT + 3} | ${'history'} | ${'Package has 2 archived updates'} | ${null} | ${null}
+ ${'pipeline-entry'} | ${HISTORY_PIPELINES_LIMIT + 2} | ${'pencil'} | ${'Package updated by commit b83d6e39 on branch master, built by pipeline #3, and published to the registry'} | ${packageData().createdAt} | ${onePipeline.commitPath}
`(
'with $amount pipelines history element $name',
({ name, icon, text, timeAgoTooltip, link, amount }) => {
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
index f0fa9592419..20a459e2c1a 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js
@@ -1,7 +1,7 @@
-import { GlKeysetPagination } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
+import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
import { packageData } from '../../mock_data';
@@ -21,7 +21,7 @@ describe('PackageVersionsList', () => {
const uiElements = {
findLoader: () => wrapper.findComponent(PackagesListLoader),
- findListPagination: () => wrapper.findComponent(GlKeysetPagination),
+ findRegistryList: () => wrapper.findComponent(RegistryList),
findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
findListRow: () => wrapper.findAllComponents(VersionRow),
};
@@ -33,6 +33,9 @@ describe('PackageVersionsList', () => {
isLoading: false,
...props,
},
+ stubs: {
+ RegistryList,
+ },
slots: {
'empty-state': EmptySlotStub,
},
@@ -55,8 +58,8 @@ describe('PackageVersionsList', () => {
expect(uiElements.findEmptySlot().exists()).toBe(false);
});
- it('does not display pagination', () => {
- expect(uiElements.findListPagination().exists()).toBe(false);
+ it('does not display registry list', () => {
+ expect(uiElements.findRegistryList().exists()).toBe(false);
});
});
@@ -77,8 +80,8 @@ describe('PackageVersionsList', () => {
expect(uiElements.findListRow().exists()).toBe(false);
});
- it('does not display pagination', () => {
- expect(uiElements.findListPagination().exists()).toBe(false);
+ it('does not display registry list', () => {
+ expect(uiElements.findRegistryList().exists()).toBe(false);
});
});
@@ -87,6 +90,19 @@ describe('PackageVersionsList', () => {
mountComponent();
});
+ it('displays package registry list', () => {
+ expect(uiElements.findRegistryList().exists()).toEqual(true);
+ });
+
+ it('binds the right props', () => {
+ expect(uiElements.findRegistryList().props()).toMatchObject({
+ items: packageList,
+ pagination: {},
+ isLoading: false,
+ hiddenDelete: true,
+ });
+ });
+
it('displays package version rows', () => {
expect(uiElements.findListRow().exists()).toEqual(true);
expect(uiElements.findListRow()).toHaveLength(packageList.length);
@@ -102,27 +118,6 @@ describe('PackageVersionsList', () => {
});
});
- describe('pagination display', () => {
- it('does not display pagination if there is no previous or next page', () => {
- expect(uiElements.findListPagination().exists()).toBe(false);
- });
-
- it('displays pagination if pageInfo.hasNextPage is true', async () => {
- await wrapper.setProps({ pageInfo: { hasNextPage: true } });
- expect(uiElements.findListPagination().exists()).toBe(true);
- });
-
- it('displays pagination if pageInfo.hasPreviousPage is true', async () => {
- await wrapper.setProps({ pageInfo: { hasPreviousPage: true } });
- expect(uiElements.findListPagination().exists()).toBe(true);
- });
-
- it('displays pagination if both pageInfo.hasNextPage and pageInfo.hasPreviousPage are true', async () => {
- await wrapper.setProps({ pageInfo: { hasNextPage: true, hasPreviousPage: true } });
- expect(uiElements.findListPagination().exists()).toBe(true);
- });
- });
-
it('does not display loader', () => {
expect(uiElements.findLoader().exists()).toBe(false);
});
@@ -137,14 +132,14 @@ describe('PackageVersionsList', () => {
mountComponent({ pageInfo: { hasNextPage: true } });
});
- it('emits prev-page event when paginator emits prev event', () => {
- uiElements.findListPagination().vm.$emit('prev');
+ it('emits prev-page event when registry list emits prev event', () => {
+ uiElements.findRegistryList().vm.$emit('prev-page');
expect(wrapper.emitted('prev-page')).toHaveLength(1);
});
- it('emits next-page when paginator emits next event', () => {
- uiElements.findListPagination().vm.$emit('next');
+ it('emits next-page when registry list emits next event', () => {
+ uiElements.findRegistryList().vm.$emit('next-page');
expect(wrapper.emitted('next-page')).toHaveLength(1);
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
index 20acb0872e5..4a27f8011df 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/pypi_installation_spec.js
@@ -16,7 +16,7 @@ const packageEntity = { ...packageData(), packageType: PACKAGE_TYPE_PYPI };
describe('PypiInstallation', () => {
let wrapper;
- const pipCommandStr = `pip install @gitlab-org/package-15 --extra-index-url ${packageEntity.pypiUrl}`;
+ const pipCommandStr = `pip install @gitlab-org/package-15 --index-url ${packageEntity.pypiUrl}`;
const pypiSetupStr = `[gitlab]
repository = ${packageEntity.pypiSetupUrl}
username = __token__
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
index 7cc5bea0f7a..5e9cb8fbb0b 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js
@@ -1,14 +1,19 @@
import { GlAlert, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
import PackagesListRow from '~/packages_and_registries/package_registry/components/list/package_list_row.vue';
import PackagesListLoader from '~/packages_and_registries/shared/components/packages_list_loader.vue';
import DeletePackageModal from '~/packages_and_registries/shared/components/delete_package_modal.vue';
+import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import RegistryList from '~/packages_and_registries/shared/components/registry_list.vue';
import {
DELETE_PACKAGE_TRACKING_ACTION,
+ DELETE_PACKAGES_TRACKING_ACTION,
REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGES_TRACKING_ACTION,
CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
+ CANCEL_DELETE_PACKAGES_TRACKING_ACTION,
} from '~/packages_and_registries/package_registry/constants';
import PackagesList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import Tracking from '~/tracking';
@@ -44,6 +49,7 @@ describe('packages_list', () => {
const findRegistryList = () => wrapper.findComponent(RegistryList);
const findPackagesListRow = () => wrapper.findComponent(PackagesListRow);
const findErrorPackageAlert = () => wrapper.findComponent(GlAlert);
+ const findDeletePackagesModal = () => wrapper.findComponent(DeleteModal);
const mountComponent = (props) => {
wrapper = shallowMountExtended(PackagesList, {
@@ -53,6 +59,11 @@ describe('packages_list', () => {
},
stubs: {
DeletePackageModal,
+ DeleteModal: stubComponent(DeleteModal, {
+ methods: {
+ show: jest.fn(),
+ },
+ }),
GlSprintf,
RegistryList,
},
@@ -125,20 +136,48 @@ describe('packages_list', () => {
});
});
- describe('when the user can destroy the package', () => {
- beforeEach(async () => {
+ describe.each`
+ description | finderFunction | deletePayload
+ ${'when the user can destroy the package'} | ${findPackagesListRow} | ${firstPackage}
+ ${'when the user can bulk destroy packages and deletes only one package'} | ${findRegistryList} | ${[firstPackage]}
+ `('$description', ({ finderFunction, deletePayload }) => {
+ let eventSpy;
+ const category = 'UI::NpmPackages';
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
mountComponent();
- await findPackagesListRow().vm.$emit('delete', firstPackage);
+ finderFunction().vm.$emit('delete', deletePayload);
});
it('passes itemToBeDeleted to the modal', () => {
expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(firstPackage);
});
- it('emits package:delete when modal confirms', async () => {
- await findPackageListDeleteModal().vm.$emit('ok');
+ it('requesting delete tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+
+ describe('when modal confirms', () => {
+ beforeEach(() => {
+ findPackageListDeleteModal().vm.$emit('ok');
+ });
+
+ it('emits package:delete when modal confirms', () => {
+ expect(wrapper.emitted('package:delete')[0]).toEqual([firstPackage]);
+ });
- expect(wrapper.emitted('package:delete')[0]).toEqual([firstPackage]);
+ it('tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ DELETE_PACKAGE_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
});
it.each(['ok', 'cancel'])('resets itemToBeDeleted when modal emits %s', async (event) => {
@@ -146,26 +185,73 @@ describe('packages_list', () => {
expect(findPackageListDeleteModal().props('itemToBeDeleted')).toBeNull();
});
+
+ it('canceling delete tracks the right action', () => {
+ findPackageListDeleteModal().vm.$emit('cancel');
+
+ expect(eventSpy).toHaveBeenCalledWith(
+ category,
+ CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
});
describe('when the user can bulk destroy packages', () => {
+ let eventSpy;
+ const items = [firstPackage, secondPackage];
+
beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
mountComponent();
+ findRegistryList().vm.$emit('delete', items);
});
- it('passes itemToBeDeleted to the modal when there is only one package', async () => {
- await findRegistryList().vm.$emit('delete', [firstPackage]);
-
- expect(findPackageListDeleteModal().props('itemToBeDeleted')).toStrictEqual(firstPackage);
+ it('passes itemsToBeDeleted to the modal', () => {
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual(items);
expect(wrapper.emitted('delete')).toBeUndefined();
});
- it('emits delete when there is more than one package', () => {
- const items = [firstPackage, secondPackage];
- findRegistryList().vm.$emit('delete', items);
+ it('requesting delete tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ undefined,
+ REQUEST_DELETE_PACKAGES_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+
+ describe('when modal confirms', () => {
+ beforeEach(() => {
+ findDeletePackagesModal().vm.$emit('confirm');
+ });
+
+ it('emits delete event', () => {
+ expect(wrapper.emitted('delete')[0]).toEqual([items]);
+ });
+
+ it('tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ undefined,
+ DELETE_PACKAGES_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+ });
+
+ it.each(['confirm', 'cancel'])('resets itemsToBeDeleted when modal emits %s', async (event) => {
+ await findDeletePackagesModal().vm.$emit(event);
- expect(wrapper.emitted('delete')).toHaveLength(1);
- expect(wrapper.emitted('delete')[0]).toEqual([items]);
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toHaveLength(0);
+ });
+
+ it('canceling delete tracks the right action', () => {
+ findDeletePackagesModal().vm.$emit('cancel');
+
+ expect(eventSpy).toHaveBeenCalledWith(
+ undefined,
+ CANCEL_DELETE_PACKAGES_TRACKING_ACTION,
+ expect.any(Object),
+ );
});
});
@@ -223,44 +309,4 @@ describe('packages_list', () => {
expect(wrapper.emitted('next-page')).toHaveLength(1);
});
});
-
- describe('tracking', () => {
- let eventSpy;
- const category = 'UI::NpmPackages';
-
- beforeEach(() => {
- eventSpy = jest.spyOn(Tracking, 'event');
- mountComponent();
- findPackagesListRow().vm.$emit('delete', firstPackage);
- return nextTick();
- });
-
- it('requesting the delete tracks the right action', () => {
- expect(eventSpy).toHaveBeenCalledWith(
- category,
- REQUEST_DELETE_PACKAGE_TRACKING_ACTION,
- expect.any(Object),
- );
- });
-
- it('confirming delete tracks the right action', () => {
- findPackageListDeleteModal().vm.$emit('ok');
-
- expect(eventSpy).toHaveBeenCalledWith(
- category,
- DELETE_PACKAGE_TRACKING_ACTION,
- expect.any(Object),
- );
- });
-
- it('canceling delete tracks the right action', () => {
- findPackageListDeleteModal().vm.$emit('cancel');
-
- expect(eventSpy).toHaveBeenCalledWith(
- category,
- CANCEL_DELETE_PACKAGE_TRACKING_ACTION,
- expect.any(Object),
- );
- });
- });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
deleted file mode 100644
index c2fecf87428..00000000000
--- a/spec/frontend/packages_and_registries/package_registry/pages/__snapshots__/list_spec.js.snap
+++ /dev/null
@@ -1,125 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`PackagesListApp renders 1`] = `
-<div>
- <!---->
-
- <gl-card-stub
- bodyclass="gl-display-flex gl-p-0!"
- class="gl-px-8 gl-py-6 gl-line-height-20 gl-mt-3"
- footerclass=""
- headerclass=""
- >
- <!---->
-
- <div
- class="gl-banner-content"
- >
- <h2
- class="gl-banner-title"
- >
- Help us learn about your registry migration needs
- </h2>
-
- <p>
- If you are interested in migrating packages from your private registry to the GitLab Package Registry, take our survey and tell us more about your needs.
- </p>
-
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- data-testid="gl-banner-primary-button"
- href="https://gitlab.fra1.qualtrics.com/jfe/form/SV_cHomH9FPzOaiDTU"
- icon=""
- size="medium"
- variant="confirm"
- >
- Take survey
- </gl-button-stub>
-
- </div>
-
- <gl-button-stub
- aria-label="Close banner"
- buttontextclasses=""
- category="tertiary"
- class="gl-banner-close"
- icon="close"
- size="small"
- variant="default"
- />
- </gl-card-stub>
-
- <package-title-stub
- count="2"
- helpurl="/help/user/packages/index"
- />
-
- <package-search-stub
- class="gl-mb-5"
- />
-
- <div>
- <section
- class="gl-display-flex empty-state gl-text-center gl-flex-direction-column"
- >
- <div
- class="gl-max-w-full"
- >
- <div
- class="svg-250 svg-content"
- >
- <img
- alt=""
- class="gl-max-w-full gl-dark-invert-keep-hue"
- role="img"
- src="emptyListIllustration"
- />
- </div>
- </div>
-
- <div
- class="gl-max-w-full gl-m-auto"
- >
- <div
- class="gl-mx-auto gl-my-0 gl-p-5"
- >
- <h1
- class="gl-font-size-h-display gl-line-height-36 h4"
- >
-
- There are no packages yet
-
- </h1>
-
- <p
- class="gl-mt-3"
- >
- Learn how to
- <b-link-stub
- class="gl-link"
- event="click"
- href="/help/user/packages/package_registry/index"
- routertag="a"
- 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>
- </div>
- </div>
- </section>
- </div>
-
- <div />
-</div>
-`;
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
index abdb875e839..b3cbd9f5dcf 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/list_spec.js
@@ -1,23 +1,18 @@
-import { GlAlert, GlBanner, GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlAlert, GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
-
import VueApollo from 'vue-apollo';
-import * as utils from '~/lib/utils/common_utils';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { stubComponent } from 'helpers/stub_component';
import ListPage from '~/packages_and_registries/package_registry/pages/list.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/list/package_title.vue';
import PackageSearch from '~/packages_and_registries/package_registry/components/list/package_search.vue';
import OriginalPackageList from '~/packages_and_registries/package_registry/components/list/packages_list.vue';
import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
-import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
GRAPHQL_PAGE_SIZE,
- HIDE_PACKAGE_MIGRATION_SURVEY_COOKIE,
EMPTY_LIST_HELP_URL,
PACKAGE_HELP_URL,
DELETE_PACKAGES_ERROR_MESSAGE,
@@ -59,13 +54,11 @@ describe('PackagesListApp', () => {
};
const findAlert = () => wrapper.findComponent(GlAlert);
- const findBanner = () => wrapper.findComponent(GlBanner);
const findPackageTitle = () => wrapper.findComponent(PackageTitle);
const findSearch = () => wrapper.findComponent(PackageSearch);
const findListComponent = () => wrapper.findComponent(PackageList);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findDeletePackage = () => wrapper.findComponent(DeletePackage);
- const findDeletePackagesModal = () => wrapper.findComponent(DeleteModal);
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
@@ -84,18 +77,12 @@ describe('PackagesListApp', () => {
apolloProvider,
provide,
stubs: {
- GlBanner,
GlEmptyState,
GlLoadingIcon,
GlSprintf,
GlLink,
PackageList,
DeletePackage,
- DeleteModal: stubComponent(DeleteModal, {
- methods: {
- show: jest.fn(),
- },
- }),
},
});
};
@@ -118,14 +105,6 @@ describe('PackagesListApp', () => {
expect(resolver).not.toHaveBeenCalled();
});
- it('renders', async () => {
- mountComponent();
-
- await waitForFirstRequest();
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
it('has a package title', async () => {
mountComponent();
@@ -138,70 +117,6 @@ describe('PackagesListApp', () => {
});
});
- describe('package migration survey banner', () => {
- describe('with no cookie set', () => {
- beforeEach(() => {
- utils.setCookie = jest.fn();
-
- mountComponent();
- });
-
- it('displays the banner', () => {
- expect(findBanner().exists()).toBe(true);
- });
-
- it('does not call setCookie', () => {
- expect(utils.setCookie).not.toHaveBeenCalled();
- });
-
- describe('when the close button is clicked', () => {
- beforeEach(() => {
- findBanner().vm.$emit('close');
- });
-
- it('sets the dismissed cookie', () => {
- expect(utils.setCookie).toHaveBeenCalledWith(
- HIDE_PACKAGE_MIGRATION_SURVEY_COOKIE,
- 'true',
- );
- });
-
- it('does not display the banner', () => {
- expect(findBanner().exists()).toBe(false);
- });
- });
-
- describe('when the primary button is clicked', () => {
- beforeEach(() => {
- findBanner().vm.$emit('primary');
- });
-
- it('sets the dismissed cookie', () => {
- expect(utils.setCookie).toHaveBeenCalledWith(
- HIDE_PACKAGE_MIGRATION_SURVEY_COOKIE,
- 'true',
- );
- });
-
- it('does not display the banner', () => {
- expect(findBanner().exists()).toBe(false);
- });
- });
- });
-
- describe('with the dismissed cookie set', () => {
- beforeEach(() => {
- jest.spyOn(utils, 'getCookie').mockReturnValue('true');
-
- mountComponent();
- });
-
- it('does not display the banner', () => {
- expect(findBanner().exists()).toBe(false);
- });
- });
- });
-
describe('search component', () => {
it('exists', () => {
mountComponent();
@@ -372,18 +287,6 @@ describe('PackagesListApp', () => {
describe('bulk delete package', () => {
const items = [{ id: '1' }, { id: '2' }];
- it('deletePackage is bound to package-list package:delete event', async () => {
- mountComponent();
-
- await waitForFirstRequest();
-
- findListComponent().vm.$emit('delete', [{ id: '1' }, { id: '2' }]);
-
- await waitForPromises();
-
- expect(findDeletePackagesModal().props('itemsToBeDeleted')).toEqual(items);
- });
-
it('calls mutation with the right values and shows success alert', async () => {
const mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutation());
mountComponent({
@@ -394,8 +297,6 @@ describe('PackagesListApp', () => {
findListComponent().vm.$emit('delete', items);
- findDeletePackagesModal().vm.$emit('confirm');
-
expect(mutationResolver).toHaveBeenCalledWith({
ids: items.map((item) => item.id),
});
@@ -417,8 +318,6 @@ describe('PackagesListApp', () => {
findListComponent().vm.$emit('delete', items);
- findDeletePackagesModal().vm.$emit('confirm');
-
await waitForPromises();
expect(findAlert().exists()).toBe(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 1790a9c9bf5..1a157beebe4 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
@@ -1,4 +1,4 @@
-import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
+import { GlEmptyState, GlLoadingIcon, GlTableLite } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
@@ -23,7 +23,9 @@ describe('BulkImportsHistoryApp', () => {
id: 1,
bulk_import_id: 1,
status: 'finished',
+ entity_type: 'group',
source_full_path: 'top-level-group-12',
+ destination_full_path: 'h5bp/top-level-group-12',
destination_name: 'top-level-group-12',
destination_namespace: 'h5bp',
created_at: '2021-07-08T10:03:44.743Z',
@@ -33,8 +35,10 @@ describe('BulkImportsHistoryApp', () => {
id: 2,
bulk_import_id: 2,
status: 'failed',
+ entity_type: 'project',
source_full_path: 'autodevops-demo',
destination_name: 'autodevops-demo',
+ destination_full_path: 'some-group/autodevops-demo',
destination_namespace: 'flightjs',
parent_id: null,
namespace_id: null,
@@ -74,6 +78,7 @@ describe('BulkImportsHistoryApp', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
+ mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
});
afterEach(() => {
@@ -97,11 +102,10 @@ describe('BulkImportsHistoryApp', () => {
});
it('renders table with data when history is available', async () => {
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
- const table = wrapper.findComponent(GlTable);
+ const table = wrapper.findComponent(GlTableLite);
expect(table.exists()).toBe(true);
// can't use .props() or .attributes() here
expect(table.vm.$attrs.items).toHaveLength(DUMMY_RESPONSE.length);
@@ -110,7 +114,6 @@ describe('BulkImportsHistoryApp', () => {
it('changes page when requested by pagination bar', async () => {
const NEW_PAGE = 4;
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
mock.resetHistory();
@@ -126,7 +129,6 @@ describe('BulkImportsHistoryApp', () => {
it('changes page size when requested by pagination bar', async () => {
const NEW_PAGE_SIZE = 4;
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
mock.resetHistory();
@@ -143,7 +145,6 @@ describe('BulkImportsHistoryApp', () => {
it('sets up the local storage sync correctly', async () => {
const NEW_PAGE_SIZE = 4;
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
mock.resetHistory();
@@ -155,12 +156,37 @@ describe('BulkImportsHistoryApp', () => {
});
it('renders correct url for destination group when relative_url is empty', async () => {
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent({ shallow: false });
await axios.waitForAll();
expect(wrapper.find('tbody tr a').attributes().href).toBe(
- `/${DUMMY_RESPONSE[0].destination_namespace}/${DUMMY_RESPONSE[0].destination_name}`,
+ `/${DUMMY_RESPONSE[0].destination_full_path}`,
+ );
+ });
+
+ it('renders loading icon when destination namespace is not defined', async () => {
+ const RESPONSE = [{ ...DUMMY_RESPONSE[0], destination_full_path: null }];
+
+ mock.onGet(API_URL).reply(200, RESPONSE, DEFAULT_HEADERS);
+ createComponent({ shallow: false });
+ await axios.waitForAll();
+
+ expect(wrapper.find('tbody tr').findComponent(GlLoadingIcon).exists()).toBe(true);
+ });
+
+ it('adds slash to group urls', async () => {
+ createComponent({ shallow: false });
+ await axios.waitForAll();
+
+ expect(wrapper.find('tbody tr a').text()).toBe(`${DUMMY_RESPONSE[0].destination_full_path}/`);
+ });
+
+ it('does not prefixes project urls with slash', async () => {
+ createComponent({ shallow: false });
+ await axios.waitForAll();
+
+ expect(wrapper.findAll('tbody tr a').at(1).text()).toBe(
+ DUMMY_RESPONSE[1].destination_full_path,
);
});
diff --git a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
index 9718d847ed5..aee56247209 100644
--- a/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/fork_form_spec.js
@@ -33,6 +33,7 @@ describe('ForkForm component', () => {
const DEFAULT_PROVIDE = {
newGroupPath: 'some/groups/path',
visibilityHelpPath: 'some/visibility/help/path',
+ cancelPath: '/some/project-full-path',
projectFullPath: '/some/project-full-path',
projectId: '10',
projectName: 'Project Name',
@@ -124,13 +125,13 @@ describe('ForkForm component', () => {
const findVisibilityRadioGroup = () =>
wrapper.find('[data-testid="fork-visibility-radio-group"]');
- it('will go to projectFullPath when click cancel button', () => {
+ it('will go to cancelPath when click cancel button', () => {
createComponent();
- const { projectFullPath } = DEFAULT_PROVIDE;
+ const { cancelPath } = DEFAULT_PROVIDE;
const cancelButton = wrapper.find('[data-testid="cancel-button"]');
- expect(cancelButton.attributes('href')).toBe(projectFullPath);
+ expect(cancelButton.attributes('href')).toBe(cancelPath);
});
const selectedMockNamespace = {
@@ -463,16 +464,12 @@ describe('ForkForm component', () => {
expect(urlUtility.redirectTo).not.toHaveBeenCalled();
});
- it('does not make POST request if no visbility is checked', async () => {
+ it('does not make POST request if no visibility is checked', async () => {
jest.spyOn(axios, 'post');
- setupComponent({
- fields: {
- visibility: {
- value: null,
- },
- },
- });
+ setupComponent();
+ wrapper.vm.form.fields.visibility.value = null;
+ await nextTick();
await submitForm();
diff --git a/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
index f6d3957115f..82f451ed6ef 100644
--- a/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
+++ b/spec/frontend/pages/projects/forks/new/components/project_namespace_spec.js
@@ -1,11 +1,4 @@
-import {
- GlButton,
- GlDropdown,
- GlDropdownItem,
- GlDropdownSectionHeader,
- GlSearchBoxByType,
- GlTruncate,
-} from '@gitlab/ui';
+import { GlButton, GlListboxItem, GlCollapsibleListbox } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -80,17 +73,16 @@ describe('ProjectNamespace component', () => {
};
const findButtonLabel = () => wrapper.findComponent(GlButton);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownText = () => wrapper.findComponent(GlTruncate);
- const findInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const findListBox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findListBoxText = () => findListBox().props('toggleText');
- const clickDropdownItem = async () => {
- wrapper.findComponent(GlDropdownItem).vm.$emit('click');
+ const clickListBoxItem = async (value = '') => {
+ wrapper.findComponent(GlListboxItem).vm.$emit('select', value);
await nextTick();
};
const showDropdown = () => {
- findDropdown().vm.$emit('shown');
+ findListBox().vm.$emit('shown');
};
beforeAll(() => {
@@ -115,7 +107,7 @@ describe('ProjectNamespace component', () => {
});
it('renders placeholder text', () => {
- expect(findDropdownText().props('text')).toBe('Select a namespace');
+ expect(findListBoxText()).toBe('Select a namespace');
});
});
@@ -127,24 +119,18 @@ describe('ProjectNamespace component', () => {
showDropdown();
});
- it('focuses on the input when the dropdown is opened', () => {
- const spy = jest.spyOn(findInput().vm, 'focusInput');
- showDropdown();
- expect(spy).toHaveBeenCalledTimes(1);
- });
-
it('displays fetched namespaces', () => {
const listItems = wrapper.findAll('li');
- expect(listItems).toHaveLength(3);
- expect(listItems.at(0).findComponent(GlDropdownSectionHeader).text()).toBe('Namespaces');
- expect(listItems.at(1).text()).toBe(data.project.forkTargets.nodes[0].fullPath);
- expect(listItems.at(2).text()).toBe(data.project.forkTargets.nodes[1].fullPath);
+ expect(listItems).toHaveLength(2);
+ expect(listItems.at(0).text()).toBe(data.project.forkTargets.nodes[0].fullPath);
+ expect(listItems.at(1).text()).toBe(data.project.forkTargets.nodes[1].fullPath);
});
it('sets the selected namespace', async () => {
const { fullPath } = data.project.forkTargets.nodes[0];
- await clickDropdownItem();
- expect(findDropdownText().props('text')).toBe(fullPath);
+ await clickListBoxItem(fullPath);
+
+ expect(findListBoxText()).toBe(fullPath);
});
});
@@ -155,7 +141,7 @@ describe('ProjectNamespace component', () => {
});
it('renders `No matches found`', () => {
- expect(wrapper.find('li').text()).toBe('No matches found');
+ expect(findListBox().text()).toContain('No matches found');
});
});
diff --git a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
index e7c7ec0d336..d67f842d011 100644
--- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
+++ b/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
@@ -45,6 +45,7 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
toggletext="rspec"
variant="default"
>
+
<!---->
<!---->
@@ -57,22 +58,31 @@ exports[`Code Coverage when fetching data is successful matches the snapshot 1`]
tabindex="-1"
>
<gl-listbox-item-stub
+ data-testid="listbox-item-0"
isselected="true"
>
rspec
</gl-listbox-item-stub>
- <gl-listbox-item-stub>
+ <gl-listbox-item-stub
+ data-testid="listbox-item-1"
+ >
cypress
</gl-listbox-item-stub>
- <gl-listbox-item-stub>
+ <gl-listbox-item-stub
+ data-testid="listbox-item-2"
+ >
karma
</gl-listbox-item-stub>
+
+ <!---->
+
+ <!---->
</ul>
<!---->
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index e99734963e3..2ff45266a07 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -6,7 +6,7 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import CodeCoverage from '~/pages/projects/graphs/components/code_coverage.vue';
import { codeCoverageMockData, sortedDataByDates } from './mock_data';
@@ -49,7 +49,7 @@ describe('Code Coverage', () => {
describe('when fetching data is successful', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
- mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData);
+ mockAxios.onGet().replyOnce(HTTP_STATUS_OK, codeCoverageMockData);
createComponent();
@@ -84,7 +84,7 @@ describe('Code Coverage', () => {
describe('when fetching data fails', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
- mockAxios.onGet().replyOnce(httpStatusCodes.BAD_REQUEST);
+ mockAxios.onGet().replyOnce(HTTP_STATUS_BAD_REQUEST);
createComponent();
@@ -108,7 +108,7 @@ describe('Code Coverage', () => {
describe('when fetching data succeed but returns an empty state', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
- mockAxios.onGet().replyOnce(httpStatusCodes.OK, []);
+ mockAxios.onGet().replyOnce(HTTP_STATUS_OK, []);
createComponent();
@@ -136,7 +136,7 @@ describe('Code Coverage', () => {
describe('dropdown options', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
- mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData);
+ mockAxios.onGet().replyOnce(HTTP_STATUS_OK, codeCoverageMockData);
createComponent();
@@ -153,7 +153,7 @@ describe('Code Coverage', () => {
describe('interactions', () => {
beforeEach(() => {
mockAxios = new MockAdapter(axios);
- mockAxios.onGet().replyOnce(httpStatusCodes.OK, codeCoverageMockData);
+ mockAxios.onGet().replyOnce(HTTP_STATUS_OK, codeCoverageMockData);
createComponent();
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js
index 897cbf5eaa4..29335308370 100644
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js
+++ b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js
@@ -85,6 +85,9 @@ describe('Learn GitLab Section Link', () => {
it('renders a popover trigger with question icon', () => {
expect(findPopoverTrigger().exists()).toBe(true);
expect(findPopoverTrigger().props('icon')).toBe('question-o');
+ expect(findPopoverTrigger().attributes('aria-label')).toBe(
+ LearnGitlabSectionLink.i18n.contactAdmin,
+ );
});
it('renders a popover', () => {
@@ -95,6 +98,15 @@ describe('Learn GitLab Section Link', () => {
});
});
+ it('renders default disabled message', () => {
+ expect(findPopover().text()).toContain(LearnGitlabSectionLink.i18n.contactAdmin);
+ });
+
+ it('renders custom disabled message if provided', () => {
+ createWrapper('trialStarted', { enabled: false, message: 'Custom message' });
+ expect(findPopover().text()).toContain('Custom message');
+ });
+
it('renders a link inside the popover', () => {
expect(findPopoverLink().exists()).toBe(true);
expect(findPopoverLink().attributes('href')).toBe(defaultProps.url);
diff --git a/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js b/spec/frontend/pages/projects/pipeline_schedules/shared/components/interval_pattern_input_spec.js
index 99df5b58d90..2d3b9afa8f6 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
@@ -138,7 +138,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 ( Cron syntax )',
+ 'Custom ( Learn more. )',
]);
});
});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
index 7c9aae13d25..c8e9a31b526 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_content_spec.js
@@ -5,7 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
import WikiContent from '~/pages/shared/wikis/components/wiki_content.vue';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import axios from '~/lib/utils/axios_utils';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import { handleLocationHash } from '~/lib/utils/common_utils';
@@ -59,7 +59,7 @@ describe('pages/shared/wikis/components/wiki_content', () => {
const content = 'content';
beforeEach(() => {
- mock.onGet(PATH, { params: { render_html: true } }).replyOnce(httpStatus.OK, { content });
+ mock.onGet(PATH, { params: { render_html: true } }).replyOnce(HTTP_STATUS_OK, { content });
buildWrapper();
return waitForPromises();
});
@@ -88,7 +88,7 @@ describe('pages/shared/wikis/components/wiki_content', () => {
describe('when loading content fails', () => {
beforeEach(() => {
- mock.onGet(PATH).replyOnce(httpStatus.INTERNAL_SERVER_ERROR, '');
+ mock.onGet(PATH).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, '');
buildWrapper();
return waitForPromises();
});
diff --git a/spec/frontend/pipeline_new/utils/format_refs_spec.js b/spec/frontend/pipeline_new/utils/format_refs_spec.js
deleted file mode 100644
index 71190f55c16..00000000000
--- a/spec/frontend/pipeline_new/utils/format_refs_spec.js
+++ /dev/null
@@ -1,21 +0,0 @@
-import { BRANCH_REF_TYPE, TAG_REF_TYPE } from '~/pipeline_new/constants';
-import formatRefs from '~/pipeline_new/utils/format_refs';
-import { mockBranchRefs, mockTagRefs } from '../mock_data';
-
-describe('Format refs util', () => {
- it('formats branch ref correctly', () => {
- expect(formatRefs(mockBranchRefs, BRANCH_REF_TYPE)).toEqual([
- { fullName: 'refs/heads/main', shortName: 'main' },
- { fullName: 'refs/heads/dev', shortName: 'dev' },
- { fullName: 'refs/heads/release', shortName: 'release' },
- ]);
- });
-
- it('formats tag ref correctly', () => {
- expect(formatRefs(mockTagRefs, TAG_REF_TYPE)).toEqual([
- { fullName: 'refs/tags/1.0.0', shortName: '1.0.0' },
- { fullName: 'refs/tags/1.1.0', shortName: '1.1.0' },
- { fullName: 'refs/tags/1.2.0', shortName: '1.2.0' },
- ]);
- });
-});
diff --git a/spec/frontend/pipeline_wizard/components/wrapper_spec.js b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
index d5b78cebcb3..33c6394eb41 100644
--- a/spec/frontend/pipeline_wizard/components/wrapper_spec.js
+++ b/spec/frontend/pipeline_wizard/components/wrapper_spec.js
@@ -364,6 +364,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
extra: {
fromStep: 0,
toStep: 1,
+ features: expect.any(Object),
},
});
});
@@ -386,6 +387,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
extra: {
fromStep: 1,
toStep: 0,
+ features: expect.any(Object),
},
});
});
@@ -409,6 +411,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
extra: {
fromStep: 2,
toStep: 1,
+ features: expect.any(Object),
},
});
});
@@ -429,6 +432,9 @@ describe('Pipeline Wizard - wrapper.vue', () => {
category: trackingCategory,
label: 'pipeline_wizard_commit',
property: 'commit',
+ extra: {
+ features: expect.any(Object),
+ },
});
});
@@ -443,6 +449,7 @@ describe('Pipeline Wizard - wrapper.vue', () => {
label: 'pipeline_wizard_editor_interaction',
extra: {
currentStep: 0,
+ features: expect.any(Object),
},
});
});
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index a3f15e25f36..351572fc83a 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -71,7 +71,7 @@ describe('Pipelines', () => {
const findTablePagination = () => wrapper.findComponent(TablePagination);
const findTab = (tab) => wrapper.findByTestId(`pipelines-tab-${tab}`);
- const findPipelineKeyDropdown = () => wrapper.findByTestId('pipeline-key-dropdown');
+ 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');
@@ -545,8 +545,8 @@ describe('Pipelines', () => {
expect(findFilteredSearch().exists()).toBe(true);
});
- it('renders the pipeline key dropdown', () => {
- expect(findPipelineKeyDropdown().exists()).toBe(true);
+ it('renders the pipeline key collapsible box', () => {
+ expect(findPipelineKeyCollapsibleBox().exists()).toBe(true);
});
it('renders tab empty state finished scope', async () => {
@@ -578,7 +578,7 @@ describe('Pipelines', () => {
});
it('does not render the pipeline key dropdown', () => {
- expect(findPipelineKeyDropdown().exists()).toBe(false);
+ expect(findPipelineKeyCollapsibleBox().exists()).toBe(false);
});
it('does not render tabs nor buttons', () => {
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 740037a5ac8..9359bd9b95f 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -17,7 +17,7 @@ import {
TRACKING_CATEGORIES,
} from '~/pipelines/constants';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
jest.mock('~/pipelines/event_hub');
@@ -50,7 +50,7 @@ describe('Pipelines Table', () => {
};
const findGlTableLite = () => wrapper.findComponent(GlTableLite);
- const findStatusBadge = () => wrapper.findComponent(CiBadge);
+ const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
const findPipelineInfo = () => wrapper.findComponent(PipelineUrl);
const findTriggerer = () => wrapper.findComponent(PipelineTriggerer);
const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
@@ -97,7 +97,7 @@ describe('Pipelines Table', () => {
describe('status cell', () => {
it('should render a status badge', () => {
- expect(findStatusBadge().exists()).toBe(true);
+ expect(findCiBadgeLink().exists()).toBe(true);
});
});
@@ -171,7 +171,7 @@ describe('Pipelines Table', () => {
});
it('tracks status badge click', () => {
- findStatusBadge().vm.$emit('ciStatusBadgeClick');
+ findCiBadgeLink().vm.$emit('ciStatusBadgeClick');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_ci_status_badge', {
label: TRACKING_CATEGORIES.table,
diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
index a84dd246f5d..7334e007e18 100644
--- a/spec/frontend/projects/commit/components/branches_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
@@ -1,9 +1,8 @@
-import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import BranchesDropdown from '~/projects/commit/components/branches_dropdown.vue';
Vue.use(Vuex);
@@ -34,12 +33,7 @@ describe('BranchesDropdown', () => {
}),
);
};
-
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
- const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
- const findNoResults = () => wrapper.findByTestId('empty-result-message');
- const findLoading = () => wrapper.findByTestId('dropdown-text-loading-icon');
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
afterEach(() => {
wrapper.destroy();
@@ -55,72 +49,6 @@ describe('BranchesDropdown', () => {
it('invokes fetchBranches', () => {
expect(spyFetchBranches).toHaveBeenCalled();
});
-
- describe('with a value but visually blanked', () => {
- beforeEach(() => {
- createComponent({ value: '_main_', blanked: true }, { branch: '_main_' });
- });
-
- it('renders all branches', () => {
- expect(findAllDropdownItems()).toHaveLength(3);
- expect(findDropdownItemByIndex(0).text()).toBe('_main_');
- expect(findDropdownItemByIndex(1).text()).toBe('_branch_1_');
- expect(findDropdownItemByIndex(2).text()).toBe('_branch_2_');
- });
-
- it('selects the active branch', () => {
- expect(wrapper.vm.isSelected('_main_')).toBe(true);
- });
- });
- });
-
- describe('Loading states', () => {
- it('shows loading icon while fetching', () => {
- createComponent({ value: '' }, { isFetching: true });
-
- expect(findLoading().isVisible()).toBe(true);
- });
-
- it('does not show loading icon', () => {
- createComponent({ value: '' });
-
- expect(findLoading().isVisible()).toBe(false);
- });
- });
-
- describe('No branches found', () => {
- beforeEach(() => {
- createComponent({ value: '_non_existent_branch_' });
- });
-
- it('renders empty results message', () => {
- expect(findNoResults().text()).toBe('No matching results');
- });
-
- it('shows GlSearchBoxByType with default attributes', () => {
- expect(findSearchBoxByType().exists()).toBe(true);
- expect(findSearchBoxByType().vm.$attrs).toMatchObject({
- placeholder: 'Search branches',
- debounce: DEFAULT_DEBOUNCE_AND_THROTTLE_MS,
- });
- });
- });
-
- describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent({ value: '' });
- });
-
- it('renders all branches when search term is empty', () => {
- expect(findAllDropdownItems()).toHaveLength(3);
- expect(findDropdownItemByIndex(0).text()).toBe('_main_');
- expect(findDropdownItemByIndex(1).text()).toBe('_branch_1_');
- expect(findDropdownItemByIndex(2).text()).toBe('_branch_2_');
- });
-
- it('should not be selected on the inactive branch', () => {
- expect(wrapper.vm.isSelected('_main_')).toBe(false);
- });
});
describe('When searching', () => {
@@ -131,7 +59,7 @@ describe('BranchesDropdown', () => {
it('invokes fetchBranches', async () => {
const spy = jest.spyOn(wrapper.vm, 'fetchBranches');
- findSearchBoxByType().vm.$emit('input', '_anything_');
+ findDropdown().vm.$emit('search', '_anything_');
await nextTick();
@@ -140,46 +68,13 @@ describe('BranchesDropdown', () => {
});
});
- describe('Branches found', () => {
- beforeEach(() => {
- createComponent({ value: '_branch_1_' }, { branch: '_branch_1_' });
- });
-
- it('renders only the branch searched for', () => {
- expect(findAllDropdownItems()).toHaveLength(1);
- expect(findDropdownItemByIndex(0).text()).toBe('_branch_1_');
- });
-
- it('should not display empty results message', () => {
- expect(findNoResults().exists()).toBe(false);
- });
-
- it('should signify this branch is selected', () => {
- expect(wrapper.vm.isSelected('_branch_1_')).toBe(true);
- });
-
- it('should signify the branch is not selected', () => {
- expect(wrapper.vm.isSelected('_not_selected_branch_')).toBe(false);
- });
-
- describe('Custom events', () => {
- it('should emit selectBranch if an branch is clicked', () => {
- findDropdownItemByIndex(0).vm.$emit('click');
-
- expect(wrapper.emitted('selectBranch')).toEqual([['_branch_1_']]);
- expect(wrapper.vm.searchTerm).toBe('_branch_1_');
- });
- });
- });
-
describe('Case insensitive for search term', () => {
beforeEach(() => {
createComponent({ value: '_BrAnCh_1_' });
});
- it('renders only the branch searched for', () => {
- expect(findAllDropdownItems()).toHaveLength(1);
- expect(findDropdownItemByIndex(0).text()).toBe('_branch_1_');
+ it('returns only the branch searched for', () => {
+ expect(findDropdown().props('items')).toEqual([{ text: '_branch_1_', value: '_branch_1_' }]);
});
});
});
diff --git a/spec/frontend/projects/commit/components/projects_dropdown_spec.js b/spec/frontend/projects/commit/components/projects_dropdown_spec.js
index bb20918e0cd..0e213ff388a 100644
--- a/spec/frontend/projects/commit/components/projects_dropdown_spec.js
+++ b/spec/frontend/projects/commit/components/projects_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
@@ -35,78 +35,23 @@ describe('ProjectsDropdown', () => {
);
};
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
- const findDropdownItemByIndex = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
- const findNoResults = () => wrapper.findByTestId('empty-result-message');
+ const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
afterEach(() => {
wrapper.destroy();
spyFetchProjects.mockReset();
});
- describe('No projects found', () => {
- beforeEach(() => {
- createComponent('_non_existent_project_');
- });
-
- it('renders empty results message', () => {
- expect(findNoResults().text()).toBe('No matching results');
- });
-
- it('shows GlSearchBoxByType with default attributes', () => {
- expect(findSearchBoxByType().exists()).toBe(true);
- expect(findSearchBoxByType().vm.$attrs).toMatchObject({
- placeholder: 'Search projects',
- });
- });
- });
-
- describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent('');
- });
-
- it('renders all projects when search term is empty', () => {
- expect(findAllDropdownItems()).toHaveLength(3);
- expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
- expect(findDropdownItemByIndex(1).text()).toBe('_project_2_');
- expect(findDropdownItemByIndex(2).text()).toBe('_project_3_');
- });
-
- it('should not be selected on the inactive project', () => {
- expect(wrapper.vm.isSelected('_project_1_')).toBe(false);
- });
- });
-
describe('Projects found', () => {
beforeEach(() => {
createComponent('_project_1_', { targetProjectId: '1' });
});
- it('renders only the project searched for', () => {
- expect(findAllDropdownItems()).toHaveLength(1);
- expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
- });
-
- it('should not display empty results message', () => {
- expect(findNoResults().exists()).toBe(false);
- });
-
- it('should signify this project is selected', () => {
- expect(findDropdownItemByIndex(0).props('isChecked')).toBe(true);
- });
-
- it('should signify the project is not selected', () => {
- expect(wrapper.vm.isSelected('_not_selected_project_')).toBe(false);
- });
-
describe('Custom events', () => {
it('should emit selectProject if a project is clicked', () => {
- findDropdownItemByIndex(0).vm.$emit('click');
+ findDropdown().vm.$emit('select', '1');
expect(wrapper.emitted('selectProject')).toEqual([['1']]);
- expect(wrapper.vm.filterTerm).toBe('_project_1_');
});
});
});
@@ -117,8 +62,7 @@ describe('ProjectsDropdown', () => {
});
it('renders only the project searched for', () => {
- expect(findAllDropdownItems()).toHaveLength(1);
- expect(findDropdownItemByIndex(0).text()).toBe('_project_1_');
+ expect(findDropdown().props('items')).toEqual([{ text: '_project_1_', value: '1' }]);
});
});
});
diff --git a/spec/frontend/projects/merge_requests/components/report_abuse_dropdown_item_spec.js b/spec/frontend/projects/merge_requests/components/report_abuse_dropdown_item_spec.js
new file mode 100644
index 00000000000..35b10375821
--- /dev/null
+++ b/spec/frontend/projects/merge_requests/components/report_abuse_dropdown_item_spec.js
@@ -0,0 +1,73 @@
+import { nextTick } from 'vue';
+import { GlDropdownItem } from '@gitlab/ui';
+import { MountingPortal } from 'portal-vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import ReportAbuseDropdownItem from '~/projects/merge_requests/components/report_abuse_dropdown_item.vue';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+
+describe('ReportAbuseDropdownItem', () => {
+ let wrapper;
+
+ const ACTION_PATH = '/abuse_reports/add_category';
+ const USER_ID = '1';
+ const REPORTED_FROM_URL = 'http://example.com';
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(ReportAbuseDropdownItem, {
+ propsData: {
+ ...props,
+ },
+ provide: {
+ reportAbusePath: ACTION_PATH,
+ reportedUserId: USER_ID,
+ reportedFromUrl: REPORTED_FROM_URL,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findReportAbuseItem = () => wrapper.findComponent(GlDropdownItem);
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+ const findMountingPortal = () => wrapper.findComponent(MountingPortal);
+
+ it('renders report abuse dropdown item', () => {
+ expect(findReportAbuseItem().text()).toBe(ReportAbuseDropdownItem.i18n.reportAbuse);
+ });
+
+ it('renders abuse category selector with the drawer initially closed', () => {
+ expect(findAbuseCategorySelector().exists()).toBe(true);
+
+ expect(findAbuseCategorySelector().props('showDrawer')).toBe(false);
+ });
+
+ it('renders abuse category selector inside MountingPortal', () => {
+ expect(findMountingPortal().props()).toMatchObject({
+ mountTo: '#js-report-abuse-drawer',
+ append: true,
+ name: 'abuse-category-selector',
+ });
+ });
+
+ describe('when dropdown item is clicked', () => {
+ beforeEach(() => {
+ findReportAbuseItem().vm.$emit('click');
+ return nextTick();
+ });
+
+ it('opens the abuse category selector', () => {
+ expect(findAbuseCategorySelector().props('showDrawer')).toBe(true);
+ });
+
+ it('closes the abuse category selector', async () => {
+ findAbuseCategorySelector().vm.$emit('close-drawer');
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().props('showDrawer')).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
index 8c18d2992ea..cf28eda5349 100644
--- a/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
+++ b/spec/frontend/projects/pipelines/charts/components/ci_cd_analytics_charts_spec.js
@@ -5,25 +5,32 @@ import CiCdAnalyticsCharts from '~/vue_shared/components/ci_cd_analytics/ci_cd_a
import SegmentedControlButtonGroup from '~/vue_shared/components/segmented_control_button_group.vue';
import { transformedAreaChartData, chartOptions } from '../mock_data';
+const charts = [
+ {
+ range: 'test range 1',
+ title: 'title 1',
+ data: transformedAreaChartData,
+ },
+ {
+ range: 'test range 2',
+ title: 'title 2',
+ data: transformedAreaChartData,
+ },
+ {
+ range: 'test range 3',
+ title: 'title 3',
+ data: transformedAreaChartData,
+ },
+ {
+ range: 'test range 4',
+ title: 'title 4',
+ data: transformedAreaChartData,
+ },
+];
+
const DEFAULT_PROPS = {
chartOptions,
- charts: [
- {
- range: 'test range 1',
- title: 'title 1',
- data: transformedAreaChartData,
- },
- {
- range: 'test range 2',
- title: 'title 2',
- data: transformedAreaChartData,
- },
- {
- range: 'test range 3',
- title: 'title 3',
- data: transformedAreaChartData,
- },
- ],
+ charts,
};
describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', () => {
@@ -55,13 +62,13 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
wrapper = createWrapper();
});
- it('should default to the first chart', () => {
- expect(findSegmentedControl().props('value')).toBe(0);
+ it('should default to the 3rd chart (last 90 days)', () => {
+ expect(findSegmentedControl().props('value')).toBe(2);
});
it('should use the title and index as values', () => {
const options = findSegmentedControl().props('options');
- expect(options).toHaveLength(3);
+ expect(options).toHaveLength(charts.length);
expect(options).toEqual([
{
text: 'title 1',
@@ -75,6 +82,10 @@ describe('~/vue_shared/components/ci_cd_analytics/ci_cd_analytics_charts.vue', (
text: 'title 3',
value: 2,
},
+ {
+ text: 'title 4',
+ value: 3,
+ },
]);
});
diff --git a/spec/frontend/projects/settings/components/default_branch_selector_spec.js b/spec/frontend/projects/settings/components/default_branch_selector_spec.js
index 94648d87524..bfbf3e234f4 100644
--- a/spec/frontend/projects/settings/components/default_branch_selector_spec.js
+++ b/spec/frontend/projects/settings/components/default_branch_selector_spec.js
@@ -32,6 +32,7 @@ describe('projects/settings/components/default_branch_selector', () => {
value: persistedDefaultBranch,
enabledRefTypes: [REF_TYPE_BRANCHES],
projectId,
+ refType: null,
state: true,
translations: {
dropdownHeader: expect.any(String),
diff --git a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
index 49c45c080b4..8d0fd390e35 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/components/branch_rule_spec.js
@@ -20,6 +20,7 @@ describe('Branch rule', () => {
};
const findDefaultBadge = () => wrapper.findByText(i18n.defaultLabel);
+ const findProtectedBadge = () => wrapper.findByText(i18n.protectedLabel);
const findBranchName = () => wrapper.findByText(branchRulePropsMock.name);
const findProtectionDetailsList = () => wrapper.findByRole('list');
const findProtectionDetailsListItems = () => wrapper.findAllByRole('listitem');
@@ -32,17 +33,23 @@ describe('Branch rule', () => {
});
describe('badges', () => {
- it('renders default badge', () => {
+ it('renders both default and protected badges', () => {
expect(findDefaultBadge().exists()).toBe(true);
+ expect(findProtectedBadge().exists()).toBe(true);
});
it('does not render default badge if isDefault is set to false', () => {
createComponent({ isDefault: false });
expect(findDefaultBadge().exists()).toBe(false);
});
+
+ it('does not render default badge if branchProtection is null', () => {
+ createComponent(branchRuleWithoutDetailsPropsMock);
+ expect(findProtectedBadge().exists()).toBe(false);
+ });
});
- it('does not render the protection details list if no details are present', () => {
+ it('does not render the protection details list when branchProtection is null', () => {
createComponent(branchRuleWithoutDetailsPropsMock);
expect(findProtectionDetailsList().exists()).toBe(false);
});
diff --git a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
index 6f506882c36..de7f6c8b88d 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/mock_data.js
@@ -92,10 +92,7 @@ export const branchRuleWithoutDetailsPropsMock = {
name: 'branch-1',
isDefault: false,
matchingBranchesCount: 1,
- branchProtection: {
- allowForcePush: false,
- codeOwnerApprovalRequired: false,
- },
+ branchProtection: null,
approvalRulesTotal: 0,
statusChecksTotal: 0,
};
diff --git a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
index 13f3eea277a..5fc9f9ba629 100644
--- a/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
+++ b/spec/frontend/projects/settings_service_desk/components/service_desk_root_spec.js
@@ -3,7 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import AxiosMockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ServiceDeskRoot from '~/projects/settings_service_desk/components/service_desk_root.vue';
import ServiceDeskSetting from '~/projects/settings_service_desk/components/service_desk_setting.vue';
@@ -95,7 +95,7 @@ describe('ServiceDeskRoot', () => {
});
it('sends a request to turn service desk on', () => {
- axiosMock.onPut(provideData.endpoint).replyOnce(httpStatusCodes.OK);
+ axiosMock.onPut(provideData.endpoint).replyOnce(HTTP_STATUS_OK);
expect(spy).toHaveBeenCalledWith(provideData.endpoint, { service_desk_enabled: true });
});
@@ -117,7 +117,7 @@ describe('ServiceDeskRoot', () => {
});
it('sends a request to turn service desk off', () => {
- axiosMock.onPut(provideData.endpoint).replyOnce(httpStatusCodes.OK);
+ axiosMock.onPut(provideData.endpoint).replyOnce(HTTP_STATUS_OK);
expect(spy).toHaveBeenCalledWith(provideData.endpoint, { service_desk_enabled: false });
});
@@ -133,7 +133,7 @@ describe('ServiceDeskRoot', () => {
describe('save event', () => {
describe('successful request', () => {
beforeEach(async () => {
- axiosMock.onPut(provideData.endpoint).replyOnce(httpStatusCodes.OK);
+ axiosMock.onPut(provideData.endpoint).replyOnce(HTTP_STATUS_OK);
wrapper = createComponent();
diff --git a/spec/frontend/read_more_spec.js b/spec/frontend/read_more_spec.js
index 80d7c941660..9eddc50d50a 100644
--- a/spec/frontend/read_more_spec.js
+++ b/spec/frontend/read_more_spec.js
@@ -1,21 +1,23 @@
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { loadHTMLFixture, resetHTMLFixture, setHTMLFixture } from 'helpers/fixtures';
import initReadMore from '~/read_more';
describe('Read more click-to-expand functionality', () => {
const fixtureName = 'projects/overview.html';
- beforeEach(() => {
- loadHTMLFixture(fixtureName);
- });
+ const findTrigger = () => document.querySelector('.js-read-more-trigger');
afterEach(() => {
resetHTMLFixture();
});
describe('expands target element', () => {
+ beforeEach(() => {
+ loadHTMLFixture(fixtureName);
+ });
+
it('adds "is-expanded" class to target element', () => {
const target = document.querySelector('.read-more-container');
- const trigger = document.querySelector('.js-read-more-trigger');
+ const trigger = findTrigger();
initReadMore();
trigger.click();
@@ -23,4 +25,25 @@ describe('Read more click-to-expand functionality', () => {
expect(target.classList.contains('is-expanded')).toEqual(true);
});
});
+
+ describe('given click on nested element', () => {
+ beforeEach(() => {
+ setHTMLFixture(`
+ <p>Target</p>
+ <button type="button" class="js-read-more-trigger">
+ <span>Button text</span>
+ </button>
+ `);
+
+ const trigger = findTrigger();
+ const nestedElement = trigger.firstElementChild;
+ initReadMore();
+
+ nestedElement.click();
+ });
+
+ it('removes the trigger element', async () => {
+ expect(findTrigger()).toBe(null);
+ });
+ });
});
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index 96601a729b2..4997c13bbb2 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -18,6 +18,8 @@ import {
REF_TYPE_BRANCHES,
REF_TYPE_TAGS,
REF_TYPE_COMMITS,
+ BRANCH_REF_TYPE,
+ TAG_REF_TYPE,
} from '~/ref/constants';
import createStore from '~/ref/stores/';
@@ -34,7 +36,7 @@ describe('Ref selector component', () => {
let commitApiCallSpy;
let requestSpies;
- const createComponent = (mountOverrides = {}) => {
+ const createComponent = (mountOverrides = {}, propsData = {}) => {
wrapper = mount(
RefSelector,
merge(
@@ -42,6 +44,7 @@ describe('Ref selector component', () => {
propsData: {
projectId,
value: '',
+ ...propsData,
},
listeners: {
// simulate a parent component v-model binding
@@ -338,13 +341,14 @@ describe('Ref selector component', () => {
describe('branches', () => {
describe('when the branches search returns results', () => {
beforeEach(() => {
- createComponent();
+ createComponent({}, { refType: BRANCH_REF_TYPE, useSymbolicRefNames: true });
return waitForRequests();
});
it('renders the branches section in the dropdown', () => {
expect(findBranchesSection().exists()).toBe(true);
+ expect(findBranchesSection().props('shouldShowCheck')).toBe(true);
});
it('renders the "Branches" heading with a total number indicator', () => {
@@ -415,13 +419,14 @@ describe('Ref selector component', () => {
describe('tags', () => {
describe('when the tags search returns results', () => {
beforeEach(() => {
- createComponent();
+ createComponent({}, { refType: TAG_REF_TYPE, useSymbolicRefNames: true });
return waitForRequests();
});
it('renders the tags section in the dropdown', () => {
expect(findTagsSection().exists()).toBe(true);
+ expect(findTagsSection().props('shouldShowCheck')).toBe(true);
});
it('renders the "Tags" heading with a total number indicator', () => {
diff --git a/spec/frontend/repository/commits_service_spec.js b/spec/frontend/repository/commits_service_spec.js
index de7c56f239a..e56975d021a 100644
--- a/spec/frontend/repository/commits_service_spec.js
+++ b/spec/frontend/repository/commits_service_spec.js
@@ -1,9 +1,10 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service';
-import httpStatus from '~/lib/utils/http_status';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { createAlert } from '~/flash';
import { I18N_COMMIT_DATA_FETCH_ERROR } from '~/repository/constants';
+import { refWithSpecialCharMock } from './mock_data';
jest.mock('~/flash');
@@ -14,7 +15,7 @@ describe('commits service', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(url).reply(httpStatus.OK, [], {});
+ mock.onGet(url).reply(HTTP_STATUS_OK, [], {});
jest.spyOn(axios, 'get');
});
@@ -39,10 +40,12 @@ describe('commits service', () => {
expect(axios.get).toHaveBeenCalledWith(testUrl, { params: { format: 'json', offset } });
});
- it('encodes the path correctly', async () => {
- await requestCommits(1, 'some-project', 'with $peci@l ch@rs/');
+ it('encodes the path and ref', async () => {
+ const encodedRef = encodeURIComponent(refWithSpecialCharMock);
+ const encodedUrl = `/some-project/-/refs/${encodedRef}/logs_tree/with%20%24peci%40l%20ch%40rs%2F`;
+
+ await requestCommits(1, 'some-project', 'with $peci@l ch@rs/', refWithSpecialCharMock);
- const encodedUrl = '/some-project/-/refs/main/logs_tree/with%20%24peci%40l%20ch%40rs%2F';
expect(axios.get).toHaveBeenCalledWith(encodedUrl, expect.anything());
});
@@ -68,7 +71,7 @@ describe('commits service', () => {
it('calls `createAlert` when the request fails', async () => {
const invalidPath = '/#@ some/path';
const invalidUrl = `${url}${invalidPath}`;
- mock.onGet(invalidUrl).replyOnce(httpStatus.INTERNAL_SERVER_ERROR, [], {});
+ mock.onGet(invalidUrl).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, [], {});
await requestCommits(1, 'my-project', invalidPath);
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 6ece72c41bb..2e8860f67ef 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -25,7 +25,7 @@ import CodeIntelligence from '~/code_navigation/components/app.vue';
import * as urlUtility from '~/lib/utils/url_utility';
import { isLoggedIn, handleLocationHash } from '~/lib/utils/common_utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import LineHighlighter from '~/blob/line_highlighter';
import { LEGACY_FILE_TYPES } from '~/repository/constants';
import { SIMPLE_BLOB_VIEWER, RICH_BLOB_VIEWER } from '~/blob/components/constants';
@@ -256,19 +256,19 @@ describe('Blob content viewer component', () => {
);
it('loads the LineHighlighter', async () => {
- mockAxios.onGet(legacyViewerUrl).replyOnce(httpStatusCodes.OK, 'test');
+ mockAxios.onGet(legacyViewerUrl).replyOnce(HTTP_STATUS_OK, 'test');
await createComponent({ blob: { ...simpleViewerMock, fileType, highlightJs } });
expect(LineHighlighter).toHaveBeenCalled();
});
it('does not load the LineHighlighter for RichViewers', async () => {
- mockAxios.onGet(legacyViewerUrl).replyOnce(httpStatusCodes.OK, 'test');
+ mockAxios.onGet(legacyViewerUrl).replyOnce(HTTP_STATUS_OK, 'test');
await createComponent({ blob: { ...richViewerMock, fileType, highlightJs } });
expect(LineHighlighter).not.toHaveBeenCalled();
});
it('scrolls to the hash', async () => {
- mockAxios.onGet(legacyViewerUrl).replyOnce(httpStatusCodes.OK, 'test');
+ mockAxios.onGet(legacyViewerUrl).replyOnce(HTTP_STATUS_OK, 'test');
await createComponent({ blob: { ...simpleViewerMock, fileType, highlightJs } });
expect(handleLocationHash).toHaveBeenCalled();
});
@@ -368,7 +368,7 @@ describe('Blob content viewer component', () => {
it('does not load a CodeIntelligence component when no viewers are loaded', async () => {
const url = 'some_file.js?format=json&viewer=rich';
- mockAxios.onGet(url).replyOnce(httpStatusCodes.INTERNAL_SERVER_ERROR);
+ mockAxios.onGet(url).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await createComponent({ blob: { ...richViewerMock, fileType: 'unknown' } });
expect(findCodeIntelligence().exists()).toBe(false);
diff --git a/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js
new file mode 100644
index 00000000000..51f3d31ec72
--- /dev/null
+++ b/spec/frontend/repository/components/blob_viewers/notebook_viewer_spec.js
@@ -0,0 +1,40 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import NotebookViewer from '~/repository/components/blob_viewers/notebook_viewer.vue';
+import notebookLoader from '~/blob/notebook';
+
+jest.mock('~/blob/notebook');
+
+describe('Notebook Viewer', () => {
+ let wrapper;
+
+ const ROOT_RELATIVE_PATH = '/some/notebook/';
+ const DEFAULT_BLOB_DATA = { rawPath: `${ROOT_RELATIVE_PATH}file.ipynb` };
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(NotebookViewer, {
+ propsData: { blob: DEFAULT_BLOB_DATA },
+ });
+ };
+
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findNotebookWrapper = () => wrapper.findByTestId('notebook');
+
+ beforeEach(() => createComponent());
+
+ it('calls the notebook loader', () => {
+ expect(notebookLoader).toHaveBeenCalledWith({
+ el: wrapper.vm.$refs.viewer,
+ relativeRawPath: ROOT_RELATIVE_PATH,
+ });
+ });
+
+ it('renders a loading icon component', () => {
+ expect(findLoadingIcon().props('size')).toBe('lg');
+ });
+
+ it('renders the notebook wrapper', () => {
+ expect(findNotebookWrapper().exists()).toBe(true);
+ expect(findNotebookWrapper().attributes('data-endpoint')).toBe(DEFAULT_BLOB_DATA.rawPath);
+ });
+});
diff --git a/spec/frontend/repository/components/blob_viewers/openapi_viewer_spec.js b/spec/frontend/repository/components/blob_viewers/openapi_viewer_spec.js
new file mode 100644
index 00000000000..21994d04076
--- /dev/null
+++ b/spec/frontend/repository/components/blob_viewers/openapi_viewer_spec.js
@@ -0,0 +1,30 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import OpenapiViewer from '~/repository/components/blob_viewers/openapi_viewer.vue';
+import renderOpenApi from '~/blob/openapi';
+
+jest.mock('~/blob/openapi');
+
+describe('OpenAPI Viewer', () => {
+ let wrapper;
+
+ const DEFAULT_BLOB_DATA = { rawPath: 'some/openapi.yml' };
+
+ const createOpenApiViewer = () => {
+ wrapper = shallowMountExtended(OpenapiViewer, {
+ propsData: { blob: DEFAULT_BLOB_DATA },
+ });
+ };
+
+ const findOpenApiViewer = () => wrapper.findByTestId('openapi');
+
+ beforeEach(() => createOpenApiViewer());
+
+ it('calls the openapi render', () => {
+ expect(renderOpenApi).toHaveBeenCalledWith(wrapper.vm.$refs.viewer);
+ });
+
+ it('renders an openapi viewer', () => {
+ expect(findOpenApiViewer().exists()).toBe(true);
+ expect(findOpenApiViewer().attributes('data-endpoint')).toBe(DEFAULT_BLOB_DATA.rawPath);
+ });
+});
diff --git a/spec/frontend/repository/components/fork_info_spec.js b/spec/frontend/repository/components/fork_info_spec.js
new file mode 100644
index 00000000000..c23d5ae5823
--- /dev/null
+++ b/spec/frontend/repository/components/fork_info_spec.js
@@ -0,0 +1,122 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlSkeletonLoader, GlIcon, GlLink } 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 { createAlert } from '~/flash';
+
+import ForkInfo, { i18n } from '~/repository/components/fork_info.vue';
+import forkDetailsQuery from '~/repository/queries/fork_details.query.graphql';
+import { propsForkInfo } from '../mock_data';
+
+jest.mock('~/flash');
+
+describe('ForkInfo component', () => {
+ let wrapper;
+ let mockResolver;
+ const forkInfoError = new Error('Something went wrong');
+
+ Vue.use(VueApollo);
+
+ const createCommitData = ({ ahead = 3, behind = 7 }) => {
+ return {
+ data: {
+ project: { id: '1', forkDetails: { ahead, behind, __typename: 'ForkDetails' } },
+ },
+ };
+ };
+
+ const createComponent = (props = {}, data = {}, isRequestFailed = false) => {
+ mockResolver = isRequestFailed
+ ? jest.fn().mockRejectedValue(forkInfoError)
+ : jest.fn().mockResolvedValue(createCommitData(data));
+
+ wrapper = shallowMountExtended(ForkInfo, {
+ apolloProvider: createMockApollo([[forkDetailsQuery, mockResolver]]),
+ propsData: { ...propsForkInfo, ...props },
+ });
+ return waitForPromises();
+ };
+
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
+ const findIcon = () => wrapper.findComponent(GlIcon);
+ const findDivergenceMessage = () => wrapper.find('.gl-text-secondary');
+ const findInaccessibleMessage = () => wrapper.findByTestId('inaccessible-project');
+ it('displays a skeleton while loading data', async () => {
+ createComponent();
+ expect(findSkeleton().exists()).toBe(true);
+ });
+
+ it('does not display skeleton when data is loaded', async () => {
+ await createComponent();
+ expect(findSkeleton().exists()).toBe(false);
+ });
+
+ it('renders fork icon', async () => {
+ await createComponent();
+ expect(findIcon().exists()).toBe(true);
+ });
+
+ it('queries the data when sourceName is present', async () => {
+ await createComponent();
+ expect(mockResolver).toHaveBeenCalled();
+ });
+
+ it('does not query the data when sourceName is empty', async () => {
+ await createComponent({ sourceName: null });
+ expect(mockResolver).not.toHaveBeenCalled();
+ });
+
+ it('renders inaccessible message when fork source is not available', async () => {
+ await createComponent({ sourceName: '' });
+ const message = findInaccessibleMessage();
+ expect(message.exists()).toBe(true);
+ expect(message.text()).toBe(i18n.inaccessibleProject);
+ });
+
+ it('shows source project name with a link to a repo', async () => {
+ await createComponent();
+ const link = findLink();
+ expect(link.text()).toBe(propsForkInfo.sourceName);
+ expect(link.attributes('href')).toBe(propsForkInfo.sourcePath);
+ });
+
+ it('renders unknown divergence message when divergence is unknown', async () => {
+ await createComponent({}, { ahead: null, behind: null });
+ expect(findDivergenceMessage().text()).toBe(i18n.unknown);
+ });
+
+ it('shows correct divergence message when data is present', async () => {
+ await createComponent();
+ expect(findDivergenceMessage().text()).toMatchInterpolatedText(
+ '7 commits behind, 3 commits ahead of the upstream repository.',
+ );
+ });
+
+ it('renders up to date message when divergence is unknown', async () => {
+ await createComponent({}, { ahead: 0, behind: 0 });
+ expect(findDivergenceMessage().text()).toBe(i18n.upToDate);
+ });
+
+ it('renders commits ahead message', async () => {
+ await createComponent({}, { behind: 0 });
+ expect(findDivergenceMessage().text()).toBe('3 commits ahead of the upstream repository.');
+ });
+
+ it('renders commits behind message', async () => {
+ await createComponent({}, { ahead: 0 });
+
+ expect(findDivergenceMessage().text()).toBe('7 commits behind the upstream repository.');
+ });
+
+ it('renders alert with error message when request fails', async () => {
+ await createComponent({}, {}, true);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: i18n.error,
+ captureError: true,
+ error: forkInfoError,
+ });
+ });
+});
diff --git a/spec/frontend/repository/components/new_directory_modal_spec.js b/spec/frontend/repository/components/new_directory_modal_spec.js
index cf0d48280f4..4e5c9a685c4 100644
--- a/spec/frontend/repository/components/new_directory_modal_spec.js
+++ b/spec/frontend/repository/components/new_directory_modal_spec.js
@@ -5,7 +5,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import NewDirectoryModal from '~/repository/components/new_directory_modal.vue';
@@ -149,7 +149,7 @@ describe('NewDirectoryModal', () => {
originalBranch,
createNewMr,
} = defaultFormValue;
- mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {});
+ mock.onPost(initialProps.path).reply(HTTP_STATUS_OK, {});
await fillForm();
await submitForm();
@@ -161,7 +161,7 @@ describe('NewDirectoryModal', () => {
});
it('does not submit "create_merge_request" formData if createNewMr is not checked', async () => {
- mock.onPost(initialProps.path).reply(httpStatusCodes.OK, {});
+ mock.onPost(initialProps.path).reply(HTTP_STATUS_OK, {});
await fillForm({ createNewMr: false });
await submitForm();
expect(mock.history.post[0].data.get('create_merge_request')).toBeNull();
@@ -169,7 +169,7 @@ describe('NewDirectoryModal', () => {
it('redirects to the new directory', async () => {
const response = { filePath: 'new-dir-path' };
- mock.onPost(initialProps.path).reply(httpStatusCodes.OK, response);
+ mock.onPost(initialProps.path).reply(HTTP_STATUS_OK, response);
await fillForm({ dirName: 'foo', branchName: 'master', commitMessage: 'foo' });
await submitForm();
diff --git a/spec/frontend/repository/components/tree_content_spec.js b/spec/frontend/repository/components/tree_content_spec.js
index 6eea66f1a7d..f694c8e9166 100644
--- a/spec/frontend/repository/components/tree_content_spec.js
+++ b/spec/frontend/repository/components/tree_content_spec.js
@@ -5,19 +5,25 @@ import FilePreview from '~/repository/components/preview/index.vue';
import FileTable from '~/repository/components/table/index.vue';
import TreeContent from 'jh_else_ce/repository/components/tree_content.vue';
import { loadCommits, isRequested, resetRequestedCommits } from '~/repository/commits_service';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import { i18n } from '~/repository/constants';
+import { graphQLErrors } from '../mock_data';
jest.mock('~/repository/commits_service', () => ({
loadCommits: jest.fn(() => Promise.resolve()),
isRequested: jest.fn(),
resetRequestedCommits: jest.fn(),
}));
+jest.mock('~/flash');
let vm;
let $apollo;
+const mockResponse = jest.fn().mockReturnValue(Promise.resolve({ data: {} }));
-function factory(path, data = () => ({})) {
+function factory(path, appoloMockResponse = mockResponse) {
$apollo = {
- query: jest.fn().mockReturnValue(Promise.resolve({ data: data() })),
+ query: appoloMockResponse,
};
vm = shallowMount(TreeContent, {
@@ -222,4 +228,17 @@ describe('Repository table component', () => {
expect(loadCommits.mock.calls).toEqual([['', path, '', 0]]);
});
});
+
+ describe('error handling', () => {
+ const gitalyError = { graphQLErrors };
+ it.each`
+ error | message
+ ${gitalyError} | ${i18n.gitalyError}
+ ${'Error'} | ${i18n.generalError}
+ `('should show an expected error', async ({ error, message }) => {
+ factory('/', jest.fn().mockRejectedValue(error));
+ await waitForPromises();
+ expect(createAlert).toHaveBeenCalledWith({ message, captureError: true });
+ });
+ });
});
diff --git a/spec/frontend/repository/components/upload_blob_modal_spec.js b/spec/frontend/repository/components/upload_blob_modal_spec.js
index 8db169b02b4..9de0666f27a 100644
--- a/spec/frontend/repository/components/upload_blob_modal_spec.js
+++ b/spec/frontend/repository/components/upload_blob_modal_spec.js
@@ -5,7 +5,7 @@ import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import UploadBlobModal from '~/repository/components/upload_blob_modal.vue';
import UploadDropzone from '~/vue_shared/components/upload_dropzone/upload_dropzone.vue';
@@ -158,7 +158,7 @@ describe('UploadBlobModal', () => {
describe('successful response', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
- mock.onPost(initialProps.path).reply(httpStatusCodes.OK, { filePath: 'blah' });
+ mock.onPost(initialProps.path).reply(HTTP_STATUS_OK, { filePath: 'blah' });
findModal().vm.$emit('primary', mockEvent);
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index cda47a5b0a5..d85434a9148 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -87,6 +87,8 @@ export const applicationInfoMock = { gitpodEnabled: true };
export const propsMock = { path: 'some_file.js', projectPath: 'some/path' };
export const refMock = 'default-ref';
+export const refWithSpecialCharMock = 'feat/selected-#-ref-#';
+export const encodedRefWithSpecialCharMock = 'feat/selected-%23-ref-%23';
export const blobControlsDataMock = {
id: '1234',
@@ -106,3 +108,19 @@ export const blobControlsDataMock = {
},
},
};
+
+export const graphQLErrors = [
+ {
+ message: '14:failed to connect to all addresses.',
+ locations: [{ line: 16, column: 7 }],
+ path: ['project', 'repository', 'paginatedTree'],
+ extensions: { code: 'unavailable', gitaly_code: 14, service: 'git' },
+ },
+];
+
+export const propsForkInfo = {
+ projectPath: 'nataliia/myGitLab',
+ selectedRef: 'main',
+ sourceName: 'gitLab',
+ sourcePath: 'gitlab-org/gitlab',
+};
diff --git a/spec/frontend/repository/utils/ref_switcher_utils_spec.js b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
index 3335059554f..4d0250fffbf 100644
--- a/spec/frontend/repository/utils/ref_switcher_utils_spec.js
+++ b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
@@ -1,5 +1,6 @@
import { generateRefDestinationPath } from '~/repository/utils/ref_switcher_utils';
import setWindowLocation from 'helpers/set_window_location_helper';
+import { refWithSpecialCharMock, encodedRefWithSpecialCharMock } from '../mock_data';
const projectRootPath = 'root/Project1';
const currentRef = 'main';
@@ -19,4 +20,10 @@ describe('generateRefDestinationPath', () => {
setWindowLocation(currentPath);
expect(generateRefDestinationPath(projectRootPath, selectedRef)).toBe(result);
});
+
+ it('encodes the selected ref', () => {
+ const result = `${projectRootPath}/-/tree/${encodedRefWithSpecialCharMock}`;
+
+ expect(generateRefDestinationPath(projectRootPath, refWithSpecialCharMock)).toBe(result);
+ });
});
diff --git a/spec/frontend/search/store/utils_spec.js b/spec/frontend/search/store/utils_spec.js
index 20d764190b1..487ed7bfe03 100644
--- a/spec/frontend/search/store/utils_spec.js
+++ b/spec/frontend/search/store/utils_spec.js
@@ -5,7 +5,10 @@ import {
setFrequentItemToLS,
mergeById,
isSidebarDirty,
+ formatSearchResultCount,
+ getAggregationsUrl,
} from '~/search/store/utils';
+import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import {
MOCK_LS_KEY,
MOCK_GROUPS,
@@ -241,4 +244,23 @@ describe('Global Search Store Utils', () => {
});
});
});
+ describe('formatSearchResultCount', () => {
+ it('returns zero as string if no count is provided', () => {
+ expect(formatSearchResultCount()).toStrictEqual('0');
+ });
+ it('returns 10K string for 10000 integer', () => {
+ expect(formatSearchResultCount(10000)).toStrictEqual('10K');
+ });
+ it('returns 23K string for "23,000+" string', () => {
+ expect(formatSearchResultCount('23,000+')).toStrictEqual('23K');
+ });
+ });
+
+ describe('getAggregationsUrl', () => {
+ useMockLocationHelper();
+ it('returns zero as string if no count is provided', () => {
+ const testURL = window.location.href;
+ expect(getAggregationsUrl()).toStrictEqual(`${testURL}search/aggregations`);
+ });
+ });
});
diff --git a/spec/frontend/self_monitor/store/actions_spec.js b/spec/frontend/self_monitor/store/actions_spec.js
index 65c9d2f5f01..4c266fabea6 100644
--- a/spec/frontend/self_monitor/store/actions_spec.js
+++ b/spec/frontend/self_monitor/store/actions_spec.js
@@ -1,7 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import statusCodes, { HTTP_STATUS_ACCEPTED } from '~/lib/utils/http_status';
+import { HTTP_STATUS_ACCEPTED, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/self_monitor/store/actions';
import * as types from '~/self_monitor/store/mutation_types';
import createState from '~/self_monitor/store/state';
@@ -47,7 +47,7 @@ describe('self-monitor actions', () => {
mock.onPost(state.createProjectEndpoint).reply(HTTP_STATUS_ACCEPTED, {
job_id: '123',
});
- mock.onGet(state.createProjectStatusEndpoint).reply(statusCodes.OK, {
+ mock.onGet(state.createProjectStatusEndpoint).reply(HTTP_STATUS_OK, {
project_full_path: '/self-monitor-url',
});
});
@@ -154,7 +154,7 @@ describe('self-monitor actions', () => {
mock.onDelete(state.deleteProjectEndpoint).reply(HTTP_STATUS_ACCEPTED, {
job_id: '456',
});
- mock.onGet(state.deleteProjectStatusEndpoint).reply(statusCodes.OK, {
+ mock.onGet(state.deleteProjectStatusEndpoint).reply(HTTP_STATUS_OK, {
status: 'success',
});
});
diff --git a/spec/frontend/set_status_modal/set_status_form_spec.js b/spec/frontend/set_status_modal/set_status_form_spec.js
index 486e06d2906..df740d4a431 100644
--- a/spec/frontend/set_status_modal/set_status_form_spec.js
+++ b/spec/frontend/set_status_modal/set_status_form_spec.js
@@ -1,12 +1,15 @@
import $ from 'jquery';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { useFakeDate } from 'helpers/fake_date';
import SetStatusForm from '~/set_status_modal/set_status_form.vue';
+import { NEVER_TIME_RANGE } from '~/set_status_modal/constants';
import EmojiPicker from '~/emoji/components/picker.vue';
import { timeRanges } from '~/vue_shared/constants';
-import { sprintf } from '~/locale';
import GfmAutoComplete from 'ee_else_ce/gfm_auto_complete';
+const [thirtyMinutes, , , oneDay] = timeRanges;
+
describe('SetStatusForm', () => {
let wrapper;
@@ -73,17 +76,71 @@ describe('SetStatusForm', () => {
});
});
- describe('when clear status after is set', () => {
- it('displays value in dropdown toggle button', async () => {
- const clearStatusAfter = timeRanges[0];
+ describe('clear status after dropdown toggle button text', () => {
+ useFakeDate(2022, 11, 5);
- await createComponent({
- propsData: {
- clearStatusAfter,
- },
+ describe('when clear status after has previously been set', () => {
+ describe('when date is today', () => {
+ it('displays time that status will clear', async () => {
+ await createComponent({
+ propsData: {
+ currentClearStatusAfter: '2022-12-05 11:00:00 UTC',
+ },
+ });
+
+ expect(wrapper.findByRole('button', { name: '11:00am' }).exists()).toBe(true);
+ });
});
- expect(wrapper.findByRole('button', { name: clearStatusAfter.label }).exists()).toBe(true);
+ describe('when date is not today', () => {
+ it('displays date and time that status will clear', async () => {
+ await createComponent({
+ propsData: {
+ currentClearStatusAfter: '2022-12-06 11:00:00 UTC',
+ },
+ });
+
+ expect(wrapper.findByRole('button', { name: 'Dec 6, 2022 11:00am' }).exists()).toBe(true);
+ });
+ });
+
+ describe('when a new option is choose from the dropdown', () => {
+ describe('when chosen option is today', () => {
+ it('displays chosen option as time', async () => {
+ await createComponent({
+ propsData: {
+ clearStatusAfter: thirtyMinutes,
+ currentClearStatusAfter: '2022-12-05 11:00:00 UTC',
+ },
+ });
+
+ expect(wrapper.findByRole('button', { name: '12:30am' }).exists()).toBe(true);
+ });
+ });
+
+ describe('when chosen option is not today', () => {
+ it('displays chosen option as date and time', async () => {
+ await createComponent({
+ propsData: {
+ clearStatusAfter: oneDay,
+ currentClearStatusAfter: '2022-12-06 11:00:00 UTC',
+ },
+ });
+
+ expect(wrapper.findByRole('button', { name: 'Dec 6, 2022 12:00am' }).exists()).toBe(
+ true,
+ );
+ });
+ });
+ });
+ });
+
+ describe('when clear status after has not been set', () => {
+ it('displays `Never`', async () => {
+ await createComponent();
+
+ expect(wrapper.findByRole('button', { name: NEVER_TIME_RANGE.label }).exists()).toBe(true);
+ });
});
});
@@ -131,7 +188,7 @@ describe('SetStatusForm', () => {
await wrapper.findByTestId('thirtyMinutes').trigger('click');
- expect(wrapper.emitted('clear-status-after-click')).toEqual([[timeRanges[0]]]);
+ expect(wrapper.emitted('clear-status-after-click')).toEqual([[thirtyMinutes]]);
});
});
@@ -150,20 +207,4 @@ describe('SetStatusForm', () => {
expect(wrapper.findByTestId('no-emoji-placeholder').exists()).toBe(true);
});
});
-
- describe('when `currentClearStatusAfter` prop is set', () => {
- it('displays clear status message', async () => {
- const date = '2022-08-25 21:14:48 UTC';
-
- await createComponent({
- propsData: {
- currentClearStatusAfter: date,
- },
- });
-
- expect(
- wrapper.findByText(sprintf(SetStatusForm.i18n.clearStatusAfterMessage, { date })).exists(),
- ).toBe(true);
- });
- });
});
diff --git a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
index 53d2a9e0978..85cd8d51272 100644
--- a/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/set_status_modal_wrapper_spec.js
@@ -1,6 +1,7 @@
import { GlModal, GlFormCheckbox } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { useFakeDate } from 'helpers/fake_date';
import { initEmojiMock, clearEmojiMock } from 'helpers/emoji';
import * as UserApi from '~/api/user_api';
import EmojiPicker from '~/emoji/components/picker.vue';
@@ -56,7 +57,6 @@ describe('SetStatusModalWrapper', () => {
wrapper.findByPlaceholderText(SetStatusForm.i18n.statusMessagePlaceholder);
const findClearStatusButton = () => wrapper.find('.js-clear-user-status-button');
const findAvailabilityCheckbox = () => wrapper.findComponent(GlFormCheckbox);
- const findClearStatusAtMessage = () => wrapper.find('[data-testid="clear-status-at-message"]');
const getEmojiPicker = () => wrapper.findComponent(EmojiPickerStub);
const initModal = async ({ mockOnUpdateSuccess = true, mockOnUpdateFailure = true } = {}) => {
@@ -103,10 +103,6 @@ describe('SetStatusModalWrapper', () => {
expect(wrapper.find('[data-testid="clear-status-at-dropdown"]').exists()).toBe(true);
});
- it('does not display the clear status at message', () => {
- expect(findClearStatusAtMessage().exists()).toBe(false);
- });
-
it('renders emoji picker dropdown with custom positioning', () => {
expect(getEmojiPicker().props()).toMatchObject({
right: false,
@@ -138,17 +134,16 @@ describe('SetStatusModalWrapper', () => {
});
describe('with currentClearStatusAfter set', () => {
+ useFakeDate(2022, 11, 5);
+
beforeEach(async () => {
await initEmojiMock();
- wrapper = createComponent({ currentClearStatusAfter: '2021-01-01 00:00:00 UTC' });
+ wrapper = createComponent({ currentClearStatusAfter: '2022-12-06 11:00:00 UTC' });
return initModal();
});
- it('displays the clear status at message', () => {
- const clearStatusAtMessage = findClearStatusAtMessage();
-
- expect(clearStatusAtMessage.exists()).toBe(true);
- expect(clearStatusAtMessage.text()).toBe('Your status resets on 2021-01-01 00:00:00 UTC.');
+ it('displays date and time that status will expire in dropdown toggle button', () => {
+ expect(wrapper.findByRole('button', { name: 'Dec 6, 2022 11:00am' }).exists()).toBe(true);
});
});
@@ -170,33 +165,33 @@ describe('SetStatusModalWrapper', () => {
});
it('clicking "setStatus" submits the user status', async () => {
- findModal().vm.$emit('primary');
- await nextTick();
-
// set the availability status
findAvailabilityCheckbox().vm.$emit('input', true);
// set the currentClearStatusAfter to 30 minutes
- wrapper.find('[data-testid="thirtyMinutes"]').trigger('click');
+ await wrapper.find('[data-testid="thirtyMinutes"]').trigger('click');
findModal().vm.$emit('primary');
await nextTick();
- const commonParams = {
+ expect(UserApi.updateUserStatus).toHaveBeenCalledWith({
+ availability: AVAILABILITY_STATUS.BUSY,
+ clearStatusAfter: '30_minutes',
emoji: defaultEmoji,
message: defaultMessage,
- };
-
- expect(UserApi.updateUserStatus).toHaveBeenCalledTimes(2);
- expect(UserApi.updateUserStatus).toHaveBeenNthCalledWith(1, {
- availability: AVAILABILITY_STATUS.NOT_SET,
- clearStatusAfter: null,
- ...commonParams,
});
- expect(UserApi.updateUserStatus).toHaveBeenNthCalledWith(2, {
- availability: AVAILABILITY_STATUS.BUSY,
- clearStatusAfter: '30_minutes',
- ...commonParams,
+ });
+
+ describe('when `Clear status after` field has not been set', () => {
+ it('does not include `clearStatusAfter` in API request', async () => {
+ findModal().vm.$emit('primary');
+ await nextTick();
+
+ expect(UserApi.updateUserStatus).toHaveBeenCalledWith({
+ availability: AVAILABILITY_STATUS.NOT_SET,
+ emoji: defaultEmoji,
+ message: defaultMessage,
+ });
});
});
diff --git a/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js b/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js
index eaee0e77311..a4a2a86dc73 100644
--- a/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js
+++ b/spec/frontend/set_status_modal/user_profile_set_status_wrapper_spec.js
@@ -1,8 +1,6 @@
import { nextTick } from 'vue';
import { cloneDeep } from 'lodash';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { resetHTMLFixture } from 'helpers/fixtures';
-import { useFakeDate } from 'helpers/fake_date';
import UserProfileSetStatusWrapper from '~/set_status_modal/user_profile_set_status_wrapper.vue';
import SetStatusForm from '~/set_status_modal/set_status_form.vue';
import { TIME_RANGES_WITH_NEVER, NEVER_TIME_RANGE } from '~/set_status_modal/constants';
@@ -51,7 +49,7 @@ describe('UserProfileSetStatusWrapper', () => {
emoji: defaultProvide.fields.emoji.value,
message: defaultProvide.fields.message.value,
availability: true,
- clearStatusAfter: NEVER_TIME_RANGE,
+ clearStatusAfter: null,
currentClearStatusAfter: defaultProvide.fields.clearStatusAfter.value,
});
});
@@ -69,27 +67,41 @@ describe('UserProfileSetStatusWrapper', () => {
);
});
- describe('when clear status after dropdown is set to `Never`', () => {
- it('renders hidden clear status after input with value unset', () => {
- createComponent();
+ describe('when clear status after has previously been set', () => {
+ describe('when clear status after dropdown is not set', () => {
+ it('does not render hidden clear status after input', () => {
+ createComponent();
- expect(
- findInput(defaultProvide.fields.clearStatusAfter.name).attributes('value'),
- ).toBeUndefined();
+ expect(findInput(defaultProvide.fields.clearStatusAfter.name).exists()).toBe(false);
+ });
});
- });
- describe('when clear status after dropdown has a value selected', () => {
- it('renders hidden clear status after input with value set', async () => {
- createComponent();
+ describe('when clear status after dropdown is set to `Never`', () => {
+ it('renders hidden clear status after input with value unset', async () => {
+ createComponent();
- findSetStatusForm().vm.$emit('clear-status-after-click', TIME_RANGES_WITH_NEVER[1]);
+ findSetStatusForm().vm.$emit('clear-status-after-click', NEVER_TIME_RANGE);
- await nextTick();
+ await nextTick();
- expect(findInput(defaultProvide.fields.clearStatusAfter.name).attributes('value')).toBe(
- TIME_RANGES_WITH_NEVER[1].shortcut,
- );
+ expect(
+ findInput(defaultProvide.fields.clearStatusAfter.name).attributes('value'),
+ ).toBeUndefined();
+ });
+ });
+
+ describe('when clear status after dropdown is set to a time range', () => {
+ it('renders hidden clear status after input with value set', async () => {
+ createComponent();
+
+ findSetStatusForm().vm.$emit('clear-status-after-click', TIME_RANGES_WITH_NEVER[1]);
+
+ await nextTick();
+
+ expect(findInput(defaultProvide.fields.clearStatusAfter.name).attributes('value')).toBe(
+ TIME_RANGES_WITH_NEVER[1].shortcut,
+ );
+ });
});
});
@@ -120,37 +132,4 @@ describe('UserProfileSetStatusWrapper', () => {
expect(findInput(defaultProvide.fields.message.name).attributes('value')).toBe(newMessage);
});
});
-
- describe('when form is successfully submitted', () => {
- // 2022-09-02 00:00:00 UTC
- useFakeDate(2022, 8, 2);
-
- const form = document.createElement('form');
- form.classList.add('js-edit-user');
-
- beforeEach(async () => {
- document.body.appendChild(form);
- createComponent();
-
- const oneDay = TIME_RANGES_WITH_NEVER[4];
-
- findSetStatusForm().vm.$emit('clear-status-after-click', oneDay);
-
- await nextTick();
-
- form.dispatchEvent(new Event('ajax:success'));
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- it('updates clear status after dropdown to `Never`', () => {
- expect(findSetStatusForm().props('clearStatusAfter')).toBe(NEVER_TIME_RANGE);
- });
-
- it('updates `currentClearStatusAfter` prop', () => {
- expect(findSetStatusForm().props('currentClearStatusAfter')).toBe('2022-09-03 00:00:00 UTC');
- });
- });
});
diff --git a/spec/frontend/set_status_modal/utils_spec.js b/spec/frontend/set_status_modal/utils_spec.js
index 1e918b75a98..a1c899be900 100644
--- a/spec/frontend/set_status_modal/utils_spec.js
+++ b/spec/frontend/set_status_modal/utils_spec.js
@@ -1,5 +1,8 @@
-import { isUserBusy } from '~/set_status_modal/utils';
-import { AVAILABILITY_STATUS } from '~/set_status_modal/constants';
+import { isUserBusy, computedClearStatusAfterValue } from '~/set_status_modal/utils';
+import { AVAILABILITY_STATUS, NEVER_TIME_RANGE } from '~/set_status_modal/constants';
+import { timeRanges } from '~/vue_shared/constants';
+
+const [thirtyMinutes] = timeRanges;
describe('Set status modal utils', () => {
describe('isUserBusy', () => {
@@ -13,4 +16,15 @@ describe('Set status modal utils', () => {
expect(isUserBusy(value)).toBe(result);
});
});
+
+ describe('computedClearStatusAfterValue', () => {
+ it.each`
+ value | expected
+ ${null} | ${null}
+ ${NEVER_TIME_RANGE} | ${null}
+ ${thirtyMinutes} | ${thirtyMinutes.shortcut}
+ `('with $value returns $expected', ({ value, expected }) => {
+ expect(computedClearStatusAfterValue(value)).toBe(expected);
+ });
+ });
});
diff --git a/spec/frontend/sidebar/components/assignees/assignees_spec.js b/spec/frontend/sidebar/components/assignees/assignees_spec.js
index 6971ae2f9ed..d422292ed9e 100644
--- a/spec/frontend/sidebar/components/assignees/assignees_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignees_spec.js
@@ -1,10 +1,10 @@
import { GlIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
import { trimText } from 'helpers/text_helper';
import UsersMockHelper from 'helpers/user_mock_data_helper';
import Assignee from '~/sidebar/components/assignees/assignees.vue';
import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
+import CollapsedAssigneeList from '~/sidebar/components/assignees/collapsed_assignee_list.vue';
import UsersMock from '../../mock_data';
describe('Assignee component', () => {
@@ -66,10 +66,8 @@ describe('Assignee component', () => {
editable: true,
});
- jest.spyOn(wrapper.vm, '$emit');
- wrapper.find('[data-testid="assign-yourself"]').trigger('click');
+ await wrapper.find('[data-testid="assign-yourself"]').trigger('click');
- await nextTick();
expect(wrapper.emitted('assign-self')).toHaveLength(1);
});
});
@@ -166,7 +164,11 @@ describe('Assignee component', () => {
editable: true,
});
- expect(wrapper.vm.sortedAssigness[0].can_merge).toBe(true);
+ expect(wrapper.findComponent(CollapsedAssigneeList).props('users')[0]).toEqual(
+ expect.objectContaining({
+ can_merge: true,
+ }),
+ );
});
it('passes the sorted assignees to the uncollapsed-assignee-list', () => {
diff --git a/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
index c5161a748a9..40f14d581dc 100644
--- a/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
+++ b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
@@ -66,7 +66,7 @@ describe('Sidebar Reference Widget', () => {
});
describe('when error occurs', () => {
- it('calls createFlash with correct parameters', async () => {
+ it(`emits 'fetch-error' event with correct parameters`, async () => {
const mockError = new Error('mayday');
createComponent({
diff --git a/spec/frontend/super_sidebar/components/counter_spec.js b/spec/frontend/super_sidebar/components/counter_spec.js
new file mode 100644
index 00000000000..1150b0a3aa8
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/counter_spec.js
@@ -0,0 +1,56 @@
+import { GlIcon } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { __ } from '~/locale';
+import Counter from '~/super_sidebar/components/counter.vue';
+
+describe('Counter component', () => {
+ let wrapper;
+
+ const defaultPropsData = {
+ count: 3,
+ href: '',
+ icon: 'issues',
+ label: __('Issues'),
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findButton = () => wrapper.find('button');
+ const findIcon = () => wrapper.getComponent(GlIcon);
+ const findLink = () => wrapper.find('a');
+
+ const createWrapper = (props = {}) => {
+ wrapper = shallowMountExtended(Counter, {
+ propsData: {
+ ...defaultPropsData,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ describe('default', () => {
+ it('renders icon', () => {
+ expect(findIcon().props('name')).toBe('issues');
+ });
+
+ it('renders button', () => {
+ expect(findButton().attributes('aria-label')).toBe('Issues 3');
+ expect(findLink().exists()).toBe(false);
+ });
+ });
+
+ describe('link', () => {
+ it('renders link', () => {
+ createWrapper({ href: '/dashboard/todos' });
+ expect(findLink().attributes('aria-label')).toBe('Issues 3');
+ expect(findLink().attributes('href')).toBe('/dashboard/todos');
+ expect(findButton().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
new file mode 100644
index 00000000000..d7d2f67dc8a
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -0,0 +1,33 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue';
+import UserBar from '~/super_sidebar/components/user_bar.vue';
+import { sidebarData } from '../mock_data';
+
+describe('SuperSidebar component', () => {
+ let wrapper;
+
+ const findUserBar = () => wrapper.findComponent(UserBar);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const createWrapper = (props = {}) => {
+ wrapper = shallowMountExtended(SuperSidebar, {
+ propsData: {
+ sidebarData,
+ ...props,
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('renders UserBar with sidebarData', () => {
+ expect(findUserBar().props('sidebarData')).toBe(sidebarData);
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/user_bar_spec.js b/spec/frontend/super_sidebar/components/user_bar_spec.js
new file mode 100644
index 00000000000..6d0186a2749
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -0,0 +1,46 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { __ } from '~/locale';
+import Counter from '~/super_sidebar/components/counter.vue';
+import UserBar from '~/super_sidebar/components/user_bar.vue';
+import { sidebarData } from '../mock_data';
+
+describe('UserBar component', () => {
+ let wrapper;
+
+ const findCounter = (at) => wrapper.findAllComponents(Counter).at(at);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const createWrapper = (props = {}) => {
+ wrapper = shallowMountExtended(UserBar, {
+ propsData: {
+ sidebarData,
+ ...props,
+ },
+ provide: {
+ rootPath: '/',
+ toggleNewNavEndpoint: '/-/profile/preferences',
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('renders issues counter', () => {
+ expect(findCounter(0).props('count')).toBe(sidebarData.assigned_open_issues_count);
+ expect(findCounter(0).props('href')).toBe(sidebarData.issues_dashboard_path);
+ expect(findCounter(0).props('label')).toBe(__('Issues'));
+ });
+
+ it('renders todos counter', () => {
+ expect(findCounter(2).props('count')).toBe(sidebarData.todos_pending_count);
+ expect(findCounter(2).props('href')).toBe('/dashboard/todos');
+ expect(findCounter(2).props('label')).toBe(__('To-Do list'));
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
new file mode 100644
index 00000000000..7db0d0ea5cc
--- /dev/null
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -0,0 +1,9 @@
+export const sidebarData = {
+ name: 'Administrator',
+ username: 'root',
+ avatar_url: 'path/to/img_administrator',
+ assigned_open_issues_count: 1,
+ assigned_open_merge_requests_count: 2,
+ todos_pending_count: 3,
+ issues_dashboard_path: 'path/to/issues',
+};
diff --git a/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js b/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js
new file mode 100644
index 00000000000..3379af3f41c
--- /dev/null
+++ b/spec/frontend/usage_quotas/storage/components/project_storage_app_spec.js
@@ -0,0 +1,150 @@
+import { GlAlert, 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 { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import ProjectStorageApp from '~/usage_quotas/storage/components/project_storage_app.vue';
+import UsageGraph from '~/usage_quotas/storage/components/usage_graph.vue';
+import { TOTAL_USAGE_DEFAULT_TEXT } from '~/usage_quotas/storage/constants';
+import getProjectStorageStatistics from '~/usage_quotas/storage/queries/project_storage.query.graphql';
+import {
+ projectData,
+ mockGetProjectStorageStatisticsGraphQLResponse,
+ mockEmptyResponse,
+ defaultProjectProvideValues,
+} from '../mock_data';
+
+Vue.use(VueApollo);
+
+describe('ProjectStorageApp', () => {
+ let wrapper;
+
+ const createMockApolloProvider = ({ reject = false, mockedValue } = {}) => {
+ let response;
+
+ if (reject) {
+ response = jest.fn().mockRejectedValue(mockedValue || new Error('GraphQL error'));
+ } else {
+ response = jest.fn().mockResolvedValue(mockedValue);
+ }
+
+ const requestHandlers = [[getProjectStorageStatistics, response]];
+
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = ({ provide = {}, mockApollo } = {}) => {
+ wrapper = extendedWrapper(
+ shallowMount(ProjectStorageApp, {
+ apolloProvider: mockApollo,
+ provide: {
+ ...defaultProjectProvideValues,
+ ...provide,
+ },
+ }),
+ );
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findUsagePercentage = () => wrapper.findByTestId('total-usage');
+ const findUsageQuotasHelpLink = () => wrapper.findByTestId('usage-quotas-help-link');
+ const findUsageGraph = () => wrapper.findComponent(UsageGraph);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('with apollo fetching successful', () => {
+ let mockApollo;
+
+ beforeEach(async () => {
+ mockApollo = createMockApolloProvider({
+ mockedValue: mockGetProjectStorageStatisticsGraphQLResponse,
+ });
+ createComponent({ mockApollo });
+ await waitForPromises();
+ });
+
+ it('renders correct total usage', () => {
+ expect(findUsagePercentage().text()).toBe(projectData.storage.totalUsage);
+ });
+
+ it('renders correct usage quotas help link', () => {
+ expect(findUsageQuotasHelpLink().attributes('href')).toBe(
+ defaultProjectProvideValues.helpLinks.usageQuotas,
+ );
+ });
+ });
+
+ describe('with apollo loading', () => {
+ let mockApollo;
+
+ beforeEach(() => {
+ mockApollo = createMockApolloProvider({
+ mockedValue: new Promise(() => {}),
+ });
+ createComponent({ mockApollo });
+ });
+
+ it('should show loading icon', () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ });
+ });
+
+ describe('with apollo returning empty data', () => {
+ let mockApollo;
+
+ beforeEach(async () => {
+ mockApollo = createMockApolloProvider({
+ mockedValue: mockEmptyResponse,
+ });
+ createComponent({ mockApollo });
+ await waitForPromises();
+ });
+
+ it('shows default text for total usage', () => {
+ expect(findUsagePercentage().text()).toBe(TOTAL_USAGE_DEFAULT_TEXT);
+ });
+ });
+
+ describe('with apollo fetching error', () => {
+ let mockApollo;
+
+ beforeEach(async () => {
+ mockApollo = createMockApolloProvider();
+ createComponent({ mockApollo, reject: true });
+ await waitForPromises();
+ });
+
+ it('renders gl-alert', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+ });
+
+ describe('rendering <usage-graph />', () => {
+ let mockApollo;
+
+ beforeEach(async () => {
+ mockApollo = createMockApolloProvider({
+ mockedValue: mockGetProjectStorageStatisticsGraphQLResponse,
+ });
+ createComponent({ mockApollo });
+ await waitForPromises();
+ });
+
+ it('renders usage-graph component if project.statistics exists', () => {
+ expect(findUsageGraph().exists()).toBe(true);
+ });
+
+ it('passes project.statistics to usage-graph component', () => {
+ const {
+ __typename,
+ ...statistics
+ } = mockGetProjectStorageStatisticsGraphQLResponse.data.project.statistics;
+ expect(findUsageGraph().props('rootStorageStatistics')).toMatchObject(statistics);
+ });
+ });
+});
diff --git a/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js
new file mode 100644
index 00000000000..ce489f69cad
--- /dev/null
+++ b/spec/frontend/usage_quotas/storage/components/project_storage_detail_spec.js
@@ -0,0 +1,129 @@
+import { GlTableLite, GlPopover } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import ProjectStorageDetail from '~/usage_quotas/storage/components/project_storage_detail.vue';
+import {
+ containerRegistryPopoverId,
+ containerRegistryId,
+ uploadsPopoverId,
+ uploadsId,
+} from '~/usage_quotas/storage/constants';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import { projectData, projectHelpLinks } from '../mock_data';
+
+describe('ProjectStorageDetail', () => {
+ let wrapper;
+
+ const { storageTypes } = projectData.storage;
+ const defaultProps = { storageTypes };
+
+ const createComponent = (props = {}) => {
+ wrapper = extendedWrapper(
+ mount(ProjectStorageDetail, {
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
+ provide: {
+ containerRegistryPopoverContent: 'Sample popover message',
+ },
+ }),
+ );
+ };
+
+ const generateStorageType = (id = 'buildArtifactsSize') => {
+ return {
+ storageType: {
+ id,
+ name: 'Test Name',
+ description: 'Test Description',
+ helpPath: '/test-type',
+ },
+ value: 400000,
+ };
+ };
+
+ const findTable = () => wrapper.findComponent(GlTableLite);
+ const findPopoverById = (id) =>
+ wrapper.findAllComponents(GlPopover).filter((p) => p.attributes('data-testid') === id);
+ const findContainerRegistryPopover = () => findPopoverById(containerRegistryPopoverId);
+ const findUploadsPopover = () => findPopoverById(uploadsPopoverId);
+ const findContainerRegistryWarningIcon = () => wrapper.find(`#${containerRegistryPopoverId}`);
+ const findUploadsWarningIcon = () => wrapper.find(`#${uploadsPopoverId}`);
+
+ beforeEach(() => {
+ createComponent();
+ });
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('with storage types', () => {
+ it.each(storageTypes)(
+ 'renders table row correctly %o',
+ ({ storageType: { id, name, description } }) => {
+ expect(wrapper.findByTestId(`${id}-name`).text()).toBe(name);
+ expect(wrapper.findByTestId(`${id}-description`).text()).toBe(description);
+ expect(wrapper.findByTestId(`${id}-icon`).props('name')).toBe(id);
+ expect(wrapper.findByTestId(`${id}-help-link`).attributes('href')).toBe(
+ projectHelpLinks[id.replace(`Size`, ``)],
+ );
+ },
+ );
+
+ it('should render items in order from the biggest usage size to the smallest', () => {
+ const rows = findTable().find('tbody').findAll('tr');
+ // Cloning array not to mutate the source
+ const sortedStorageTypes = [...storageTypes].sort((a, b) => b.value - a.value);
+
+ sortedStorageTypes.forEach((storageType, i) => {
+ const rowUsageAmount = rows.wrappers[i].find('td:last-child').text();
+ const expectedUsageAmount = numberToHumanSize(storageType.value, 1);
+ expect(rowUsageAmount).toBe(expectedUsageAmount);
+ });
+ });
+ });
+
+ describe('without storage types', () => {
+ beforeEach(() => {
+ createComponent({ storageTypes: [] });
+ });
+
+ it('should render the table header <th>', () => {
+ expect(findTable().find('th').exists()).toBe(true);
+ });
+
+ it('should not render any table data <td>', () => {
+ expect(findTable().find('td').exists()).toBe(false);
+ });
+ });
+
+ describe.each`
+ description | mockStorageTypes | rendersContainerRegistryPopover | rendersUploadsPopover
+ ${'without any storage type that has popover'} | ${[generateStorageType()]} | ${false} | ${false}
+ ${'with container registry storage type'} | ${[generateStorageType(containerRegistryId)]} | ${true} | ${false}
+ ${'with uploads storage type'} | ${[generateStorageType(uploadsId)]} | ${false} | ${true}
+ ${'with container registry and uploads storage types'} | ${[generateStorageType(containerRegistryId), generateStorageType(uploadsId)]} | ${true} | ${true}
+ `(
+ '$description',
+ ({ mockStorageTypes, rendersContainerRegistryPopover, rendersUploadsPopover }) => {
+ beforeEach(() => {
+ createComponent({ storageTypes: mockStorageTypes });
+ });
+
+ it(`does ${
+ rendersContainerRegistryPopover ? '' : ' not'
+ } render container registry warning icon and popover`, () => {
+ expect(findContainerRegistryWarningIcon().exists()).toBe(rendersContainerRegistryPopover);
+ expect(findContainerRegistryPopover().exists()).toBe(rendersContainerRegistryPopover);
+ });
+
+ it(`does ${
+ rendersUploadsPopover ? '' : ' not'
+ } render container registry warning icon and popover`, () => {
+ expect(findUploadsWarningIcon().exists()).toBe(rendersUploadsPopover);
+ expect(findUploadsPopover().exists()).toBe(rendersUploadsPopover);
+ });
+ },
+ );
+});
diff --git a/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js b/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js
new file mode 100644
index 00000000000..1eb3386bfb8
--- /dev/null
+++ b/spec/frontend/usage_quotas/storage/components/storage_type_icon_spec.js
@@ -0,0 +1,41 @@
+import { mount } from '@vue/test-utils';
+import { GlIcon } from '@gitlab/ui';
+import StorageTypeIcon from '~/usage_quotas/storage/components/storage_type_icon.vue';
+
+describe('StorageTypeIcon', () => {
+ let wrapper;
+
+ const createComponent = (props = {}) => {
+ wrapper = mount(StorageTypeIcon, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ const findGlIcon = () => wrapper.findComponent(GlIcon);
+
+ describe('rendering icon', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it.each`
+ expected | provided
+ ${'doc-image'} | ${'lfsObjectsSize'}
+ ${'snippet'} | ${'snippetsSize'}
+ ${'infrastructure-registry'} | ${'repositorySize'}
+ ${'package'} | ${'packagesSize'}
+ ${'upload'} | ${'uploadsSize'}
+ ${'disk'} | ${'wikiSize'}
+ ${'disk'} | ${'anything-else'}
+ `(
+ 'renders icon with name of $expected when name prop is $provided',
+ ({ expected, provided }) => {
+ createComponent({ name: provided });
+
+ expect(findGlIcon().props('name')).toBe(expected);
+ },
+ );
+ });
+});
diff --git a/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
new file mode 100644
index 00000000000..75b970d937a
--- /dev/null
+++ b/spec/frontend/usage_quotas/storage/components/usage_graph_spec.js
@@ -0,0 +1,144 @@
+import { shallowMount } from '@vue/test-utils';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import UsageGraph from '~/usage_quotas/storage/components/usage_graph.vue';
+
+let data;
+let wrapper;
+
+function mountComponent({ rootStorageStatistics, limit }) {
+ wrapper = shallowMount(UsageGraph, {
+ propsData: {
+ rootStorageStatistics,
+ limit,
+ },
+ });
+}
+function findStorageTypeUsagesSerialized() {
+ return wrapper
+ .findAll('[data-testid="storage-type-usage"]')
+ .wrappers.map((wp) => wp.element.style.flex);
+}
+
+describe('Storage Counter usage graph component', () => {
+ beforeEach(() => {
+ data = {
+ rootStorageStatistics: {
+ wikiSize: 5000,
+ repositorySize: 4000,
+ packagesSize: 3000,
+ containerRegistrySize: 2500,
+ lfsObjectsSize: 2000,
+ buildArtifactsSize: 500,
+ pipelineArtifactsSize: 500,
+ snippetsSize: 2000,
+ storageSize: 17000,
+ uploadsSize: 1000,
+ },
+ limit: 2000,
+ };
+ mountComponent(data);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders the legend in order', () => {
+ const types = wrapper.findAll('[data-testid="storage-type-legend"]');
+
+ const {
+ buildArtifactsSize,
+ pipelineArtifactsSize,
+ lfsObjectsSize,
+ packagesSize,
+ containerRegistrySize,
+ repositorySize,
+ wikiSize,
+ snippetsSize,
+ uploadsSize,
+ } = data.rootStorageStatistics;
+
+ expect(types.at(0).text()).toMatchInterpolatedText(`Wiki ${numberToHumanSize(wikiSize)}`);
+ expect(types.at(1).text()).toMatchInterpolatedText(
+ `Repository ${numberToHumanSize(repositorySize)}`,
+ );
+ expect(types.at(2).text()).toMatchInterpolatedText(
+ `Packages ${numberToHumanSize(packagesSize)}`,
+ );
+ expect(types.at(3).text()).toMatchInterpolatedText(
+ `Container Registry ${numberToHumanSize(containerRegistrySize)}`,
+ );
+ expect(types.at(4).text()).toMatchInterpolatedText(
+ `LFS storage ${numberToHumanSize(lfsObjectsSize)}`,
+ );
+ expect(types.at(5).text()).toMatchInterpolatedText(
+ `Snippets ${numberToHumanSize(snippetsSize)}`,
+ );
+ expect(types.at(6).text()).toMatchInterpolatedText(
+ `Artifacts ${numberToHumanSize(buildArtifactsSize + pipelineArtifactsSize)}`,
+ );
+ expect(types.at(7).text()).toMatchInterpolatedText(`Uploads ${numberToHumanSize(uploadsSize)}`);
+ });
+
+ describe('when storage type is not used', () => {
+ beforeEach(() => {
+ data.rootStorageStatistics.wikiSize = 0;
+ mountComponent(data);
+ });
+
+ it('filters the storage type', () => {
+ expect(wrapper.text()).not.toContain('Wikis');
+ });
+ });
+
+ describe('when there is no storage usage', () => {
+ beforeEach(() => {
+ data.rootStorageStatistics.storageSize = 0;
+ mountComponent(data);
+ });
+
+ it('does not render', () => {
+ expect(wrapper.html()).toEqual('');
+ });
+ });
+
+ describe('when limit is 0', () => {
+ beforeEach(() => {
+ data.limit = 0;
+ mountComponent(data);
+ });
+
+ it('sets correct flex values', () => {
+ expect(findStorageTypeUsagesSerialized()).toStrictEqual([
+ '0.29411764705882354',
+ '0.23529411764705882',
+ '0.17647058823529413',
+ '0.14705882352941177',
+ '0.11764705882352941',
+ '0.11764705882352941',
+ '0.058823529411764705',
+ '0.058823529411764705',
+ ]);
+ });
+ });
+
+ describe('when storage exceeds limit', () => {
+ beforeEach(() => {
+ data.limit = data.rootStorageStatistics.storageSize - 1;
+ mountComponent(data);
+ });
+
+ it('does render correclty', () => {
+ expect(findStorageTypeUsagesSerialized()).toStrictEqual([
+ '0.29411764705882354',
+ '0.23529411764705882',
+ '0.17647058823529413',
+ '0.14705882352941177',
+ '0.11764705882352941',
+ '0.11764705882352941',
+ '0.058823529411764705',
+ '0.058823529411764705',
+ ]);
+ });
+ });
+});
diff --git a/spec/frontend/usage_quotas/storage/mock_data.js b/spec/frontend/usage_quotas/storage/mock_data.js
new file mode 100644
index 00000000000..b1c6be10d80
--- /dev/null
+++ b/spec/frontend/usage_quotas/storage/mock_data.js
@@ -0,0 +1,101 @@
+import mockGetProjectStorageStatisticsGraphQLResponse from 'test_fixtures/graphql/usage_quotas/storage/project_storage.query.graphql.json';
+
+export { mockGetProjectStorageStatisticsGraphQLResponse };
+export const mockEmptyResponse = { data: { project: null } };
+
+export const projectData = {
+ storage: {
+ totalUsage: '13.8 MiB',
+ storageTypes: [
+ {
+ storageType: {
+ id: 'containerRegistrySize',
+ name: 'Container Registry',
+ description: 'Gitlab-integrated Docker Container Registry for storing Docker Images.',
+ helpPath: '/container_registry',
+ },
+ value: 3_900_000,
+ },
+ {
+ storageType: {
+ id: 'buildArtifactsSize',
+ name: 'Artifacts',
+ description: 'Pipeline artifacts and job artifacts, created with CI/CD.',
+ helpPath: '/build-artifacts',
+ },
+ value: 400000,
+ },
+ {
+ storageType: {
+ id: 'lfsObjectsSize',
+ name: 'LFS storage',
+ description: 'Audio samples, videos, datasets, and graphics.',
+ helpPath: '/lsf-objects',
+ },
+ value: 4800000,
+ },
+ {
+ storageType: {
+ id: 'packagesSize',
+ name: 'Packages',
+ description: 'Code packages and container images.',
+ helpPath: '/packages',
+ },
+ value: 3800000,
+ },
+ {
+ storageType: {
+ id: 'repositorySize',
+ name: 'Repository',
+ description: 'Git repository.',
+ helpPath: '/repository',
+ },
+ value: 3900000,
+ },
+ {
+ storageType: {
+ id: 'snippetsSize',
+ name: 'Snippets',
+ description: 'Shared bits of code and text.',
+ helpPath: '/snippets',
+ },
+ value: 0,
+ },
+ {
+ storageType: {
+ id: 'uploadsSize',
+ name: 'Uploads',
+ description: 'File attachments and smaller design graphics.',
+ helpPath: '/uploads',
+ },
+ value: 900000,
+ },
+ {
+ storageType: {
+ id: 'wikiSize',
+ name: 'Wiki',
+ description: 'Wiki content.',
+ helpPath: '/wiki',
+ },
+ value: 300000,
+ },
+ ],
+ },
+};
+
+export const projectHelpLinks = {
+ containerRegistry: '/container_registry',
+ usageQuotas: '/usage-quotas',
+ buildArtifacts: '/build-artifacts',
+ lfsObjects: '/lsf-objects',
+ packages: '/packages',
+ repository: '/repository',
+ snippets: '/snippets',
+ uploads: '/uploads',
+ wiki: '/wiki',
+};
+
+export const defaultProjectProvideValues = {
+ projectPath: '/project-path',
+ helpLinks: projectHelpLinks,
+};
diff --git a/spec/frontend/usage_quotas/storage/utils_spec.js b/spec/frontend/usage_quotas/storage/utils_spec.js
new file mode 100644
index 00000000000..8fdd307c008
--- /dev/null
+++ b/spec/frontend/usage_quotas/storage/utils_spec.js
@@ -0,0 +1,88 @@
+import cloneDeep from 'lodash/cloneDeep';
+import { PROJECT_STORAGE_TYPES } from '~/usage_quotas/storage/constants';
+import {
+ parseGetProjectStorageResults,
+ getStorageTypesFromProjectStatistics,
+ descendingStorageUsageSort,
+} from '~/usage_quotas/storage/utils';
+import {
+ mockGetProjectStorageStatisticsGraphQLResponse,
+ defaultProjectProvideValues,
+ projectData,
+} from './mock_data';
+
+describe('getStorageTypesFromProjectStatistics', () => {
+ const projectStatistics = mockGetProjectStorageStatisticsGraphQLResponse.data.project.statistics;
+
+ describe('matches project statistics value with matching storage type', () => {
+ const typesWithStats = getStorageTypesFromProjectStatistics(projectStatistics);
+
+ it.each(PROJECT_STORAGE_TYPES)('storage type: $id', ({ id }) => {
+ expect(typesWithStats).toContainEqual({
+ storageType: expect.objectContaining({
+ id,
+ }),
+ value: projectStatistics[id],
+ });
+ });
+ });
+
+ it('adds helpPath to a relevant type', () => {
+ const trimTypeId = (id) => id.replace('Size', '');
+ const helpLinks = PROJECT_STORAGE_TYPES.reduce((acc, { id }) => {
+ const key = trimTypeId(id);
+ return {
+ ...acc,
+ [key]: `url://${id}`,
+ };
+ }, {});
+
+ const typesWithStats = getStorageTypesFromProjectStatistics(projectStatistics, helpLinks);
+
+ typesWithStats.forEach((type) => {
+ const key = trimTypeId(type.storageType.id);
+ expect(type.storageType.helpPath).toBe(helpLinks[key]);
+ });
+ });
+});
+describe('parseGetProjectStorageResults', () => {
+ it('parses project statistics correctly', () => {
+ expect(
+ parseGetProjectStorageResults(
+ mockGetProjectStorageStatisticsGraphQLResponse.data,
+ defaultProjectProvideValues.helpLinks,
+ ),
+ ).toMatchObject(projectData);
+ });
+
+ it('includes storage type with size of 0 in returned value', () => {
+ const mockedResponse = cloneDeep(mockGetProjectStorageStatisticsGraphQLResponse.data);
+ // ensuring a specific storage type item has size of 0
+ mockedResponse.project.statistics.repositorySize = 0;
+
+ const response = parseGetProjectStorageResults(
+ mockedResponse,
+ defaultProjectProvideValues.helpLinks,
+ );
+
+ expect(response.storage.storageTypes).toEqual(
+ expect.arrayContaining([
+ {
+ storageType: expect.any(Object),
+ value: 0,
+ },
+ ]),
+ );
+ });
+});
+
+describe('descendingStorageUsageSort', () => {
+ it('sorts items by a given key in descending order', () => {
+ const items = [{ k: 1 }, { k: 3 }, { k: 2 }];
+
+ const sorted = [...items].sort(descendingStorageUsageSort('k'));
+
+ const expectedSorted = [{ k: 3 }, { k: 2 }, { k: 1 }];
+ expect(sorted).toEqual(expectedSorted);
+ });
+});
diff --git a/spec/frontend/users/profile/components/report_abuse_button_spec.js b/spec/frontend/users/profile/components/report_abuse_button_spec.js
new file mode 100644
index 00000000000..7ad28566f49
--- /dev/null
+++ b/spec/frontend/users/profile/components/report_abuse_button_spec.js
@@ -0,0 +1,79 @@
+import { GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
+
+import ReportAbuseButton from '~/users/profile/components/report_abuse_button.vue';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
+
+describe('ReportAbuseButton', () => {
+ let wrapper;
+
+ const ACTION_PATH = '/abuse_reports/add_category';
+ const USER_ID = '1';
+ const REPORTED_FROM_URL = 'http://example.com';
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(ReportAbuseButton, {
+ propsData: {
+ ...props,
+ },
+ provide: {
+ reportAbusePath: ACTION_PATH,
+ reportedUserId: USER_ID,
+ reportedFromUrl: REPORTED_FROM_URL,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findReportAbuseButton = () => wrapper.findComponent(GlButton);
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+
+ it('renders report abuse button', () => {
+ expect(findReportAbuseButton().exists()).toBe(true);
+
+ expect(findReportAbuseButton().props()).toMatchObject({
+ category: 'primary',
+ icon: 'error',
+ });
+
+ expect(findReportAbuseButton().attributes('aria-label')).toBe(
+ wrapper.vm.$options.i18n.reportAbuse,
+ );
+ });
+
+ it('renders abuse category selector with the drawer initially closed', () => {
+ expect(findAbuseCategorySelector().exists()).toBe(true);
+
+ expect(findAbuseCategorySelector().props('showDrawer')).toBe(false);
+ });
+
+ describe('when button is clicked', () => {
+ beforeEach(async () => {
+ await findReportAbuseButton().vm.$emit('click');
+ });
+
+ it('opens the abuse category selector', () => {
+ expect(findAbuseCategorySelector().props('showDrawer')).toBe(true);
+ });
+
+ it('closes the abuse category selector', async () => {
+ await findAbuseCategorySelector().vm.$emit('close-drawer');
+
+ expect(findAbuseCategorySelector().props('showDrawer')).toBe(false);
+ });
+ });
+
+ describe('when user hovers out of the button', () => {
+ it(`should emit ${BV_HIDE_TOOLTIP} to close the tooltip`, () => {
+ jest.spyOn(wrapper.vm.$root, '$emit');
+
+ findReportAbuseButton().vm.$emit('mouseout');
+
+ expect(wrapper.vm.$root.$emit).toHaveBeenCalledWith(BV_HIDE_TOOLTIP);
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
index c253dc63f23..81f266d8070 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_collapsible_extension_spec.js
@@ -42,8 +42,8 @@ describe('Merge Request Collapsible Extension', () => {
expect(wrapper.find('[data-testid="collapsed-header"]').text()).toBe('hello there');
});
- it('renders chevron-lg-right icon', () => {
- expect(findIcon().props('name')).toBe('chevron-lg-right');
+ it('renders chevron-right icon', () => {
+ expect(findIcon().props('name')).toBe('chevron-right');
});
describe('onClick', () => {
@@ -60,8 +60,8 @@ describe('Merge Request Collapsible Extension', () => {
expect(findTitle().text()).toBe('Collapse');
});
- it('renders chevron-lg-down icon', () => {
- expect(findIcon().props('name')).toBe('chevron-lg-down');
+ it('renders chevron-down icon', () => {
+ expect(findIcon().props('name')).toBe('chevron-down');
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap b/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
deleted file mode 100644
index 4077564486c..00000000000
--- a/spec/frontend/vue_merge_request_widget/components/states/__snapshots__/mr_widget_auto_merge_enabled_spec.js.snap
+++ /dev/null
@@ -1,163 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`MRWidgetAutoMergeEnabled template should have correct elements 1`] = `
-<div
- class="mr-widget-body media gl-display-flex gl-align-items-center"
->
- <div
- class="gl-w-6 gl-h-6 gl-display-flex gl-align-self-start gl-mr-3"
- >
- <div
- class="gl-display-flex gl-m-auto"
- >
- <div
- class="gl-mr-3 gl-p-2 gl-m-0! gl-text-blue-500 gl-w-6 gl-p-2"
- >
- <div
- class="gl-rounded-full gl-relative gl-display-flex mr-widget-extension-icon"
- >
- <div
- class="gl-absolute gl-top-half gl-left-50p gl-translate-x-n50 gl-display-flex gl-m-auto"
- >
- <div
- class="gl-display-flex gl-m-auto gl-translate-y-n50"
- >
- <svg
- aria-label="Scheduled "
- class="gl-display-block gl-icon s12"
- data-qa-selector="status_scheduled_icon"
- data-testid="status-scheduled-icon"
- role="img"
- >
- <use
- href="#status-scheduled"
- />
- </svg>
- </div>
- </div>
- </div>
- </div>
- </div>
- </div>
-
- <div
- class="gl-display-flex gl-w-full"
- >
- <div
- class="media-body gl-display-flex gl-align-items-center"
- >
-
- <h4
- class="gl-mr-3"
- data-testid="statusText"
- >
- Set by to be merged automatically when the pipeline succeeds
- </h4>
-
- <div
- class="gl-display-flex gl-font-size-0 gl-ml-auto gl-gap-3"
- >
- <div
- class="gl-display-flex gl-align-items-flex-start"
- >
- <div
- class="dropdown b-dropdown gl-dropdown gl-display-block gl-md-display-none! btn-group"
- lazy=""
- no-caret=""
- title="Options"
- >
- <!---->
- <button
- aria-expanded="false"
- aria-haspopup="true"
- class="btn dropdown-toggle btn-default btn-sm gl-p-2! gl-button gl-dropdown-toggle btn-default-tertiary dropdown-icon-only dropdown-toggle-no-caret"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="dropdown-icon gl-icon s16"
- data-testid="ellipsis_v-icon"
- role="img"
- >
- <use
- href="#ellipsis_v"
- />
- </svg>
-
- <span
- class="gl-dropdown-button-text gl-sr-only"
- >
-
- </span>
-
- <svg
- aria-hidden="true"
- class="gl-button-icon dropdown-chevron gl-icon s16"
- data-testid="chevron-down-icon"
- role="img"
- >
- <use
- href="#chevron-down"
- />
- </svg>
- </button>
- <ul
- class="dropdown-menu dropdown-menu-right"
- role="menu"
- tabindex="-1"
- >
- <!---->
- </ul>
- </div>
-
- <button
- class="btn gl-display-none gl-md-display-block gl-float-left btn-confirm btn-sm gl-button btn-confirm-tertiary js-cancel-auto-merge"
- data-qa-selector="cancel_auto_merge_button"
- data-testid="cancelAutomaticMergeButton"
- type="button"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
-
- Cancel auto-merge
-
- </span>
- </button>
- </div>
- </div>
- </div>
-
- <div
- class="gl-md-display-none gl-border-l-1 gl-border-l-solid gl-border-gray-100 gl-ml-3 gl-pl-3 gl-h-6 gl-mt-1"
- >
- <button
- class="btn gl-vertical-align-top btn-default btn-sm gl-button btn-default-tertiary btn-icon"
- title="Collapse merge details"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="gl-button-icon gl-icon s16"
- data-testid="chevron-lg-up-icon"
- role="img"
- >
- <use
- href="#chevron-lg-up"
- />
- </svg>
-
- <!---->
- </button>
- </div>
- </div>
-</div>
-`;
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
index 5b9f30dfb86..fef5fee5f19 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_auto_merge_enabled_spec.js
@@ -128,14 +128,6 @@ describe('MRWidgetAutoMergeEnabled', () => {
});
describe('template', () => {
- it('should have correct elements', () => {
- factory({
- ...defaultMrProps(),
- });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-
it('should disable cancel auto merge button when the action is in progress', async () => {
factory({
...defaultMrProps(),
diff --git a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
index 4c93c88de16..7e941c5ceaa 100644
--- a/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js
@@ -1,6 +1,6 @@
import { nextTick } from 'vue';
import * as Sentry from '@sentry/browser';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import HelpPopover from '~/vue_shared/components/help_popover.vue';
import waitForPromises from 'helpers/wait_for_promises';
import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_icon.vue';
@@ -26,8 +26,8 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
const findHelpPopover = () => wrapper.findComponent(HelpPopover);
const findDynamicScroller = () => wrapper.findByTestId('dynamic-content-scroller');
- const createComponent = ({ propsData, slots } = {}) => {
- wrapper = shallowMountExtended(Widget, {
+ const createComponent = ({ propsData, slots, mountFn = shallowMountExtended } = {}) => {
+ wrapper = mountFn(Widget, {
propsData: {
isCollapsible: false,
loadingText: 'Loading widget',
@@ -73,6 +73,13 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
expect(findStatusIcon().props()).toMatchObject({ iconName: 'failed', isLoading: false });
});
+ it('displays the error text when :has-error is true', () => {
+ createComponent({
+ propsData: { hasError: true, errorText: 'API error' },
+ });
+ expect(wrapper.findByText('API error').exists()).toBe(true);
+ });
+
it('displays loading icon until request is made and then displays status icon when the request is complete', async () => {
const fetchCollapsedData = jest
.fn()
@@ -425,6 +432,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
beforeEach(() => {
createComponent({
+ mountFn: mountExtended,
propsData: {
isCollapsible: true,
content,
@@ -437,5 +445,11 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
await waitForPromises();
expect(findDynamicScroller().props('items')).toEqual(content);
});
+
+ it('renders the dynamic content inside the dynamic scroller', async () => {
+ findToggleButton().vm.$emit('click');
+ await waitForPromises();
+ expect(wrapper.findByText('Main text for the row').exists()).toBe(true);
+ });
});
});
diff --git a/spec/frontend/vue_merge_request_widget/extensions/security_reports/mock_data.js b/spec/frontend/vue_merge_request_widget/extensions/security_reports/mock_data.js
new file mode 100644
index 00000000000..c7354483e8b
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/extensions/security_reports/mock_data.js
@@ -0,0 +1,141 @@
+export const mockArtifacts = () => ({
+ data: {
+ project: {
+ id: 'gid://gitlab/Project/9',
+ mergeRequest: {
+ id: 'gid://gitlab/MergeRequest/1',
+ headPipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/1',
+ jobs: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Build/14',
+ name: 'sam_scan',
+ artifacts: {
+ nodes: [
+ {
+ downloadPath:
+ '/root/security-reports/-/jobs/14/artifacts/download?file_type=trace',
+ fileType: 'TRACE',
+ __typename: 'CiJobArtifact',
+ },
+ {
+ downloadPath:
+ '/root/security-reports/-/jobs/14/artifacts/download?file_type=sast',
+ fileType: 'SAST',
+ __typename: 'CiJobArtifact',
+ },
+ ],
+ __typename: 'CiJobArtifactConnection',
+ },
+ __typename: 'CiJob',
+ },
+ {
+ id: 'gid://gitlab/Ci::Build/11',
+ name: 'sast-spotbugs',
+ artifacts: {
+ nodes: [
+ {
+ downloadPath:
+ '/root/security-reports/-/jobs/11/artifacts/download?file_type=trace',
+ fileType: 'TRACE',
+ __typename: 'CiJobArtifact',
+ },
+ {
+ downloadPath:
+ '/root/security-reports/-/jobs/11/artifacts/download?file_type=sast',
+ fileType: 'SAST',
+ __typename: 'CiJobArtifact',
+ },
+ ],
+ __typename: 'CiJobArtifactConnection',
+ },
+ __typename: 'CiJob',
+ },
+ {
+ id: 'gid://gitlab/Ci::Build/10',
+ name: 'sast-sobelow',
+ artifacts: {
+ nodes: [
+ {
+ downloadPath:
+ '/root/security-reports/-/jobs/10/artifacts/download?file_type=trace',
+ fileType: 'TRACE',
+ __typename: 'CiJobArtifact',
+ },
+ ],
+ __typename: 'CiJobArtifactConnection',
+ },
+ __typename: 'CiJob',
+ },
+ {
+ id: 'gid://gitlab/Ci::Build/9',
+ name: 'sast-pmd-apex',
+ artifacts: {
+ nodes: [
+ {
+ downloadPath:
+ '/root/security-reports/-/jobs/9/artifacts/download?file_type=trace',
+ fileType: 'TRACE',
+ __typename: 'CiJobArtifact',
+ },
+ ],
+ __typename: 'CiJobArtifactConnection',
+ },
+ __typename: 'CiJob',
+ },
+ {
+ id: 'gid://gitlab/Ci::Build/8',
+ name: 'sast-eslint',
+ artifacts: {
+ nodes: [
+ {
+ downloadPath:
+ '/root/security-reports/-/jobs/8/artifacts/download?file_type=trace',
+ fileType: 'TRACE',
+ __typename: 'CiJobArtifact',
+ },
+ {
+ downloadPath:
+ '/root/security-reports/-/jobs/8/artifacts/download?file_type=sast',
+ fileType: 'SAST',
+ __typename: 'CiJobArtifact',
+ },
+ ],
+ __typename: 'CiJobArtifactConnection',
+ },
+ __typename: 'CiJob',
+ },
+ {
+ id: 'gid://gitlab/Ci::Build/7',
+ name: 'secrets',
+ artifacts: {
+ nodes: [
+ {
+ downloadPath:
+ '/root/security-reports/-/jobs/7/artifacts/download?file_type=trace',
+ fileType: 'TRACE',
+ __typename: 'CiJobArtifact',
+ },
+ {
+ downloadPath:
+ '/root/security-reports/-/jobs/7/artifacts/download?file_type=secret_detection',
+ fileType: 'SECRET_DETECTION',
+ __typename: 'CiJobArtifact',
+ },
+ ],
+ __typename: 'CiJobArtifactConnection',
+ },
+ __typename: 'CiJob',
+ },
+ ],
+ __typename: 'CiJobConnection',
+ },
+ __typename: 'Pipeline',
+ },
+ __typename: 'MergeRequest',
+ },
+ __typename: 'Project',
+ },
+ },
+});
diff --git a/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js b/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js
new file mode 100644
index 00000000000..16c2adaffaf
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import { GlDropdown } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import MRSecurityWidget from '~/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue';
+import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
+import securityReportMergeRequestDownloadPathsQuery from '~/vue_merge_request_widget/extensions/security_reports/graphql/security_report_merge_request_download_paths.query.graphql';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mockArtifacts } from './mock_data';
+
+Vue.use(VueApollo);
+
+describe('vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports.vue', () => {
+ let wrapper;
+
+ const createComponent = ({ propsData, mockResponse = mockArtifacts() } = {}) => {
+ wrapper = mountExtended(MRSecurityWidget, {
+ apolloProvider: createMockApollo([
+ [securityReportMergeRequestDownloadPathsQuery, jest.fn().mockResolvedValue(mockResponse)],
+ ]),
+ propsData: {
+ ...propsData,
+ mr: {},
+ },
+ });
+ };
+
+ const findWidget = () => wrapper.findComponent(Widget);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItem = (name) => wrapper.findByTestId(name);
+
+ describe('with data', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('displays the correct message', () => {
+ expect(wrapper.findByText('Security scans have run').exists()).toBe(true);
+ });
+
+ it('displays the help popover', () => {
+ expect(findWidget().props('helpPopover')).toEqual({
+ content: {
+ learnMorePath:
+ '/help/user/application_security/index#view-security-scan-information-in-merge-requests',
+ text:
+ 'New vulnerabilities are vulnerabilities that the security scan detects in the merge request that are different to existing vulnerabilities in the default branch.',
+ },
+ options: {
+ title: 'Security scan results',
+ },
+ });
+ });
+
+ it.each`
+ artifactName | exists | downloadPath
+ ${'sam_scan'} | ${true} | ${'/root/security-reports/-/jobs/14/artifacts/download?file_type=sast'}
+ ${'sast-spotbugs'} | ${true} | ${'/root/security-reports/-/jobs/11/artifacts/download?file_type=sast'}
+ ${'sast-sobelow'} | ${false} | ${''}
+ ${'sast-pmd-apex'} | ${false} | ${''}
+ ${'sast-eslint'} | ${true} | ${'/root/security-reports/-/jobs/8/artifacts/download?file_type=sast'}
+ ${'secrets'} | ${true} | ${'/root/security-reports/-/jobs/7/artifacts/download?file_type=secret_detection'}
+ `(
+ 'has a dropdown to download $artifactName artifacts',
+ ({ artifactName, exists, downloadPath }) => {
+ expect(findDropdown().exists()).toBe(true);
+ expect(wrapper.findByText(`Download ${artifactName}`).exists()).toBe(exists);
+
+ if (exists) {
+ const dropdownItem = findDropdownItem(`download-${artifactName}`);
+ expect(dropdownItem.attributes('download')).toBe('');
+ expect(dropdownItem.attributes('href')).toBe(downloadPath);
+ }
+ },
+ );
+ });
+
+ describe('without data', () => {
+ beforeEach(() => {
+ createComponent({ mockResponse: { data: { project: { id: 'project-id' } } } });
+ });
+
+ it('displays the correct message', () => {
+ expect(wrapper.findByText('Security scans have run').exists()).toBe(true);
+ });
+
+ it('should not display the artifacts dropdown', () => {
+ expect(findDropdown().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
index baef247b649..548b68bc103 100644
--- a/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extensions/test_report/index_spec.js
@@ -8,7 +8,11 @@ import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
-import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import TestCaseDetails from '~/pipelines/components/test_reports/test_case_details.vue';
import { failedReport } from 'jest/ci/reports/mock_data/mock_data';
@@ -57,7 +61,7 @@ describe('Test report extension', () => {
};
const createExpandedWidgetWithData = async (data = mixedResultsTestReports) => {
- mockApi(httpStatusCodes.OK, data);
+ mockApi(HTTP_STATUS_OK, data);
createComponent();
await waitForPromises();
findToggleCollapsedButton().trigger('click');
@@ -75,7 +79,7 @@ describe('Test report extension', () => {
describe('summary', () => {
it('displays loading state initially', () => {
- mockApi(httpStatusCodes.OK);
+ mockApi(HTTP_STATUS_OK);
createComponent();
expect(wrapper.text()).toContain(i18n.loading);
@@ -91,7 +95,7 @@ describe('Test report extension', () => {
});
it('with an error response, displays failed to load text', async () => {
- mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
+ mockApi(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
await waitForPromises();
@@ -107,7 +111,7 @@ describe('Test report extension', () => {
${'failed test results'} | ${newFailedTestReports} | ${'Test summary: 2 failed, 11 total tests'}
${'resolved failures'} | ${resolvedFailures} | ${'Test summary: 4 fixed test results, 11 total tests'}
`('displays summary text for $description', async ({ mockData, expectedResult }) => {
- mockApi(httpStatusCodes.OK, mockData);
+ mockApi(HTTP_STATUS_OK, mockData);
createComponent();
await waitForPromises();
@@ -116,7 +120,7 @@ describe('Test report extension', () => {
});
it('displays report level recently failed count', async () => {
- mockApi(httpStatusCodes.OK, recentFailures);
+ mockApi(HTTP_STATUS_OK, recentFailures);
createComponent();
await waitForPromises();
@@ -127,7 +131,7 @@ describe('Test report extension', () => {
});
it('displays a link to the full report', async () => {
- mockApi(httpStatusCodes.OK);
+ mockApi(HTTP_STATUS_OK);
createComponent();
await waitForPromises();
@@ -137,7 +141,7 @@ describe('Test report extension', () => {
});
it('hides copy failed tests button when there are no failing tests', async () => {
- mockApi(httpStatusCodes.OK);
+ mockApi(HTTP_STATUS_OK);
createComponent();
await waitForPromises();
@@ -146,7 +150,7 @@ describe('Test report extension', () => {
});
it('displays copy failed tests button when there are failing tests', async () => {
- mockApi(httpStatusCodes.OK, newFailedTestReports);
+ mockApi(HTTP_STATUS_OK, newFailedTestReports);
createComponent();
await waitForPromises();
@@ -159,7 +163,7 @@ describe('Test report extension', () => {
});
it('hides copy failed tests button when endpoint returns null files', async () => {
- mockApi(httpStatusCodes.OK, newFailedTestWithNullFilesReport);
+ mockApi(HTTP_STATUS_OK, newFailedTestWithNullFilesReport);
createComponent();
await waitForPromises();
@@ -168,7 +172,7 @@ describe('Test report extension', () => {
});
it('copy failed tests button updates tooltip text when clicked', async () => {
- mockApi(httpStatusCodes.OK, newFailedTestReports);
+ mockApi(HTTP_STATUS_OK, newFailedTestReports);
createComponent();
await waitForPromises();
@@ -195,7 +199,7 @@ describe('Test report extension', () => {
});
it('shows an error when a suite has a parsing error', async () => {
- mockApi(httpStatusCodes.OK, reportWithParsingErrors);
+ mockApi(HTTP_STATUS_OK, reportWithParsingErrors);
createComponent();
await waitForPromises();
diff --git a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
index a06ad930abe..01049e54a7f 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/accessibility/index_spec.js
@@ -6,7 +6,7 @@ import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import accessibilityExtension from '~/vue_merge_request_widget/extensions/accessibility';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { accessibilityReportResponseErrors, accessibilityReportResponseSuccess } from './mock_data';
describe('Accessibility extension', () => {
@@ -45,7 +45,7 @@ describe('Accessibility extension', () => {
describe('summary', () => {
it('displays loading text', () => {
- mockApi(httpStatusCodes.OK, accessibilityReportResponseErrors);
+ mockApi(HTTP_STATUS_OK, accessibilityReportResponseErrors);
createComponent();
@@ -53,7 +53,7 @@ describe('Accessibility extension', () => {
});
it('displays failed loading text', async () => {
- mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
+ mockApi(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
@@ -63,7 +63,7 @@ describe('Accessibility extension', () => {
});
it('displays detected errors and is expandable', async () => {
- mockApi(httpStatusCodes.OK, accessibilityReportResponseErrors);
+ mockApi(HTTP_STATUS_OK, accessibilityReportResponseErrors);
createComponent();
@@ -76,7 +76,7 @@ describe('Accessibility extension', () => {
});
it('displays no detected errors and is not expandable', async () => {
- mockApi(httpStatusCodes.OK, accessibilityReportResponseSuccess);
+ mockApi(HTTP_STATUS_OK, accessibilityReportResponseSuccess);
createComponent();
@@ -91,7 +91,7 @@ describe('Accessibility extension', () => {
describe('expanded data', () => {
beforeEach(async () => {
- mockApi(httpStatusCodes.OK, accessibilityReportResponseErrors);
+ mockApi(HTTP_STATUS_OK, accessibilityReportResponseErrors);
createComponent();
diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
index f0ebbb1a82e..67b327217ef 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/index_spec.js
@@ -7,10 +7,18 @@ import axios from '~/lib/utils/axios_utils';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
import codeQualityExtension from '~/vue_merge_request_widget/extensions/code_quality';
-import httpStatusCodes, { HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
-import { i18n } from '~/vue_merge_request_widget/extensions/code_quality/constants';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
+import {
+ i18n,
+ codeQualityPrefixes,
+} from '~/vue_merge_request_widget/extensions/code_quality/constants';
import {
codeQualityResponseNewErrors,
+ codeQualityResponseResolvedErrors,
codeQualityResponseResolvedAndNewErrors,
codeQualityResponseNoErrors,
} from './mock_data';
@@ -29,6 +37,10 @@ describe('Code Quality extension', () => {
const findToggleCollapsedButton = () => wrapper.findByTestId('toggle-button');
const findAllExtensionListItems = () => wrapper.findAllByTestId('extension-list-item');
+ const isCollapsable = () => wrapper.findByTestId('toggle-button').exists();
+ const getNeutralIcon = () => wrapper.findByTestId('status-neutral-icon').exists();
+ const getAlertIcon = () => wrapper.findByTestId('status-alert-icon').exists();
+ const getSuccessIcon = () => wrapper.findByTestId('status-success-icon').exists();
const createComponent = () => {
wrapper = mountExtended(extensionsContainer, {
@@ -55,7 +67,7 @@ describe('Code Quality extension', () => {
describe('summary', () => {
it('displays loading text', () => {
- mockApi(httpStatusCodes.OK, codeQualityResponseNewErrors);
+ mockApi(HTTP_STATUS_OK, codeQualityResponseNewErrors);
createComponent();
@@ -72,28 +84,57 @@ describe('Code Quality extension', () => {
});
it('displays failed loading text', async () => {
- mockApi(httpStatusCodes.INTERNAL_SERVER_ERROR);
+ mockApi(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
await waitForPromises();
+
expect(wrapper.text()).toBe(i18n.error);
+ expect(isCollapsable()).toBe(false);
});
- it('displays correct single Report', async () => {
- mockApi(httpStatusCodes.OK, codeQualityResponseNewErrors);
+ it('displays new Errors finding', async () => {
+ mockApi(HTTP_STATUS_OK, codeQualityResponseNewErrors);
createComponent();
await waitForPromises();
+ expect(wrapper.text()).toBe(
+ i18n
+ .singularCopy(
+ i18n.findings(codeQualityResponseNewErrors.new_errors, codeQualityPrefixes.new),
+ )
+ .replace(/%{strong_start}/g, '')
+ .replace(/%{strong_end}/g, ''),
+ );
+ expect(isCollapsable()).toBe(true);
+ expect(getAlertIcon()).toBe(true);
+ });
+
+ it('displays resolved Errors finding', async () => {
+ mockApi(HTTP_STATUS_OK, codeQualityResponseResolvedErrors);
+ createComponent();
+
+ await waitForPromises();
expect(wrapper.text()).toBe(
- i18n.degradedCopy(i18n.singularReport(codeQualityResponseNewErrors.new_errors)),
+ i18n
+ .singularCopy(
+ i18n.findings(
+ codeQualityResponseResolvedErrors.resolved_errors,
+ codeQualityPrefixes.fixed,
+ ),
+ )
+ .replace(/%{strong_start}/g, '')
+ .replace(/%{strong_end}/g, ''),
);
+ expect(isCollapsable()).toBe(true);
+ expect(getSuccessIcon()).toBe(true);
});
it('displays quality improvement and degradation', async () => {
- mockApi(httpStatusCodes.OK, codeQualityResponseResolvedAndNewErrors);
+ mockApi(HTTP_STATUS_OK, codeQualityResponseResolvedAndNewErrors);
createComponent();
await waitForPromises();
@@ -102,28 +143,38 @@ describe('Code Quality extension', () => {
expect(wrapper.text()).toBe(
i18n
.improvementAndDegradationCopy(
- i18n.pluralReport(codeQualityResponseResolvedAndNewErrors.resolved_errors),
- i18n.pluralReport(codeQualityResponseResolvedAndNewErrors.new_errors),
+ i18n.findings(
+ codeQualityResponseResolvedAndNewErrors.resolved_errors,
+ codeQualityPrefixes.fixed,
+ ),
+ i18n.findings(
+ codeQualityResponseResolvedAndNewErrors.new_errors,
+ codeQualityPrefixes.new,
+ ),
)
.replace(/%{strong_start}/g, '')
.replace(/%{strong_end}/g, ''),
);
+ expect(isCollapsable()).toBe(true);
+ expect(getAlertIcon()).toBe(true);
});
it('displays no detected errors', async () => {
- mockApi(httpStatusCodes.OK, codeQualityResponseNoErrors);
+ mockApi(HTTP_STATUS_OK, codeQualityResponseNoErrors);
createComponent();
await waitForPromises();
expect(wrapper.text()).toBe(i18n.noChanges);
+ expect(isCollapsable()).toBe(false);
+ expect(getNeutralIcon()).toBe(true);
});
});
describe('expanded data', () => {
beforeEach(async () => {
- mockApi(httpStatusCodes.OK, codeQualityResponseResolvedAndNewErrors);
+ mockApi(HTTP_STATUS_OK, codeQualityResponseResolvedAndNewErrors);
createComponent();
diff --git a/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
index 2e8e70f25db..cb23b730a93 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/code_quality/mock_data.js
@@ -17,9 +17,34 @@ export const codeQualityResponseNewErrors = {
resolved_errors: [],
existing_errors: [],
summary: {
- total: 2,
+ total: 12235,
resolved: 0,
- errored: 2,
+ errored: 12235,
+ },
+};
+
+export const codeQualityResponseResolvedErrors = {
+ status: 'success',
+ new_errors: [],
+ resolved_errors: [
+ {
+ description: "Parsing error: 'return' outside of function",
+ severity: 'minor',
+ file_path: 'index.js',
+ line: 12,
+ },
+ {
+ description: 'TODO found',
+ severity: 'minor',
+ file_path: '.gitlab-ci.yml',
+ line: 73,
+ },
+ ],
+ existing_errors: [],
+ summary: {
+ total: 12235,
+ resolved: 0,
+ errored: 12235,
},
};
@@ -43,9 +68,9 @@ export const codeQualityResponseResolvedAndNewErrors = {
],
existing_errors: [],
summary: {
- total: 2,
+ total: 12233,
resolved: 1,
- errored: 1,
+ errored: 12233,
},
};
@@ -55,8 +80,8 @@ export const codeQualityResponseNoErrors = {
resolved_errors: [],
existing_errors: [],
summary: {
- total: 0,
+ total: 12234,
resolved: 0,
- errored: 0,
+ errored: 12234,
},
};
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js
index d038660e6d3..015d394312a 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_how_to_merge_modal_spec.js
@@ -34,7 +34,7 @@ describe('MRWidgetHowToMerge', () => {
});
it('renders a selection of markdown fields', () => {
- expect(findInstructionsFields().length).toBe(3);
+ expect(findInstructionsFields().length).toBe(2);
});
it('renders a tip including a link to docs when a valid link is present', () => {
@@ -48,23 +48,11 @@ describe('MRWidgetHowToMerge', () => {
it('should render different instructions based on if the user can merge', () => {
mountComponent({ props: { canMerge: true } });
- expect(findInstructionsFields().at(2).text()).toContain('git push origin');
- });
-
- it('should render different instructions based on if the merge is based off a fork', () => {
- mountComponent({ props: { isFork: true } });
- expect(findInstructionsFields().at(0).text()).toContain('FETCH_HEAD');
- });
-
- it('escapes the target branch name shell-secure', () => {
- mountComponent({ props: { targetBranch: '";echo$IFS"you_shouldnt_run_this' } });
-
- expect(findInstructionsFields().at(1).text()).toContain('\'";echo$IFS"you_shouldnt_run_this\'');
+ expect(findInstructionsFields().at(1).text()).toContain('git push origin');
});
it('escapes the source branch name shell-secure', () => {
mountComponent({ props: { sourceBranch: 'branch-of-$USER' } });
-
expect(findInstructionsFields().at(0).text()).toContain("'branch-of-$USER'");
});
});
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 07cbfe1e79b..4f24ec2d015 100644
--- a/spec/frontend/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/frontend/vue_shared/components/ci_badge_link_spec.js
@@ -1,6 +1,6 @@
import { GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import CiBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -79,7 +79,7 @@ describe('CI Badge Link Component', () => {
const findIcon = () => wrapper.findComponent(CiIcon);
const createComponent = (propsData) => {
- wrapper = shallowMount(CiBadge, { propsData });
+ wrapper = shallowMount(CiBadgeLink, { propsData });
};
afterEach(() => {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
index 66ef473f368..63c22aff3d5 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/actions_spec.js
@@ -4,7 +4,7 @@ import testAction from 'helpers/vuex_action_helper';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import Api from '~/api';
import { createAlert } from '~/flash';
-import httpStatusCodes from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK, HTTP_STATUS_SERVICE_UNAVAILABLE } from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/components/filtered_search_bar/store/modules/filters/actions';
import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
import initialState from '~/vue_shared/components/filtered_search_bar/store/modules/filters/state';
@@ -122,7 +122,7 @@ describe('Filters actions', () => {
':id',
encodeURIComponent(projectEndpoint),
);
- mock.onGet(url).replyOnce(httpStatusCodes.OK, mockBranches);
+ mock.onGet(url).replyOnce(HTTP_STATUS_OK, mockBranches);
});
it('dispatches RECEIVE_BRANCHES_SUCCESS with received data', () => {
@@ -143,7 +143,7 @@ describe('Filters actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
+ mock.onAny().replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_BRANCHES_ERROR', () => {
@@ -155,7 +155,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_BRANCHES },
{
type: types.RECEIVE_BRANCHES_ERROR,
- payload: httpStatusCodes.SERVICE_UNAVAILABLE,
+ payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],
@@ -177,7 +177,7 @@ describe('Filters actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onAny().replyOnce(httpStatusCodes.OK, filterUsers);
+ mock.onAny().replyOnce(HTTP_STATUS_OK, filterUsers);
});
it('dispatches RECEIVE_AUTHORS_SUCCESS with received data and groupEndpoint set', () => {
@@ -215,7 +215,7 @@ describe('Filters actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
+ mock.onAny().replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_AUTHORS_ERROR and groupEndpoint set', () => {
@@ -227,7 +227,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_AUTHORS },
{
type: types.RECEIVE_AUTHORS_ERROR,
- payload: httpStatusCodes.SERVICE_UNAVAILABLE,
+ payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],
@@ -246,7 +246,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_AUTHORS },
{
type: types.RECEIVE_AUTHORS_ERROR,
- payload: httpStatusCodes.SERVICE_UNAVAILABLE,
+ payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],
@@ -261,7 +261,7 @@ describe('Filters actions', () => {
describe('fetchMilestones', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(milestonesEndpoint).replyOnce(httpStatusCodes.OK, filterMilestones);
+ mock.onGet(milestonesEndpoint).replyOnce(HTTP_STATUS_OK, filterMilestones);
});
it('dispatches RECEIVE_MILESTONES_SUCCESS with received data', () => {
@@ -282,7 +282,7 @@ describe('Filters actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
+ mock.onAny().replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_MILESTONES_ERROR', () => {
@@ -294,7 +294,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_MILESTONES },
{
type: types.RECEIVE_MILESTONES_ERROR,
- payload: httpStatusCodes.SERVICE_UNAVAILABLE,
+ payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],
@@ -307,7 +307,7 @@ describe('Filters actions', () => {
describe('success', () => {
let restoreVersion;
beforeEach(() => {
- mock.onAny().replyOnce(httpStatusCodes.OK, filterUsers);
+ mock.onAny().replyOnce(HTTP_STATUS_OK, filterUsers);
restoreVersion = gon.api_version;
gon.api_version = 'v1';
});
@@ -352,7 +352,7 @@ describe('Filters actions', () => {
describe('error', () => {
let restoreVersion;
beforeEach(() => {
- mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
+ mock.onAny().replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
restoreVersion = gon.api_version;
gon.api_version = 'v1';
});
@@ -370,7 +370,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_ASSIGNEES },
{
type: types.RECEIVE_ASSIGNEES_ERROR,
- payload: httpStatusCodes.SERVICE_UNAVAILABLE,
+ payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],
@@ -389,7 +389,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_ASSIGNEES },
{
type: types.RECEIVE_ASSIGNEES_ERROR,
- payload: httpStatusCodes.SERVICE_UNAVAILABLE,
+ payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],
@@ -404,7 +404,7 @@ describe('Filters actions', () => {
describe('fetchLabels', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(labelsEndpoint).replyOnce(httpStatusCodes.OK, filterLabels);
+ mock.onGet(labelsEndpoint).replyOnce(HTTP_STATUS_OK, filterLabels);
});
it('dispatches RECEIVE_LABELS_SUCCESS with received data', () => {
@@ -425,7 +425,7 @@ describe('Filters actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onAny().replyOnce(httpStatusCodes.SERVICE_UNAVAILABLE);
+ mock.onAny().replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
});
it('dispatches RECEIVE_LABELS_ERROR', () => {
@@ -437,7 +437,7 @@ describe('Filters actions', () => {
{ type: types.REQUEST_LABELS },
{
type: types.RECEIVE_LABELS_ERROR,
- payload: httpStatusCodes.SERVICE_UNAVAILABLE,
+ payload: HTTP_STATUS_SERVICE_UNAVAILABLE,
},
],
[],
diff --git a/spec/frontend/vue_shared/components/group_select/group_select_spec.js b/spec/frontend/vue_shared/components/group_select/group_select_spec.js
index c10b32c6acc..87dd7795b98 100644
--- a/spec/frontend/vue_shared/components/group_select/group_select_spec.js
+++ b/spec/frontend/vue_shared/components/group_select/group_select_spec.js
@@ -1,20 +1,18 @@
import { nextTick } from 'vue';
-import { GlCollapsibleListbox } from '@gitlab/ui';
+import { GlFormGroup, GlCollapsibleListbox } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
-import { createAlert } from '~/flash';
import GroupSelect from '~/vue_shared/components/group_select/group_select.vue';
import {
TOGGLE_TEXT,
+ RESET_LABEL,
FETCH_GROUPS_ERROR,
FETCH_GROUP_ERROR,
QUERY_TOO_SHORT_MESSAGE,
} from '~/vue_shared/components/group_select/constants';
import waitForPromises from 'helpers/wait_for_promises';
-jest.mock('~/flash');
-
describe('GroupSelect', () => {
let wrapper;
let mock;
@@ -26,22 +24,34 @@ describe('GroupSelect', () => {
};
const groupEndpoint = `/api/undefined/groups/${groupMock.id}`;
+ // Stubs
+ const GlAlert = {
+ template: '<div><slot /></div>',
+ };
+
// Props
+ const label = 'label';
const inputName = 'inputName';
const inputId = 'inputId';
// Finders
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findInput = () => wrapper.findByTestId('input');
+ const findAlert = () => wrapper.findComponent(GlAlert);
// Helpers
const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(GroupSelect, {
propsData: {
+ label,
inputName,
inputId,
...props,
},
+ stubs: {
+ GlAlert,
+ },
});
};
const openListbox = () => findListbox().vm.$emit('shown');
@@ -65,6 +75,12 @@ describe('GroupSelect', () => {
mock.restore();
});
+ it('passes the label to GlFormGroup', () => {
+ createComponent();
+
+ expect(findFormGroup().attributes('label')).toBe(label);
+ });
+
describe('on mount', () => {
it('fetches groups when the listbox is opened', async () => {
createComponent();
@@ -94,13 +110,13 @@ describe('GroupSelect', () => {
.reply(200, [{ full_name: 'notTheSelectedGroup', id: '2' }]);
mock.onGet(groupEndpoint).reply(500);
createComponent({ props: { initialSelection: groupMock.id } });
+
+ expect(findAlert().exists()).toBe(false);
+
await waitForPromises();
- expect(createAlert).toHaveBeenCalledWith({
- message: FETCH_GROUP_ERROR,
- error: expect.any(Error),
- parent: wrapper.vm.$el,
- });
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_GROUP_ERROR);
});
});
});
@@ -109,13 +125,12 @@ describe('GroupSelect', () => {
mock.onGet('/api/undefined/groups.json').reply(500);
createComponent();
openListbox();
+ expect(findAlert().exists()).toBe(false);
+
await waitForPromises();
- expect(createAlert).toHaveBeenCalledWith({
- message: FETCH_GROUPS_ERROR,
- error: expect.any(Error),
- parent: wrapper.vm.$el,
- });
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_GROUPS_ERROR);
});
describe('selection', () => {
@@ -186,7 +201,11 @@ describe('GroupSelect', () => {
await waitForPromises();
expect(mock.history.get).toHaveLength(2);
- expect(mock.history.get[1].params).toStrictEqual({ search: searchString });
+ expect(mock.history.get[1].params).toStrictEqual({
+ page: 1,
+ per_page: 20,
+ search: searchString,
+ });
});
it('shows a notice if the search query is too short', async () => {
@@ -199,4 +218,105 @@ describe('GroupSelect', () => {
expect(findListbox().props('noResultsText')).toBe(QUERY_TOO_SHORT_MESSAGE);
});
});
+
+ describe('pagination', () => {
+ const searchString = 'searchString';
+
+ beforeEach(async () => {
+ let requestCount = 0;
+ mock.onGet('/api/undefined/groups.json').reply(({ params }) => {
+ requestCount += 1;
+ return [
+ 200,
+ [
+ {
+ full_name: `Group [page: ${params.page} - search: ${params.search}]`,
+ id: requestCount,
+ },
+ ],
+ {
+ page: params.page,
+ 'x-total-pages': 3,
+ },
+ ];
+ });
+ createComponent();
+ openListbox();
+ findListbox().vm.$emit('bottom-reached');
+ return waitForPromises();
+ });
+
+ it('fetches the next page when bottom is reached', async () => {
+ expect(mock.history.get).toHaveLength(2);
+ expect(mock.history.get[1].params).toStrictEqual({
+ page: 2,
+ per_page: 20,
+ search: '',
+ });
+ });
+
+ it('fetches the first page when the search query changes', async () => {
+ search(searchString);
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(3);
+ expect(mock.history.get[2].params).toStrictEqual({
+ page: 1,
+ per_page: 20,
+ search: searchString,
+ });
+ });
+
+ it('retains the search query when infinite scrolling', async () => {
+ search(searchString);
+ await waitForPromises();
+ findListbox().vm.$emit('bottom-reached');
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(4);
+ expect(mock.history.get[3].params).toStrictEqual({
+ page: 2,
+ per_page: 20,
+ search: searchString,
+ });
+ });
+
+ it('pauses infinite scroll after fetching the last page', async () => {
+ expect(findListbox().props('infiniteScroll')).toBe(true);
+
+ findListbox().vm.$emit('bottom-reached');
+ await waitForPromises();
+
+ expect(findListbox().props('infiniteScroll')).toBe(false);
+ });
+
+ it('resumes infinite scroll when search query changes', async () => {
+ findListbox().vm.$emit('bottom-reached');
+ await waitForPromises();
+
+ expect(findListbox().props('infiniteScroll')).toBe(false);
+
+ search(searchString);
+ await waitForPromises();
+
+ expect(findListbox().props('infiniteScroll')).toBe(true);
+ });
+ });
+
+ it.each`
+ description | clearable | expectedLabel
+ ${'passes'} | ${true} | ${RESET_LABEL}
+ ${'does not pass'} | ${false} | ${''}
+ `(
+ '$description the reset button label to the listbox when clearable is $clearable',
+ ({ clearable, expectedLabel }) => {
+ createComponent({
+ props: {
+ clearable,
+ },
+ });
+
+ expect(findListbox().props('resetButtonLabel')).toBe(expectedLabel);
+ },
+ );
});
diff --git a/spec/frontend/vue_shared/components/header_ci_component_spec.js b/spec/frontend/vue_shared/components/header_ci_component_spec.js
index aea76f164f0..94e1ece8c6b 100644
--- a/spec/frontend/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -84,11 +84,12 @@ describe('Header CI Component', () => {
expect(findUserLink().text()).toContain(defaultProps.user.username);
});
- it('has the correct data attributes', () => {
+ it('has the correct HTML attributes', () => {
expect(findUserLink().attributes()).toMatchObject({
'data-user-id': defaultProps.user.id.toString(),
'data-username': defaultProps.user.username,
'data-name': defaultProps.user.name,
+ href: defaultProps.user.web_url,
});
});
diff --git a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
index cb7262b15e3..7ed6a59c844 100644
--- a/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
+++ b/spec/frontend/vue_shared/components/listbox_input/listbox_input_spec.js
@@ -1,11 +1,13 @@
import { shallowMount } from '@vue/test-utils';
-import { GlListbox } from '@gitlab/ui';
+import { GlFormGroup, GlListbox } from '@gitlab/ui';
import ListboxInput from '~/vue_shared/components/listbox_input/listbox_input.vue';
describe('ListboxInput', () => {
let wrapper;
// Props
+ const label = 'label';
+ const decription = 'decription';
const name = 'name';
const defaultToggleText = 'defaultToggleText';
const items = [
@@ -21,30 +23,70 @@ describe('ListboxInput', () => {
options: [{ text: 'Item 3', value: '3' }],
},
];
+ const id = 'id';
// Finders
+ const findGlFormGroup = () => wrapper.findComponent(GlFormGroup);
const findGlListbox = () => wrapper.findComponent(GlListbox);
const findInput = () => wrapper.find('input');
const createComponent = (propsData) => {
wrapper = shallowMount(ListboxInput, {
propsData: {
+ label,
+ decription,
name,
defaultToggleText,
items,
...propsData,
},
+ attrs: {
+ id,
+ },
});
};
- describe('input attributes', () => {
+ describe('wrapper', () => {
+ it.each`
+ description | labelProp | descriptionProp | rendersGlFormGroup
+ ${'does not render'} | ${''} | ${''} | ${false}
+ ${'renders'} | ${'labelProp'} | ${''} | ${true}
+ ${'renders'} | ${''} | ${'descriptionProp'} | ${true}
+ ${'renders'} | ${'labelProp'} | ${'descriptionProp'} | ${true}
+ `(
+ "$description a GlFormGroup when label is '$labelProp' and description is '$descriptionProp'",
+ ({ labelProp, descriptionProp, rendersGlFormGroup }) => {
+ createComponent({ label: labelProp, description: descriptionProp });
+
+ expect(findGlFormGroup().exists()).toBe(rendersGlFormGroup);
+ },
+ );
+ });
+
+ describe('options', () => {
beforeEach(() => {
createComponent();
});
+ it('passes the label to the form group', () => {
+ expect(findGlFormGroup().attributes('label')).toBe(label);
+ });
+
+ it('passes the decription to the form group', () => {
+ expect(findGlFormGroup().attributes('decription')).toBe(decription);
+ });
+
it('sets the input name', () => {
expect(findInput().attributes('name')).toBe(name);
});
+
+ it('is not filterable with few items', () => {
+ expect(findGlListbox().props('searchable')).toBe(false);
+ });
+
+ it('passes attributes to the root element', () => {
+ expect(findGlFormGroup().attributes('id')).toBe(id);
+ });
});
describe('toggle text', () => {
@@ -91,12 +133,29 @@ describe('ListboxInput', () => {
});
describe('search', () => {
- beforeEach(() => {
- createComponent();
+ it('is searchable when there are more than 10 items', () => {
+ createComponent({
+ items: [
+ {
+ text: 'Group 1',
+ options: [...Array(10).keys()].map((index) => ({
+ text: index + 1,
+ value: String(index + 1),
+ })),
+ },
+ {
+ text: 'Group 2',
+ options: [{ text: 'Item 11', value: '11' }],
+ },
+ ],
+ });
+
+ expect(findGlListbox().props('searchable')).toBe(true);
});
it('passes all items to GlListbox by default', () => {
createComponent();
+
expect(findGlListbox().props('items')).toStrictEqual(items);
});
diff --git a/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js b/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js
new file mode 100644
index 00000000000..34071775b9c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/editor_mode_dropdown_spec.js
@@ -0,0 +1,58 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue';
+
+describe('vue_shared/component/markdown/editor_mode_dropdown', () => {
+ let wrapper;
+
+ const createComponent = ({ value, size } = {}) => {
+ wrapper = shallowMount(EditorModeDropdown, {
+ propsData: {
+ value,
+ size,
+ },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdownItem = (text) =>
+ wrapper
+ .findAllComponents(GlDropdownItem)
+ .filter((item) => item.text().startsWith(text))
+ .at(0);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe.each`
+ modeText | value | dropdownText | otherMode
+ ${'Rich text'} | ${'richText'} | ${'View markdown'} | ${'Markdown'}
+ ${'Markdown'} | ${'markdown'} | ${'View rich text'} | ${'Rich text'}
+ `('$modeText', ({ modeText, value, dropdownText, otherMode }) => {
+ beforeEach(() => {
+ createComponent({ value });
+ });
+
+ it('shows correct dropdown label', () => {
+ expect(findDropdown().props('text')).toEqual(dropdownText);
+ });
+
+ it('checks correct checked dropdown item', () => {
+ expect(findDropdownItem(modeText).props().isChecked).toBe(true);
+ expect(findDropdownItem(otherMode).props().isChecked).toBe(false);
+ });
+
+ it('emits event on click', () => {
+ findDropdownItem(modeText).vm.$emit('click');
+
+ expect(wrapper.emitted().input).toEqual([[value]]);
+ });
+ });
+
+ it('passes size to dropdown', () => {
+ createComponent({ size: 'small', value: 'markdown' });
+
+ expect(findDropdown().props('size')).toEqual('small');
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 285ea10c813..3b8e78bbadd 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -37,7 +37,7 @@ describe('Markdown field component', () => {
axiosMock.restore();
});
- function createSubject({ lines = [], enablePreview = true } = {}) {
+ function createSubject({ lines = [], enablePreview = true, showContentEditorSwitcher } = {}) {
// We actually mount a wrapper component so that we can force Vue to rerender classes in order to test a regression
// caused by mixing Vanilla JS and Vue.
subject = mountExtended(
@@ -68,6 +68,7 @@ describe('Markdown field component', () => {
lines,
enablePreview,
restrictedToolBarItems,
+ showContentEditorSwitcher,
},
},
);
@@ -191,6 +192,7 @@ describe('Markdown field component', () => {
markdownDocsPath,
quickActionsDocsPath: '',
showCommentToolBar: true,
+ showContentEditorSwitcher: false,
});
});
});
@@ -342,4 +344,18 @@ describe('Markdown field component', () => {
restrictedToolBarItems,
);
});
+
+ describe('showContentEditorSwitcher', () => {
+ it('defaults to false', () => {
+ createSubject();
+
+ expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(false);
+ });
+
+ it('passes showContentEditorSwitcher', () => {
+ createSubject({ showContentEditorSwitcher: true });
+
+ expect(findMarkdownToolbar().props('showContentEditorSwitcher')).toBe(true);
+ });
+ });
});
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 5f416db2676..e3df2cde1c1 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -1,4 +1,3 @@
-import { GlSegmentedControl } from '@gitlab/ui';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
@@ -49,7 +48,6 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
},
});
};
- const findSegmentedControl = () => wrapper.findComponent(GlSegmentedControl);
const findMarkdownField = () => wrapper.findComponent(MarkdownField);
const findTextarea = () => wrapper.find('textarea');
const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
@@ -97,36 +95,28 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findTextarea().element.value).toBe(value);
});
- it('renders switch segmented control', () => {
+ it(`emits ${EDITING_MODE_CONTENT_EDITOR} event when enableContentEditor emitted from markdown editor`, async () => {
buildWrapper();
- expect(findSegmentedControl().props()).toEqual({
- checked: EDITING_MODE_MARKDOWN_FIELD,
- options: [
- {
- text: expect.any(String),
- value: EDITING_MODE_MARKDOWN_FIELD,
- },
- {
- text: expect.any(String),
- value: EDITING_MODE_CONTENT_EDITOR,
- },
- ],
- });
- });
+ findMarkdownField().vm.$emit('enableContentEditor');
- describe.each`
- editingMode
- ${EDITING_MODE_CONTENT_EDITOR}
- ${EDITING_MODE_MARKDOWN_FIELD}
- `('when segmented control emits change event with $editingMode value', ({ editingMode }) => {
- it(`emits ${editingMode} event`, () => {
- buildWrapper();
+ await nextTick();
- findSegmentedControl().vm.$emit('change', editingMode);
+ expect(wrapper.emitted(EDITING_MODE_CONTENT_EDITOR)).toHaveLength(1);
+ });
- expect(wrapper.emitted(editingMode)).toHaveLength(1);
+ it(`emits ${EDITING_MODE_MARKDOWN_FIELD} event when enableMarkdownEditor emitted from content editor`, async () => {
+ buildWrapper({
+ stubs: { ContentEditor: stubComponent(ContentEditor) },
});
+
+ findMarkdownField().vm.$emit('enableContentEditor');
+
+ await nextTick();
+
+ findContentEditor().vm.$emit('enableMarkdownEditor');
+
+ expect(wrapper.emitted(EDITING_MODE_MARKDOWN_FIELD)).toHaveLength(1);
});
describe(`when editingMode is ${EDITING_MODE_MARKDOWN_FIELD}`, () => {
@@ -159,11 +149,10 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(wrapper.emitted('keydown')).toHaveLength(1);
});
- describe(`when segmented control triggers input event with ${EDITING_MODE_CONTENT_EDITOR} value`, () => {
+ describe(`when markdown field triggers enableContentEditor event`, () => {
beforeEach(() => {
buildWrapper();
- findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
- findSegmentedControl().vm.$emit('change', EDITING_MODE_CONTENT_EDITOR);
+ findMarkdownField().vm.$emit('enableContentEditor');
});
it('displays the content editor', () => {
@@ -202,7 +191,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
describe(`when editingMode is ${EDITING_MODE_CONTENT_EDITOR}`, () => {
beforeEach(() => {
buildWrapper();
- findSegmentedControl().vm.$emit('input', EDITING_MODE_CONTENT_EDITOR);
+ findMarkdownField().vm.$emit('enableContentEditor');
});
describe('when autofocus is true', () => {
@@ -234,9 +223,9 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(wrapper.emitted('keydown')).toEqual([[event]]);
});
- describe(`when segmented control triggers input event with ${EDITING_MODE_MARKDOWN_FIELD} value`, () => {
+ describe(`when richText editor triggers enableMarkdownEditor event`, () => {
beforeEach(() => {
- findSegmentedControl().vm.$emit('input', EDITING_MODE_MARKDOWN_FIELD);
+ findContentEditor().vm.$emit('enableMarkdownEditor');
});
it('hides the content editor', () => {
@@ -251,29 +240,5 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findLocalStorageSync().props().value).toBe(EDITING_MODE_MARKDOWN_FIELD);
});
});
-
- describe('when content editor emits loading event', () => {
- beforeEach(() => {
- findContentEditor().vm.$emit('loading');
- });
-
- it('disables switch editing mode control', () => {
- // This is the only way that I found to check the segmented control is disabled
- expect(findSegmentedControl().find('input[disabled]').exists()).toBe(true);
- });
-
- describe.each`
- event
- ${'loadingSuccess'}
- ${'loadingError'}
- `('when content editor emits $event event', ({ event }) => {
- beforeEach(() => {
- findContentEditor().vm.$emit(event);
- });
- it('enables the switch editing mode control', () => {
- expect(findSegmentedControl().find('input[disabled]').exists()).toBe(false);
- });
- });
- });
});
});
diff --git a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
index f698794b951..b1a1dbbeb7a 100644
--- a/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/toolbar_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import Toolbar from '~/vue_shared/components/markdown/toolbar.vue';
+import EditorModeDropdown from '~/vue_shared/components/markdown/editor_mode_dropdown.vue';
describe('toolbar', () => {
let wrapper;
@@ -47,4 +48,18 @@ describe('toolbar', () => {
expect(wrapper.find('.comment-toolbar').exists()).toBe(true);
});
});
+
+ describe('with content editor switcher', () => {
+ beforeEach(() => {
+ createMountedWrapper({
+ showContentEditorSwitcher: true,
+ });
+ });
+
+ it('re-emits event from switcher', () => {
+ wrapper.findComponent(EditorModeDropdown).vm.$emit('input', 'richText');
+
+ expect(wrapper.emitted('enableContentEditor')).toEqual([[]]);
+ });
+ });
});
diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap b/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap
deleted file mode 100644
index 2ea8985b16a..00000000000
--- a/spec/frontend/vue_shared/components/runner_aws_deployments/__snapshots__/runner_aws_deployments_modal_spec.js.snap
+++ /dev/null
@@ -1,177 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`RunnerAwsDeploymentsModal renders the modal 1`] = `
-<gl-modal-stub
- actionprimary="[object Object]"
- actionsecondary="[object Object]"
- arialabel=""
- dismisslabel="Close"
- modalclass=""
- modalid="runner-aws-deployments-modal"
- size="sm"
- title="Deploy GitLab Runner in AWS"
- titletag="h4"
->
- <p>
- Select your preferred option here. In the next step, you can choose the capacity for your runner in the AWS CloudFormation console.
- </p>
-
- <gl-form-radio-group-stub
- checked="[object Object]"
- disabledfield="disabled"
- htmlfield="html"
- label="Choose your preferred GitLab Runner"
- label-sr-only=""
- options=""
- textfield="text"
- valuefield="value"
- >
- <gl-form-radio-stub
- class="gl-py-5 gl-pl-8 gl-border-b"
- value="[object Object]"
- >
- <div
- class="gl-mt-n1 gl-pl-4 gl-pb-2 gl-font-weight-bold"
- >
-
- Amazon Linux 2 Docker HA with manual scaling and optional scheduling. Non-spot.
-
- <gl-accordion-stub
- class="gl-pt-3"
- headerlevel="3"
- >
- <gl-accordion-item-stub
- class="gl-font-weight-normal"
- headerclass=""
- title="More Details"
- titlevisible="Less Details"
- >
- <p
- class="gl-pt-2"
- >
- No spot. This is the default choice for Linux Docker executor.
- </p>
-
- <p
- class="gl-m-0"
- >
- A capacity of 1 enables warm HA through Auto Scaling group re-spawn. A capacity of 2 enables hot HA because the service is available even when a node is lost. A capacity of 3 or more enables hot HA and manual scaling of runner fleet.
- </p>
- </gl-accordion-item-stub>
- </gl-accordion-stub>
- </div>
- </gl-form-radio-stub>
- <gl-form-radio-stub
- class="gl-py-5 gl-pl-8 gl-border-b"
- value="[object Object]"
- >
- <div
- class="gl-mt-n1 gl-pl-4 gl-pb-2 gl-font-weight-bold"
- >
-
- Amazon Linux 2 Docker HA with manual scaling and optional scheduling. 100% spot.
-
- <gl-accordion-stub
- class="gl-pt-3"
- headerlevel="3"
- >
- <gl-accordion-item-stub
- class="gl-font-weight-normal"
- headerclass=""
- title="More Details"
- titlevisible="Less Details"
- >
- <p
- class="gl-pt-2"
- >
- 100% spot.
- </p>
-
- <p
- class="gl-m-0"
- >
- Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.
- </p>
- </gl-accordion-item-stub>
- </gl-accordion-stub>
- </div>
- </gl-form-radio-stub>
- <gl-form-radio-stub
- class="gl-py-5 gl-pl-8 gl-border-b"
- value="[object Object]"
- >
- <div
- class="gl-mt-n1 gl-pl-4 gl-pb-2 gl-font-weight-bold"
- >
-
- Windows 2019 Shell with manual scaling and optional scheduling. Non-spot.
-
- <gl-accordion-stub
- class="gl-pt-3"
- headerlevel="3"
- >
- <gl-accordion-item-stub
- class="gl-font-weight-normal"
- headerclass=""
- title="More Details"
- titlevisible="Less Details"
- >
- <p
- class="gl-pt-2"
- >
- No spot. Default choice for Windows Shell executor.
- </p>
-
- <p
- class="gl-m-0"
- >
- Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.
- </p>
- </gl-accordion-item-stub>
- </gl-accordion-stub>
- </div>
- </gl-form-radio-stub>
- <gl-form-radio-stub
- class="gl-py-5 gl-pl-8"
- value="[object Object]"
- >
- <div
- class="gl-mt-n1 gl-pl-4 gl-pb-2 gl-font-weight-bold"
- >
-
- Windows 2019 Shell with manual scaling and optional scheduling. 100% spot.
-
- <gl-accordion-stub
- class="gl-pt-3"
- headerlevel="3"
- >
- <gl-accordion-item-stub
- class="gl-font-weight-normal"
- headerclass=""
- title="More Details"
- titlevisible="Less Details"
- >
- <p
- class="gl-pt-2"
- >
- 100% spot.
- </p>
-
- <p
- class="gl-m-0"
- >
- Capacity of 1 enables warm HA through Auto Scaling group re-spawn. Capacity of 2 enables hot HA because the service is available even when a node is lost. Capacity of 3 or more enables hot HA and manual scaling of runner fleet.
- </p>
- </gl-accordion-item-stub>
- </gl-accordion-stub>
- </div>
- </gl-form-radio-stub>
- </gl-form-radio-group-stub>
-
- <p>
- <gl-sprintf-stub
- message="Don't see what you are looking for? See the full list of options, including a fully customizable option %{linkStart}here%{linkEnd}."
- />
- </p>
-</gl-modal-stub>
-`;
diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js b/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js
index a9ba4946358..c8ca75787f1 100644
--- a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js
@@ -1,30 +1,28 @@
-import { GlModal, GlFormRadio } from '@gitlab/ui';
+import { GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { getBaseURL, visitUrl } from '~/lib/utils/url_utility';
-import { mockTracking } from 'helpers/tracking_helper';
-import {
- CF_BASE_URL,
- TEMPLATES_BASE_URL,
- EASY_BUTTONS,
-} from '~/vue_shared/components/runner_aws_deployments/constants';
+import { s__ } from '~/locale';
import RunnerAwsDeploymentsModal from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue';
+import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
visitUrl: jest.fn(),
}));
+const mockModalId = 'runner-aws-deployments-modal';
+
describe('RunnerAwsDeploymentsModal', () => {
let wrapper;
const findModal = () => wrapper.findComponent(GlModal);
- const findEasyButtons = () => wrapper.findAllComponents(GlFormRadio);
+ const findRunnerAwsInstructions = () => wrapper.findComponent(RunnerAwsInstructions);
- const createComponent = () => {
+ const createComponent = (options) => {
wrapper = shallowMount(RunnerAwsDeploymentsModal, {
propsData: {
- modalId: 'runner-aws-deployments-modal',
+ modalId: mockModalId,
},
+ ...options,
});
};
@@ -36,39 +34,39 @@ describe('RunnerAwsDeploymentsModal', () => {
wrapper.destroy();
});
- it('renders the modal', () => {
- expect(wrapper.element).toMatchSnapshot();
+ it('renders modal', () => {
+ expect(findModal().props()).toMatchObject({
+ size: 'sm',
+ modalId: mockModalId,
+ title: s__('Runners|Deploy GitLab Runner in AWS'),
+ });
+ expect(findModal().attributes()).toMatchObject({
+ 'hide-footer': '',
+ });
});
- it('should contain all easy buttons', () => {
- expect(findEasyButtons()).toHaveLength(EASY_BUTTONS.length);
+ it('renders modal contents', () => {
+ expect(findRunnerAwsInstructions().exists()).toBe(true);
});
- describe('first easy button', () => {
- it('should contain the correct description', () => {
- expect(findEasyButtons().at(0).text()).toContain(EASY_BUTTONS[0].description);
- });
-
- it('should contain the correct link', () => {
- const templateUrl = encodeURIComponent(TEMPLATES_BASE_URL + EASY_BUTTONS[0].templateName);
- const { stackName } = EASY_BUTTONS[0];
- const instanceUrl = encodeURIComponent(getBaseURL());
- const url = `${CF_BASE_URL}templateURL=${templateUrl}&stackName=${stackName}&param_3GITLABRunnerInstanceURL=${instanceUrl}`;
-
- findModal().vm.$emit('primary');
+ it('when contents trigger closing, modal closes', () => {
+ const mockClose = jest.fn();
- expect(visitUrl).toHaveBeenCalledWith(url, true);
+ createComponent({
+ stubs: {
+ GlModal: {
+ template: '<div><slot/></div>',
+ methods: {
+ close: mockClose,
+ },
+ },
+ },
});
- it('should track an event when clicked', () => {
- const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ expect(mockClose).toHaveBeenCalledTimes(0);
- findModal().vm.$emit('primary');
+ findRunnerAwsInstructions().vm.$emit('close');
- expect(trackingSpy).toHaveBeenCalledTimes(1);
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
- label: EASY_BUTTONS[0].stackName,
- });
- });
+ expect(mockClose).toHaveBeenCalledTimes(1);
});
});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_docker_instructions_spec.js.snap b/spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_docker_instructions_spec.js.snap
new file mode 100644
index 00000000000..d14f66df8a1
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_docker_instructions_spec.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RunnerDockerInstructions renders contents 1`] = `"To install Runner in a container follow the instructions described in the GitLab documentation View installation instructions Close"`;
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_kubernetes_instructions_spec.js.snap b/spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_kubernetes_instructions_spec.js.snap
new file mode 100644
index 00000000000..1172bf07dff
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/__snapshots__/runner_kubernetes_instructions_spec.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`RunnerKubernetesInstructions renders contents 1`] = `"To install Runner in Kubernetes follow the instructions described in the GitLab documentation. View installation instructions Close"`;
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js
new file mode 100644
index 00000000000..4d566dbec0c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js
@@ -0,0 +1,117 @@
+import {
+ GlAccordion,
+ GlAccordionItem,
+ GlButton,
+ GlFormRadio,
+ GlFormRadioGroup,
+ GlLink,
+ GlSprintf,
+} from '@gitlab/ui';
+import { getBaseURL, visitUrl } from '~/lib/utils/url_utility';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mockTracking } from 'helpers/tracking_helper';
+import {
+ AWS_README_URL,
+ AWS_CF_BASE_URL,
+ AWS_TEMPLATES_BASE_URL,
+ AWS_EASY_BUTTONS,
+} from '~/vue_shared/components/runner_instructions/constants';
+import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
+import { __ } from '~/locale';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ visitUrl: jest.fn(),
+}));
+
+describe('RunnerAwsInstructions', () => {
+ let wrapper;
+
+ const findEasyButtonsRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
+ const findEasyButtons = () => wrapper.findAllComponents(GlFormRadio);
+ const findEasyButtonAt = (i) => findEasyButtons().at(i);
+ const findLink = () => wrapper.findComponent(GlLink);
+ const findOkButton = () =>
+ wrapper
+ .findAllComponents(GlButton)
+ .filter((w) => w.props('variant') === 'confirm')
+ .at(0);
+ const findCloseButton = () => wrapper.findByText(__('Close'));
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(RunnerAwsInstructions, {
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should contain every button', () => {
+ expect(findEasyButtons()).toHaveLength(AWS_EASY_BUTTONS.length);
+ });
+
+ const AWS_EASY_BUTTONS_PARAMS = AWS_EASY_BUTTONS.map((val, idx) => ({ ...val, idx }));
+
+ describe.each(AWS_EASY_BUTTONS_PARAMS)(
+ 'easy button %#',
+ ({ idx, description, moreDetails1, moreDetails2, templateName, stackName }) => {
+ it('should contain button description', () => {
+ const text = findEasyButtonAt(idx).text();
+
+ expect(text).toContain(description);
+ expect(text).toContain(moreDetails1);
+ expect(text).toContain(moreDetails2);
+ });
+
+ it('should show more details', () => {
+ const accordion = findEasyButtonAt(idx).findComponent(GlAccordion);
+ const accordionItem = accordion.findComponent(GlAccordionItem);
+
+ expect(accordion.props('headerLevel')).toBe(3);
+ expect(accordionItem.props('title')).toBe(__('More Details'));
+ expect(accordionItem.props('titleVisible')).toBe(__('Less Details'));
+ });
+
+ describe('when clicked', () => {
+ let trackingSpy;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ findEasyButtonsRadioGroup().vm.$emit('input', idx);
+ findOkButton().vm.$emit('click');
+ });
+
+ it('should contain the correct link', () => {
+ const templateUrl = encodeURIComponent(AWS_TEMPLATES_BASE_URL + templateName);
+ const instanceUrl = encodeURIComponent(getBaseURL());
+ const url = `${AWS_CF_BASE_URL}templateURL=${templateUrl}&stackName=${stackName}&param_3GITLABRunnerInstanceURL=${instanceUrl}`;
+
+ expect(visitUrl).toHaveBeenCalledTimes(1);
+ expect(visitUrl).toHaveBeenCalledWith(url, true);
+ });
+
+ it('should track an event when clicked', () => {
+ expect(trackingSpy).toHaveBeenCalledTimes(1);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'template_clicked', {
+ label: stackName,
+ });
+ });
+ });
+ },
+ );
+
+ it('displays link with more information', () => {
+ expect(findLink().attributes('href')).toBe(AWS_README_URL);
+ });
+
+ it('triggers the modal to close', () => {
+ findCloseButton().vm.$emit('click');
+
+ expect(wrapper.emitted('close')).toHaveLength(1);
+ });
+});
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
new file mode 100644
index 00000000000..f9d700fe67f
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_cli_instructions_spec.js
@@ -0,0 +1,169 @@
+import { GlAlert, 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 { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/get_runner_setup.query.graphql';
+import RunnerCliInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue';
+
+import { mockRunnerPlatforms, mockInstructions, mockInstructionsWindows } from '../mock_data';
+
+Vue.use(VueApollo);
+
+jest.mock('@gitlab/ui/dist/utils');
+
+const mockPlatforms = mockRunnerPlatforms.data.runnerPlatforms.nodes.map(
+ ({ name, humanReadableName, architectures }) => ({
+ name,
+ humanReadableName,
+ architectures: architectures?.nodes || [],
+ }),
+);
+
+const [mockPlatform, mockPlatform2] = mockPlatforms;
+const mockArchitectures = mockPlatform.architectures;
+
+describe('RunnerCliInstructions component', () => {
+ let wrapper;
+ let fakeApollo;
+ let runnerSetupInstructionsHandler;
+
+ const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findArchitectureDropdownItems = () => wrapper.findAllByTestId('architecture-dropdown-item');
+ const findBinaryDownloadButton = () => wrapper.findByTestId('binary-download-button');
+ const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions');
+ const findRegisterCommand = () => wrapper.findByTestId('register-command');
+
+ const createComponent = ({ props, ...options } = {}) => {
+ const requestHandlers = [[getRunnerSetupInstructionsQuery, runnerSetupInstructionsHandler]];
+
+ fakeApollo = createMockApollo(requestHandlers);
+
+ wrapper = extendedWrapper(
+ shallowMount(RunnerCliInstructions, {
+ propsData: {
+ platform: mockPlatform,
+ registrationToken: 'MY_TOKEN',
+ ...props,
+ },
+ apolloProvider: fakeApollo,
+ ...options,
+ }),
+ );
+ };
+
+ beforeEach(() => {
+ runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockInstructions);
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ describe('when the instructions are shown', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('should not show alert', async () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+
+ it('should contain a number of dropdown items for the architecture options', () => {
+ expect(findArchitectureDropdownItems()).toHaveLength(
+ mockRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
+ );
+ });
+
+ describe('should display instructions', () => {
+ const { installInstructions } = mockInstructions.data.runnerSetup;
+
+ it('runner instructions are requested', () => {
+ expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
+ platform: 'linux',
+ architecture: 'amd64',
+ });
+ });
+
+ it('binary instructions are shown', async () => {
+ const instructions = findBinaryInstructions().text();
+
+ expect(instructions).toBe(installInstructions.trim());
+ });
+
+ it('register command is shown with a replaced token', async () => {
+ const command = findRegisterCommand().text();
+
+ expect(command).toBe(
+ 'sudo gitlab-runner register --url http://localhost/ --registration-token MY_TOKEN',
+ );
+ });
+
+ it('architecture download link is shown', () => {
+ expect(findBinaryDownloadButton().attributes('href')).toBe(
+ mockArchitectures[0].downloadLocation,
+ );
+ });
+ });
+
+ describe('after another platform and architecture are selected', () => {
+ beforeEach(async () => {
+ runnerSetupInstructionsHandler.mockResolvedValue(mockInstructionsWindows);
+
+ findArchitectureDropdownItems().at(1).vm.$emit('click');
+
+ wrapper.setProps({ platform: mockPlatform2 });
+ await waitForPromises();
+ });
+
+ it('runner instructions are requested', () => {
+ expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
+ platform: mockPlatform2.name,
+ architecture: mockPlatform2.architectures[0].name,
+ });
+ });
+ });
+ });
+
+ describe('when a register token is not known', () => {
+ beforeEach(async () => {
+ createComponent({ props: { registrationToken: undefined } });
+ await waitForPromises();
+ });
+
+ it('register command is shown without a defined registration token', () => {
+ const instructions = findRegisterCommand().text();
+
+ expect(instructions).toBe(mockInstructions.data.runnerSetup.registerInstructions);
+ });
+ });
+
+ describe('when apollo is loading', () => {
+ it('should show a loading icon', async () => {
+ createComponent();
+
+ expect(findGlLoadingIcon().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findGlLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('when instructions cannot be loaded', () => {
+ beforeEach(async () => {
+ runnerSetupInstructionsHandler.mockRejectedValue();
+
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('should show alert', () => {
+ expect(wrapper.emitted()).toEqual({ error: [[]] });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js
new file mode 100644
index 00000000000..2922d261b24
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_docker_instructions_spec.js
@@ -0,0 +1,28 @@
+import { shallowMount } from '@vue/test-utils';
+
+import { GlButton } from '@gitlab/ui';
+import RunnerDockerInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue';
+
+describe('RunnerDockerInstructions', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(RunnerDockerInstructions, {});
+ };
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders contents', () => {
+ expect(wrapper.text().replace(/\s+/g, ' ')).toMatchSnapshot();
+ });
+
+ it('renders link', () => {
+ expect(findButton().attributes('href')).toBe(
+ 'https://docs.gitlab.com/runner/install/docker.html',
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js
new file mode 100644
index 00000000000..0bfcc0e3d86
--- /dev/null
+++ b/spec/frontend/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions_spec.js
@@ -0,0 +1,28 @@
+import { shallowMount } from '@vue/test-utils';
+
+import { GlButton } from '@gitlab/ui';
+import RunnerKubernetesInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue';
+
+describe('RunnerKubernetesInstructions', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMount(RunnerKubernetesInstructions, {});
+ };
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders contents', () => {
+ expect(wrapper.text().replace(/\s+/g, ' ')).toMatchSnapshot();
+ });
+
+ it('renders link', () => {
+ expect(findButton().attributes('href')).toBe(
+ 'https://docs.gitlab.com/runner/install/kubernetes.html',
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/runner_instructions/mock_data.js b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
index 79cacadd6af..add334f166c 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/mock_data.js
@@ -1,5 +1,5 @@
-import mockGraphqlRunnerPlatforms from 'test_fixtures/graphql/runner_instructions/get_runner_platforms.query.graphql.json';
-import mockGraphqlInstructions from 'test_fixtures/graphql/runner_instructions/get_runner_setup.query.graphql.json';
-import mockGraphqlInstructionsWindows from 'test_fixtures/graphql/runner_instructions/get_runner_setup.query.graphql.windows.json';
+import mockRunnerPlatforms from 'test_fixtures/graphql/runner_instructions/get_runner_platforms.query.graphql.json';
+import mockInstructions from 'test_fixtures/graphql/runner_instructions/get_runner_setup.query.graphql.json';
+import mockInstructionsWindows from 'test_fixtures/graphql/runner_instructions/get_runner_setup.query.graphql.windows.json';
-export { mockGraphqlRunnerPlatforms, mockGraphqlInstructions, mockGraphqlInstructionsWindows };
+export { mockRunnerPlatforms, mockInstructions, mockInstructionsWindows };
diff --git a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
index ae9157591c5..19f2dd137ff 100644
--- a/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
+++ b/spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlModal, GlButton, GlLoadingIcon, GlSkeletonLoader } from '@gitlab/ui';
+import { GlAlert, GlModal, GlButton, GlSkeletonLoader } from '@gitlab/ui';
import { GlBreakpointInstance as bp } from '@gitlab/ui/dist/utils';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
@@ -6,15 +6,13 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_platforms.query.graphql';
-import getRunnerSetupInstructionsQuery from '~/vue_shared/components/runner_instructions/graphql/queries/get_runner_setup.query.graphql';
+import getRunnerPlatformsQuery from '~/vue_shared/components/runner_instructions/graphql/get_runner_platforms.query.graphql';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import RunnerCliInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_cli_instructions.vue';
+import RunnerDockerInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_docker_instructions.vue';
+import RunnerKubernetesInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_kubernetes_instructions.vue';
-import {
- mockGraphqlRunnerPlatforms,
- mockGraphqlInstructions,
- mockGraphqlInstructionsWindows,
-} from './mock_data';
+import { mockRunnerPlatforms } from './mock_data';
Vue.use(VueApollo);
@@ -40,24 +38,16 @@ describe('RunnerInstructionsModal component', () => {
let wrapper;
let fakeApollo;
let runnerPlatformsHandler;
- let runnerSetupInstructionsHandler;
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
- const findGlLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findAlert = () => wrapper.findComponent(GlAlert);
const findModal = () => wrapper.findComponent(GlModal);
const findPlatformButtonGroup = () => wrapper.findByTestId('platform-buttons');
const findPlatformButtons = () => findPlatformButtonGroup().findAllComponents(GlButton);
- const findArchitectureDropdownItems = () => wrapper.findAllByTestId('architecture-dropdown-item');
- const findBinaryDownloadButton = () => wrapper.findByTestId('binary-download-button');
- const findBinaryInstructions = () => wrapper.findByTestId('binary-instructions');
- const findRegisterCommand = () => wrapper.findByTestId('register-command');
+ const findRunnerCliInstructions = () => wrapper.findComponent(RunnerCliInstructions);
const createComponent = ({ props, shown = true, ...options } = {}) => {
- const requestHandlers = [
- [getRunnerPlatformsQuery, runnerPlatformsHandler],
- [getRunnerSetupInstructionsQuery, runnerSetupInstructionsHandler],
- ];
+ const requestHandlers = [[getRunnerPlatformsQuery, runnerPlatformsHandler]];
fakeApollo = createMockApollo(requestHandlers);
@@ -80,8 +70,7 @@ describe('RunnerInstructionsModal component', () => {
};
beforeEach(() => {
- runnerPlatformsHandler = jest.fn().mockResolvedValue(mockGraphqlRunnerPlatforms);
- runnerSetupInstructionsHandler = jest.fn().mockResolvedValue(mockGraphqlInstructions);
+ runnerPlatformsHandler = jest.fn().mockResolvedValue(mockRunnerPlatforms);
});
afterEach(() => {
@@ -103,90 +92,15 @@ describe('RunnerInstructionsModal component', () => {
const buttons = findPlatformButtons();
- expect(buttons).toHaveLength(mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes.length);
+ expect(buttons).toHaveLength(mockRunnerPlatforms.data.runnerPlatforms.nodes.length);
});
- it('should contain a number of dropdown items for the architecture options', () => {
- expect(findArchitectureDropdownItems()).toHaveLength(
- mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes.length,
- );
- });
-
- describe('should display default instructions', () => {
- const { installInstructions } = mockGraphqlInstructions.data.runnerSetup;
-
- it('runner instructions are requested', () => {
- expect(runnerSetupInstructionsHandler).toHaveBeenCalledWith({
- platform: 'linux',
- architecture: 'amd64',
- });
- });
-
- it('binary instructions are shown', async () => {
- const instructions = findBinaryInstructions().text();
-
- expect(instructions).toBe(installInstructions.trim());
- });
-
- it('register command is shown with a replaced token', async () => {
- const command = findRegisterCommand().text();
-
- expect(command).toBe(
- 'sudo gitlab-runner register --url http://localhost/ --registration-token MY_TOKEN',
- );
- });
- });
-
- describe('after a platform and architecture are selected', () => {
- const windowsIndex = 2;
- const { installInstructions } = mockGraphqlInstructionsWindows.data.runnerSetup;
-
- beforeEach(async () => {
- runnerSetupInstructionsHandler.mockResolvedValue(mockGraphqlInstructionsWindows);
-
- findPlatformButtons().at(windowsIndex).vm.$emit('click');
- await waitForPromises();
- });
+ it('should display architecture options', () => {
+ const { architectures } = findRunnerCliInstructions().props('platform');
- it('runner instructions are requested', () => {
- expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
- platform: 'windows',
- architecture: 'amd64',
- });
- });
-
- it('architecture download link is updated', () => {
- const architectures =
- mockGraphqlRunnerPlatforms.data.runnerPlatforms.nodes[windowsIndex].architectures.nodes;
-
- expect(findBinaryDownloadButton().attributes('href')).toBe(
- architectures[0].downloadLocation,
- );
- });
-
- it('other binary instructions are shown', () => {
- const instructions = findBinaryInstructions().text();
-
- expect(instructions).toBe(installInstructions.trim());
- });
-
- it('register command is shown', () => {
- const command = findRegisterCommand().text();
-
- expect(command).toBe(
- './gitlab-runner.exe register --url http://localhost/ --registration-token MY_TOKEN',
- );
- });
-
- it('runner instructions are requested with another architecture', async () => {
- findArchitectureDropdownItems().at(1).vm.$emit('click');
- await waitForPromises();
-
- expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
- platform: 'windows',
- architecture: '386',
- });
- });
+ expect(architectures).toEqual(
+ mockRunnerPlatforms.data.runnerPlatforms.nodes[0].architectures.nodes,
+ );
});
describe('when the modal resizes', () => {
@@ -206,16 +120,14 @@ describe('RunnerInstructionsModal component', () => {
});
});
- describe('when a register token is not known', () => {
+ describe.each([null, 'DEFINED'])('when registration token is %p', (token) => {
beforeEach(async () => {
- createComponent({ props: { registrationToken: undefined } });
+ createComponent({ props: { registrationToken: token } });
await waitForPromises();
});
it('register command is shown without a defined registration token', () => {
- const instructions = findRegisterCommand().text();
-
- expect(instructions).toBe(mockGraphqlInstructions.data.runnerSetup.registerInstructions);
+ expect(findRunnerCliInstructions().props('registrationToken')).toBe(token);
});
});
@@ -225,21 +137,33 @@ describe('RunnerInstructionsModal component', () => {
await waitForPromises();
});
- it('runner instructions for the default selected platform are requested', () => {
- expect(runnerSetupInstructionsHandler).toHaveBeenLastCalledWith({
- platform: 'osx',
- architecture: 'amd64',
- });
+ it('should preselect', () => {
+ const selected = findPlatformButtons()
+ .filter((btn) => btn.props('selected'))
+ .at(0);
+
+ expect(selected.text()).toBe('macOS');
});
- it('sets the focus on the default selected platform', () => {
- const findOsxPlatformButton = () => wrapper.findComponent({ ref: 'osx' });
+ it('runner instructions for the default selected platform are requested', () => {
+ const { name } = findRunnerCliInstructions().props('platform');
- findOsxPlatformButton().element.focus = jest.fn();
+ expect(name).toBe('osx');
+ });
+ });
- findModal().vm.$emit('shown');
+ describe.each`
+ platform | component
+ ${'docker'} | ${RunnerDockerInstructions}
+ ${'kubernetes'} | ${RunnerKubernetesInstructions}
+ `('with platform "$platform"', ({ platform, component }) => {
+ beforeEach(async () => {
+ createComponent({ props: { defaultPlatformName: platform } });
+ await waitForPromises();
+ });
- expect(findOsxPlatformButton().element.focus).toHaveBeenCalled();
+ it(`runner instructions for ${platform} are shown`, () => {
+ expect(wrapper.findComponent(component).exists()).toBe(true);
});
});
@@ -251,7 +175,6 @@ describe('RunnerInstructionsModal component', () => {
it('does not fetch instructions', () => {
expect(runnerPlatformsHandler).not.toHaveBeenCalled();
- expect(runnerSetupInstructionsHandler).not.toHaveBeenCalled();
});
});
@@ -259,43 +182,41 @@ describe('RunnerInstructionsModal component', () => {
it('should show a skeleton loader', async () => {
createComponent();
await nextTick();
- await nextTick();
expect(findSkeletonLoader().exists()).toBe(true);
- expect(findGlLoadingIcon().exists()).toBe(false);
-
- // wait on fetch of both `platforms` and `instructions`
- await nextTick();
- await nextTick();
-
- expect(findGlLoadingIcon().exists()).toBe(true);
});
it('once loaded, should not show a loading state', async () => {
createComponent();
-
await waitForPromises();
expect(findSkeletonLoader().exists()).toBe(false);
- expect(findGlLoadingIcon().exists()).toBe(false);
});
});
- describe('when instructions cannot be loaded', () => {
- beforeEach(async () => {
- runnerSetupInstructionsHandler.mockRejectedValue();
+ describe('errors', () => {
+ it('should show an alert when platforms cannot be loaded', async () => {
+ runnerPlatformsHandler.mockRejectedValue();
createComponent();
await waitForPromises();
- });
- it('should show alert', () => {
expect(findAlert().exists()).toBe(true);
});
- it('should not show instructions', () => {
- expect(findBinaryInstructions().exists()).toBe(false);
- expect(findRegisterCommand().exists()).toBe(false);
+ it('should show alert when instructions cannot be loaded', async () => {
+ createComponent();
+ await waitForPromises();
+
+ findRunnerCliInstructions().vm.$emit('error');
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+
+ findAlert().vm.$emit('dismiss');
+ await nextTick();
+
+ expect(findAlert().exists()).toBe(false);
});
});
@@ -312,14 +233,16 @@ describe('RunnerInstructionsModal component', () => {
describe('show()', () => {
let mockShow;
+ let mockClose;
beforeEach(() => {
mockShow = jest.fn();
+ mockClose = jest.fn();
createComponent({
shown: false,
stubs: {
- GlModal: getGlModalStub({ show: mockShow }),
+ GlModal: getGlModalStub({ show: mockShow, close: mockClose }),
},
});
});
@@ -329,6 +252,12 @@ describe('RunnerInstructionsModal component', () => {
expect(mockShow).toHaveBeenCalledTimes(1);
});
+
+ it('delegates close()', () => {
+ wrapper.vm.close();
+
+ expect(mockClose).toHaveBeenCalledTimes(1);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
index 33f370efdfa..5461d38599d 100644
--- a/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js
@@ -90,6 +90,17 @@ describe('Source Viewer component', () => {
});
});
+ describe('legacy fallbacks', () => {
+ it('tracks a fallback event and emits an error when viewing python files', () => {
+ const fallbackLanguage = 'python';
+ const eventData = { label: EVENT_LABEL_FALLBACK, property: fallbackLanguage };
+ createComponent({ language: fallbackLanguage });
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
+
describe('highlight.js', () => {
beforeEach(() => createComponent({ language: mappedLanguage }));
@@ -114,10 +125,10 @@ describe('Source Viewer component', () => {
});
it('correctly maps languages starting with uppercase', async () => {
- await createComponent({ language: 'Python3' });
- const languageDefinition = await import(`highlight.js/lib/languages/python`);
+ await createComponent({ language: 'Ruby' });
+ const languageDefinition = await import(`highlight.js/lib/languages/ruby`);
- expect(hljs.registerLanguage).toHaveBeenCalledWith('python', languageDefinition.default);
+ expect(hljs.registerLanguage).toHaveBeenCalledWith('ruby', languageDefinition.default);
});
it('highlights the first chunk', () => {
diff --git a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
index e5f56c63031..c8351ed61d7 100644
--- a/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
+++ b/spec/frontend/vue_shared/components/timezone_dropdown/timezone_dropdown_spec.js
@@ -1,4 +1,5 @@
import { GlDropdownItem, GlDropdown } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import TimezoneDropdown from '~/vue_shared/components/timezone_dropdown/timezone_dropdown.vue';
import { formatTimezone } from '~/lib/utils/datetime_utility';
@@ -105,7 +106,14 @@ describe('Deploy freeze timezone dropdown', () => {
});
it('renders selected time zone as dropdown label', () => {
- expect(wrapper.findComponent(GlDropdown).props().text).toBe('[UTC + 2] Berlin');
+ expect(wrapper.findComponent(GlDropdown).props().text).toBe('[UTC+2] Berlin');
+ });
+
+ it('adds a checkmark to the selected option', async () => {
+ const selectedTZOption = findAllDropdownItems().at(0);
+ selectedTZOption.vm.$emit('click');
+ await nextTick();
+ expect(selectedTZOption.attributes('ischecked')).toBe('true');
});
});
});
diff --git a/spec/frontend/vue_shared/components/web_ide_link_spec.js b/spec/frontend/vue_shared/components/web_ide_link_spec.js
index 3b0f0fe6e73..2a0d2089fe3 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -7,15 +7,19 @@ import WebIdeLink, {
i18n,
PREFERRED_EDITOR_RESET_KEY,
PREFERRED_EDITOR_KEY,
- KEY_WEB_IDE,
} from '~/vue_shared/components/web_ide_link.vue';
import ConfirmForkModal from '~/vue_shared/components/confirm_fork_modal.vue';
import UserCalloutDismisser from '~/vue_shared/components/user_callout_dismisser.vue';
+import { KEY_WEB_IDE } from '~/vue_shared/components/constants';
import { stubComponent } from 'helpers/stub_component';
import { shallowMountExtended, mountExtended } from 'helpers/vue_test_utils_helper';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { visitUrl } from '~/lib/utils/url_utility';
+
+jest.mock('~/lib/utils/url_utility');
+
const TEST_EDIT_URL = '/gitlab-test/test/-/edit/main/';
const TEST_WEB_IDE_URL = '/-/ide/project/gitlab-test/test/edit/main/-/';
const TEST_GITPOD_URL = 'https://gitpod.test/';
@@ -52,6 +56,7 @@ const ACTION_WEB_IDE = {
'data-track-action': 'click_consolidated_edit_ide',
'data-track-label': 'web_ide',
},
+ handle: expect.any(Function),
};
const ACTION_WEB_IDE_CONFIRM_FORK = {
...ACTION_WEB_IDE,
@@ -258,6 +263,14 @@ describe('Web IDE link component', () => {
selectedKey: ACTION_PIPELINE_EDITOR.key,
});
});
+
+ it('when web ide button is clicked it opens in a new tab', async () => {
+ findActionsButton().props('actions')[1].handle({
+ preventDefault: jest.fn(),
+ });
+ await nextTick();
+ expect(visitUrl).toHaveBeenCalledWith(ACTION_WEB_IDE.href, true);
+ });
});
describe('with multiple actions', () => {
diff --git a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
index e5594b6d37e..159be4cd1ef 100644
--- a/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
+++ b/spec/frontend/vue_shared/issuable/show/components/issuable_edit_form_spec.js
@@ -5,9 +5,12 @@ import { nextTick } from 'vue';
import IssuableEditForm from '~/vue_shared/issuable/show/components/issuable_edit_form.vue';
import IssuableEventHub from '~/vue_shared/issuable/show/event_hub';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import Autosave from '~/autosave';
import { mockIssuableShowProps, mockIssuable } from '../mock_data';
+jest.mock('~/autosave');
+
const issuableEditFormProps = {
issuable: mockIssuable,
...mockIssuableShowProps,
@@ -36,10 +39,12 @@ describe('IssuableEditForm', () => {
beforeEach(() => {
wrapper = createComponent();
+ jest.spyOn(Autosave.prototype, 'reset');
});
afterEach(() => {
wrapper.destroy();
+ jest.resetAllMocks();
});
describe('watch', () => {
@@ -100,21 +105,18 @@ describe('IssuableEditForm', () => {
describe('methods', () => {
describe('initAutosave', () => {
- it('initializes `autosaveTitle` and `autosaveDescription` props', () => {
- expect(wrapper.vm.autosaveTitle).toBeDefined();
- expect(wrapper.vm.autosaveDescription).toBeDefined();
+ it('initializes autosave', () => {
+ expect(Autosave.mock.calls).toEqual([
+ [expect.any(Element), ['/', '', 'title']],
+ [expect.any(Element), ['/', '', 'description']],
+ ]);
});
});
describe('resetAutosave', () => {
- it('calls `reset` on `autosaveTitle` and `autosaveDescription` props', () => {
- jest.spyOn(wrapper.vm.autosaveTitle, 'reset').mockImplementation(jest.fn);
- jest.spyOn(wrapper.vm.autosaveDescription, 'reset').mockImplementation(jest.fn);
-
- wrapper.vm.resetAutosave();
-
- expect(wrapper.vm.autosaveTitle.reset).toHaveBeenCalled();
- expect(wrapper.vm.autosaveDescription.reset).toHaveBeenCalled();
+ it('resets title and description on "update.issuable event"', () => {
+ IssuableEventHub.$emit('update.issuable');
+ expect(Autosave.prototype.reset.mock.calls).toEqual([[], []]);
});
});
});
diff --git a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_body_spec.js.snap b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_body_spec.js.snap
new file mode 100644
index 00000000000..52838dcd0bc
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_body_spec.js.snap
@@ -0,0 +1,9 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Work Item Note Body should have the wrapper to show the note body 1`] = `
+"<div data-testid=\\"work-item-note-body\\" class=\\"note-text md\\">
+ <p dir=\\"auto\\" data-sourcepos=\\"1:1-1:76\\">
+ <gl-emoji data-unicode-version=\\"6.0\\" data-name=\\"wave\\" title=\\"waving hand sign\\">👋</gl-emoji> Hi <a title=\\"Sherie Nitzsche\\" class=\\"gfm gfm-project_member js-user-link\\" data-placement=\\"top\\" data-container=\\"body\\" data-user=\\"3\\" data-reference-type=\\"user\\" href=\\"/fredda.brekke\\">@fredda.brekke</a> How are you ? what do you think about this ? <gl-emoji data-unicode-version=\\"6.0\\" data-name=\\"pray\\" title=\\"person with folded hands\\">🙏</gl-emoji>
+ </p>
+</div>"
+`;
diff --git a/spec/frontend/work_items/components/notes/activity_filter_spec.js b/spec/frontend/work_items/components/notes/activity_filter_spec.js
new file mode 100644
index 00000000000..eb4bcbf942b
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/activity_filter_spec.js
@@ -0,0 +1,74 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
+import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
+import { ASC, DESC } from '~/notes/constants';
+
+import { mockTracking } from 'helpers/tracking_helper';
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+
+describe('Activity Filter', () => {
+ let wrapper;
+
+ const findLocalStorageSync = () => wrapper.findComponent(LocalStorageSync);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ const findNewestFirstItem = () => wrapper.findByTestId('js-newest-first');
+
+ const createComponent = ({ sortOrder = ASC, loading = false, workItemType = 'Task' } = {}) => {
+ wrapper = shallowMountExtended(ActivityFilter, {
+ propsData: {
+ sortOrder,
+ loading,
+ workItemType,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('default', () => {
+ it('has a dropdown with 2 options', () => {
+ expect(findDropdown().exists()).toBe(true);
+ expect(findAllDropdownItems()).toHaveLength(ActivityFilter.SORT_OPTIONS.length);
+ });
+
+ it('has local storage sync with the correct props', () => {
+ expect(findLocalStorageSync().props('asString')).toBe(true);
+ });
+
+ it('emits `updateSavedSortOrder` event when update is emitted', async () => {
+ findLocalStorageSync().vm.$emit('input', ASC);
+
+ await nextTick();
+ expect(wrapper.emitted('updateSavedSortOrder')).toHaveLength(1);
+ expect(wrapper.emitted('updateSavedSortOrder')).toEqual([[ASC]]);
+ });
+ });
+
+ describe('when asc', () => {
+ describe('when the dropdown is clicked', () => {
+ it('calls the right actions', async () => {
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+ findNewestFirstItem().vm.$emit('click');
+ await nextTick();
+
+ expect(wrapper.emitted('changeSortOrder')).toHaveLength(1);
+ expect(wrapper.emitted('changeSortOrder')).toEqual([[DESC]]);
+
+ expect(trackingSpy).toHaveBeenCalledWith(
+ TRACKING_CATEGORY_SHOW,
+ 'notes_sort_order_changed',
+ {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_track_notes_sorting',
+ property: 'type_Task',
+ },
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_body_spec.js b/spec/frontend/work_items/components/notes/work_item_note_body_spec.js
new file mode 100644
index 00000000000..4fcbcfcaf30
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_note_body_spec.js
@@ -0,0 +1,32 @@
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WorkItemNoteBody from '~/work_items/components/notes/work_item_note_body.vue';
+import NoteEditedText from '~/notes/components/note_edited_text.vue';
+import { mockWorkItemCommentNote } from 'jest/work_items/mock_data';
+
+describe('Work Item Note Body', () => {
+ let wrapper;
+
+ const findNoteBody = () => wrapper.findByTestId('work-item-note-body');
+ const findNoteEditedText = () => wrapper.findComponent(NoteEditedText);
+
+ const createComponent = ({ note = mockWorkItemCommentNote } = {}) => {
+ wrapper = shallowMountExtended(WorkItemNoteBody, {
+ propsData: {
+ note,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should have the wrapper to show the note body', () => {
+ expect(findNoteBody().exists()).toBe(true);
+ expect(findNoteBody().html()).toMatchSnapshot();
+ });
+
+ it('should not show the edited text when the value is not present', () => {
+ expect(findNoteEditedText().exists()).toBe(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
new file mode 100644
index 00000000000..7257d5c8023
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -0,0 +1,53 @@
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
+import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
+import NoteHeader from '~/notes/components/note_header.vue';
+import { mockWorkItemCommentNote } from 'jest/work_items/mock_data';
+
+describe('Work Item Note', () => {
+ let wrapper;
+
+ const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
+ const findNoteHeader = () => wrapper.findComponent(NoteHeader);
+ const findNoteBody = () => wrapper.findComponent(NoteBody);
+ const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+
+ const createComponent = ({ note = mockWorkItemCommentNote } = {}) => {
+ wrapper = shallowMount(WorkItemNote, {
+ propsData: {
+ note,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('Should be wrapped inside the timeline entry item', () => {
+ expect(findTimelineEntryItem().exists()).toBe(true);
+ });
+
+ it('should have the author avatar of the work item note', () => {
+ expect(findAvatarLink().exists()).toBe(true);
+ expect(findAvatarLink().attributes('href')).toBe(mockWorkItemCommentNote.author.webUrl);
+
+ expect(findAvatar().exists()).toBe(true);
+ expect(findAvatar().props('src')).toBe(mockWorkItemCommentNote.author.avatarUrl);
+ expect(findAvatar().props('entityName')).toBe(mockWorkItemCommentNote.author.username);
+ });
+
+ it('has note header', () => {
+ expect(findNoteHeader().exists()).toBe(true);
+ expect(findNoteHeader().props('author')).toEqual(mockWorkItemCommentNote.author);
+ expect(findNoteHeader().props('createdAt')).toBe(mockWorkItemCommentNote.createdAt);
+ });
+
+ it('has note body', () => {
+ expect(findNoteBody().exists()).toBe(true);
+ expect(findNoteBody().props('note')).toEqual(mockWorkItemCommentNote);
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_comment_form_spec.js b/spec/frontend/work_items/components/work_item_comment_form_spec.js
new file mode 100644
index 00000000000..07c00119398
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_comment_form_spec.js
@@ -0,0 +1,205 @@
+import { GlButton } 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 { mockTracking } from 'helpers/tracking_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { updateDraft } from '~/lib/utils/autosave';
+import MarkdownField from '~/vue_shared/components/markdown/field.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue';
+import WorkItemCommentLocked from '~/work_items/components/work_item_comment_locked.vue';
+import createNoteMutation from '~/work_items/graphql/create_work_item_note.mutation.graphql';
+import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import {
+ workItemResponseFactory,
+ workItemQueryResponse,
+ projectWorkItemResponse,
+ createWorkItemNoteResponse,
+} from '../mock_data';
+
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
+jest.mock('~/lib/utils/autosave');
+
+const workItemId = workItemQueryResponse.data.workItem.id;
+
+describe('WorkItemCommentForm', () => {
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ const mutationSuccessHandler = jest.fn().mockResolvedValue(createWorkItemNoteResponse);
+ const workItemByIidResponseHandler = jest.fn().mockResolvedValue(projectWorkItemResponse);
+ let workItemResponseHandler;
+
+ const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
+
+ const setText = (newText) => {
+ return findMarkdownEditor().vm.$emit('input', newText);
+ };
+
+ const clickSave = () =>
+ wrapper
+ .findAllComponents(GlButton)
+ .filter((button) => button.text().startsWith('Comment'))
+ .at(0)
+ .vm.$emit('click', {});
+
+ const createComponent = async ({
+ mutationHandler = mutationSuccessHandler,
+ canUpdate = true,
+ workItemResponse = workItemResponseFactory({ canUpdate }),
+ queryVariables = { id: workItemId },
+ fetchByIid = false,
+ signedIn = true,
+ isEditing = true,
+ } = {}) => {
+ workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
+
+ if (signedIn) {
+ window.gon.current_user_id = '1';
+ window.gon.current_user_avatar_url = 'avatar.png';
+ }
+
+ const { id } = workItemQueryResponse.data.workItem;
+ wrapper = shallowMount(WorkItemCommentForm, {
+ apolloProvider: createMockApollo([
+ [workItemQuery, workItemResponseHandler],
+ [createNoteMutation, mutationHandler],
+ [workItemByIidQuery, workItemByIidResponseHandler],
+ ]),
+ propsData: {
+ workItemId: id,
+ fullPath: 'test-project-path',
+ queryVariables,
+ fetchByIid,
+ },
+ stubs: {
+ MarkdownField,
+ WorkItemCommentLocked,
+ },
+ });
+
+ await waitForPromises();
+
+ if (isEditing) {
+ wrapper.findComponent(GlButton).vm.$emit('click');
+ }
+ };
+
+ describe('adding a comment', () => {
+ it('calls update widgets mutation', async () => {
+ const noteText = 'updated desc';
+
+ await createComponent({
+ isEditing: true,
+ signedIn: true,
+ });
+
+ setText(noteText);
+
+ clickSave();
+
+ await waitForPromises();
+
+ expect(mutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ noteableId: workItemId,
+ body: noteText,
+ },
+ });
+ });
+
+ it('tracks adding comment', async () => {
+ await createComponent();
+ const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ setText('test');
+
+ clickSave();
+
+ await waitForPromises();
+
+ expect(trackingSpy).toHaveBeenCalledWith(TRACKING_CATEGORY_SHOW, 'add_work_item_comment', {
+ category: TRACKING_CATEGORY_SHOW,
+ label: 'item_comment',
+ property: 'type_Task',
+ });
+ });
+
+ it('emits error when mutation returns error', async () => {
+ const error = 'eror';
+
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockResolvedValue({
+ data: {
+ createNote: {
+ note: null,
+ errors: [error],
+ },
+ },
+ }),
+ });
+
+ setText('updated desc');
+
+ clickSave();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
+
+ it('emits error when mutation fails', async () => {
+ const error = 'eror';
+
+ await createComponent({
+ isEditing: true,
+ mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
+ });
+
+ setText('updated desc');
+
+ clickSave();
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([[error]]);
+ });
+
+ it('autosaves', async () => {
+ await createComponent({
+ isEditing: true,
+ });
+
+ setText('updated');
+
+ expect(updateDraft).toHaveBeenCalled();
+ });
+ });
+
+ it('calls the global ID work item query when `fetchByIid` prop is false', async () => {
+ createComponent({ fetchByIid: false });
+ await waitForPromises();
+
+ expect(workItemResponseHandler).toHaveBeenCalled();
+ expect(workItemByIidResponseHandler).not.toHaveBeenCalled();
+ });
+
+ it('calls the IID work item query when when `fetchByIid` prop is true', async () => {
+ await createComponent({ fetchByIid: true, isEditing: false });
+
+ expect(workItemResponseHandler).not.toHaveBeenCalled();
+ expect(workItemByIidResponseHandler).toHaveBeenCalled();
+ });
+
+ it('skips calling the handlers when missing the needed queryVariables', async () => {
+ await createComponent({ queryVariables: {}, fetchByIid: false, isEditing: false });
+
+ expect(workItemResponseHandler).not.toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_comment_locked_spec.js b/spec/frontend/work_items/components/work_item_comment_locked_spec.js
new file mode 100644
index 00000000000..58491c4b09c
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_comment_locked_spec.js
@@ -0,0 +1,41 @@
+import { GlLink, GlIcon } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import WorkItemCommentLocked from '~/work_items/components/work_item_comment_locked.vue';
+
+const createComponent = ({ workItemType = 'Task', isProjectArchived = false } = {}) =>
+ shallowMount(WorkItemCommentLocked, {
+ propsData: {
+ workItemType,
+ isProjectArchived,
+ },
+ });
+
+describe('WorkItemCommentLocked', () => {
+ let wrapper;
+ const findLockedIcon = () => wrapper.findComponent(GlIcon);
+ const findLearnMoreLink = () => wrapper.findComponent(GlLink);
+
+ it('renders the locked icon', () => {
+ wrapper = createComponent();
+ expect(findLockedIcon().props('name')).toBe('lock');
+ });
+
+ it('has the learn more link', () => {
+ wrapper = createComponent();
+ expect(findLearnMoreLink().attributes('href')).toBe(
+ WorkItemCommentLocked.constantOptions.lockedIssueDocsPath,
+ );
+ });
+
+ describe('when the project is archived', () => {
+ beforeEach(() => {
+ wrapper = createComponent({ isProjectArchived: true });
+ });
+
+ it('learn more link is directed to archived project docs path', () => {
+ expect(findLearnMoreLink().attributes('href')).toBe(
+ WorkItemCommentLocked.constantOptions.archivedProjectDocsPath,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_detail_modal_spec.js b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
index 686641800b3..8976cd6e22b 100644
--- a/spec/frontend/work_items/components/work_item_detail_modal_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_modal_spec.js
@@ -4,10 +4,11 @@ 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 WorkItemDetail from '~/work_items/components/work_item_detail.vue';
+import { stubComponent } from 'helpers/stub_component';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import deleteWorkItemFromTaskMutation from '~/work_items/graphql/delete_task_from_work_item.mutation.graphql';
import deleteWorkItemMutation from '~/work_items/graphql/delete_work_item.mutation.graphql';
+import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import {
deleteWorkItemFromTaskMutationErrorResponse,
deleteWorkItemFromTaskMutationResponse,
@@ -69,8 +70,14 @@ describe('WorkItemDetailModal component', () => {
error,
};
},
+ provide: {
+ fullPath: 'group/project',
+ },
stubs: {
GlModal,
+ WorkItemDetail: stubComponent(WorkItemDetail, {
+ apollo: {},
+ }),
},
});
};
@@ -126,6 +133,15 @@ describe('WorkItemDetailModal component', () => {
expect(closeSpy).toHaveBeenCalled();
});
+ it('updates the work item when WorkItemDetail emits `update-modal` event', async () => {
+ createComponent();
+
+ findWorkItemDetail().vm.$emit('update-modal', null, 'updatedId');
+ await waitForPromises();
+
+ expect(findWorkItemDetail().props().workItemId).toEqual('updatedId');
+ });
+
describe('delete work item', () => {
describe('when there is task data', () => {
it('emits workItemDeleted and closes modal', async () => {
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 bbab45c7055..a50a48de921 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -12,6 +12,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import setWindowLocation from 'helpers/set_window_location_helper';
+import { stubComponent } from 'helpers/stub_component';
import WorkItemDetail from '~/work_items/components/work_item_detail.vue';
import WorkItemActions from '~/work_items/components/work_item_actions.vue';
import WorkItemDescription from '~/work_items/components/work_item_description.vue';
@@ -22,6 +23,8 @@ import WorkItemAssignees from '~/work_items/components/work_item_assignees.vue';
import WorkItemLabels from '~/work_items/components/work_item_labels.vue';
import WorkItemMilestone from '~/work_items/components/work_item_milestone.vue';
import WorkItemTree from '~/work_items/components/work_item_links/work_item_tree.vue';
+import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
+import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import { i18n } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
@@ -63,6 +66,7 @@ describe('WorkItemDetail component', () => {
const assigneesSubscriptionHandler = jest
.fn()
.mockResolvedValue(workItemAssigneesSubscriptionResponse);
+ const showModalHandler = jest.fn();
const findAlert = () => wrapper.findComponent(GlAlert);
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
@@ -81,6 +85,8 @@ describe('WorkItemDetail component', () => {
const findCloseButton = () => wrapper.find('[data-testid="work-item-close"]');
const findWorkItemType = () => wrapper.find('[data-testid="work-item-type"]');
const findHierarchyTree = () => wrapper.findComponent(WorkItemTree);
+ const findNotesWidget = () => wrapper.findComponent(WorkItemNotes);
+ const findModal = () => wrapper.findComponent(WorkItemDetailModal);
const createComponent = ({
isModal = false,
@@ -129,6 +135,12 @@ describe('WorkItemDetail component', () => {
stubs: {
WorkItemWeight: true,
WorkItemIteration: true,
+ WorkItemHealthStatus: true,
+ WorkItemDetailModal: stubComponent(WorkItemDetailModal, {
+ methods: {
+ show: showModalHandler,
+ },
+ }),
},
});
};
@@ -652,15 +664,89 @@ describe('WorkItemDetail component', () => {
expect(findHierarchyTree().exists()).toBe(false);
});
- it('renders children tree when work item is an Objective', async () => {
+ describe('work item has children', () => {
const objectiveWorkItem = workItemResponseFactory({
workItemType: objectiveType,
+ confidential: true,
});
const handler = jest.fn().mockResolvedValue(objectiveWorkItem);
- createComponent({ handler });
+
+ it('renders children tree when work item is an Objective', async () => {
+ createComponent({ handler });
+ await waitForPromises();
+
+ expect(findHierarchyTree().exists()).toBe(true);
+ });
+
+ it('renders a modal', async () => {
+ createComponent({ handler });
+ await waitForPromises();
+
+ expect(findModal().exists()).toBe(true);
+ });
+
+ it('opens the modal with the child when `show-modal` is emitted', async () => {
+ createComponent({ handler });
+ await waitForPromises();
+
+ const event = {
+ preventDefault: jest.fn(),
+ };
+
+ findHierarchyTree().vm.$emit('show-modal', event, { id: 'childWorkItemId' });
+ await waitForPromises();
+
+ expect(wrapper.findComponent(WorkItemDetailModal).props().workItemId).toBe(
+ 'childWorkItemId',
+ );
+ expect(showModalHandler).toHaveBeenCalled();
+ });
+
+ describe('work item is rendered in a modal and has children', () => {
+ beforeEach(async () => {
+ createComponent({
+ isModal: true,
+ handler,
+ });
+
+ await waitForPromises();
+ });
+
+ it('does not render a new modal', () => {
+ expect(findModal().exists()).toBe(false);
+ });
+
+ it('emits `update-modal` when `show-modal` is emitted', async () => {
+ const event = {
+ preventDefault: jest.fn(),
+ };
+
+ findHierarchyTree().vm.$emit('show-modal', event, { id: 'childWorkItemId' });
+ await waitForPromises();
+
+ expect(wrapper.emitted('update-modal')).toBeDefined();
+ });
+ });
+ });
+ });
+
+ describe('notes widget', () => {
+ it('does not render notes by default', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findNotesWidget().exists()).toBe(false);
+ });
+
+ it('renders notes when the work_items_mvc flag is on', async () => {
+ const notesWorkItem = workItemResponseFactory({
+ notesWidgetPresent: true,
+ });
+ const handler = jest.fn().mockResolvedValue(notesWorkItem);
+ createComponent({ workItemsMvcEnabled: true, handler });
await waitForPromises();
- expect(findHierarchyTree().exists()).toBe(true);
+ expect(findNotesWidget().exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
index 47489d4796b..e693ccfb156 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_link_child_metadata_spec.js
@@ -5,23 +5,22 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import ItemMilestone from '~/issuable/components/issue_milestone.vue';
import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/work_item_link_child_metadata.vue';
-import { mockMilestone, mockAssignees, mockLabels } from '../../mock_data';
+import { workItemObjectiveMetadataWidgets } from '../../mock_data';
describe('WorkItemLinkChildMetadata', () => {
+ const { MILESTONE, ASSIGNEES, LABELS } = workItemObjectiveMetadataWidgets;
+ const mockMilestone = MILESTONE.milestone;
+ const mockAssignees = ASSIGNEES.assignees.nodes;
+ const mockLabels = LABELS.labels.nodes;
let wrapper;
- const createComponent = ({
- allowsScopedLabels = true,
- milestone = mockMilestone,
- assignees = mockAssignees,
- labels = mockLabels,
- } = {}) => {
+ const createComponent = ({ metadataWidgets = workItemObjectiveMetadataWidgets } = {}) => {
wrapper = shallowMountExtended(WorkItemLinkChildMetadata, {
propsData: {
- allowsScopedLabels,
- milestone,
- assignees,
- labels,
+ metadataWidgets,
+ },
+ slots: {
+ default: `<div data-testid="default-slot">Foo</div>`,
},
});
};
@@ -30,7 +29,11 @@ describe('WorkItemLinkChildMetadata', () => {
createComponent();
});
- it('renders milestone link button', () => {
+ it('renders default slot contents', () => {
+ expect(wrapper.findByTestId('default-slot').text()).toBe('Foo');
+ });
+
+ it('renders item milestone', () => {
const milestoneLink = wrapper.findComponent(ItemMilestone);
expect(milestoneLink.exists()).toBe(true);
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 73d498ad055..0470249d7ce 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
@@ -5,11 +5,12 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import WorkItemLinkChildMetadata from 'ee_else_ce/work_items/components/work_item_links/work_item_link_child_metadata.vue';
+
import { createAlert } from '~/flash';
import RichTimestampTooltip from '~/vue_shared/components/rich_timestamp_tooltip.vue';
import getWorkItemTreeQuery from '~/work_items/graphql/work_item_tree.query.graphql';
-import WorkItemLinkChildMetadata from '~/work_items/components/work_item_links/work_item_link_child_metadata.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import WorkItemLinksMenu from '~/work_items/components/work_item_links/work_item_links_menu.vue';
import WorkItemTreeChildren from '~/work_items/components/work_item_links/work_item_tree_children.vue';
@@ -25,11 +26,9 @@ import {
workItemObjectiveNoMetadata,
confidentialWorkItemTask,
closedWorkItemTask,
- mockMilestone,
- mockAssignees,
- mockLabels,
workItemHierarchyTreeResponse,
workItemHierarchyTreeFailureResponse,
+ workItemObjectiveMetadataWidgets,
} from '../../mock_data';
jest.mock('~/flash');
@@ -148,10 +147,7 @@ describe('WorkItemLinkChild', () => {
const metadataEl = findMetadataComponent();
expect(metadataEl.exists()).toBe(true);
expect(metadataEl.props()).toMatchObject({
- allowsScopedLabels: true,
- milestone: mockMilestone,
- assignees: mockAssignees,
- labels: mockLabels,
+ metadataWidgets: workItemObjectiveMetadataWidgets,
});
});
@@ -265,5 +261,14 @@ describe('WorkItemLinkChild', () => {
message: 'Something went wrong while fetching children.',
});
});
+
+ it('click event on child emits `click` event', async () => {
+ findExpandButton().vm.$emit('click');
+ await waitForPromises();
+
+ findTreeChildren().vm.$emit('click', 'event');
+
+ expect(wrapper.emitted('click')).toEqual([['event']]);
+ });
});
});
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 bbe460a55ba..5e1c46826cc 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
@@ -1,11 +1,18 @@
import Vue from 'vue';
-import { GlForm, GlFormInput, GlTokenSelector } from '@gitlab/ui';
+import { GlForm, GlFormInput, GlFormCheckbox, GlTooltip, GlTokenSelector } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
+import { sprintf, s__ } from '~/locale';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WorkItemLinksForm from '~/work_items/components/work_item_links/work_item_links_form.vue';
-import { FORM_TYPES } from '~/work_items/constants';
+import {
+ FORM_TYPES,
+ WORK_ITEM_TYPE_ENUM_TASK,
+ WORK_ITEM_TYPE_VALUE_ISSUE,
+ I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL,
+ I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP,
+} from '~/work_items/constants';
import projectWorkItemsQuery from '~/work_items/graphql/project_work_items.query.graphql';
import projectWorkItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
@@ -36,6 +43,8 @@ describe('WorkItemLinksForm', () => {
workItemsMvcEnabled = false,
parentIteration = null,
formType = FORM_TYPES.create,
+ parentWorkItemType = WORK_ITEM_TYPE_VALUE_ISSUE,
+ childrenType = WORK_ITEM_TYPE_ENUM_TASK,
} = {}) => {
wrapper = shallowMountExtended(WorkItemLinksForm, {
apolloProvider: createMockApollo([
@@ -48,6 +57,8 @@ describe('WorkItemLinksForm', () => {
issuableGid: 'gid://gitlab/WorkItem/1',
parentConfidential,
parentIteration,
+ parentWorkItemType,
+ childrenType,
formType,
},
provide: {
@@ -65,6 +76,7 @@ describe('WorkItemLinksForm', () => {
const findForm = () => wrapper.findComponent(GlForm);
const findTokenSelector = () => wrapper.findComponent(GlTokenSelector);
const findInput = () => wrapper.findComponent(GlFormInput);
+ const findConfidentialCheckbox = () => wrapper.findComponent(GlFormCheckbox);
const findAddChildButton = () => wrapper.findByTestId('add-child-button');
afterEach(() => {
@@ -90,6 +102,7 @@ describe('WorkItemLinksForm', () => {
preventDefault: jest.fn(),
});
await waitForPromises();
+ expect(wrapper.vm.childWorkItemType).toEqual('gid://gitlab/WorkItems::Type/3');
expect(createMutationResolver).toHaveBeenCalledWith({
input: {
title: 'Create task test',
@@ -112,6 +125,7 @@ describe('WorkItemLinksForm', () => {
preventDefault: jest.fn(),
});
await waitForPromises();
+ expect(wrapper.vm.childWorkItemType).toEqual('gid://gitlab/WorkItems::Type/3');
expect(createMutationResolver).toHaveBeenCalledWith({
input: {
title: 'Create confidential task',
@@ -124,9 +138,50 @@ describe('WorkItemLinksForm', () => {
},
});
});
+
+ describe('confidentiality checkbox', () => {
+ it('renders confidentiality checkbox', () => {
+ const confidentialCheckbox = findConfidentialCheckbox();
+
+ expect(confidentialCheckbox.exists()).toBe(true);
+ expect(wrapper.findComponent(GlTooltip).exists()).toBe(false);
+ expect(confidentialCheckbox.text()).toBe(
+ sprintf(I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_LABEL, {
+ workItemType: WORK_ITEM_TYPE_ENUM_TASK.toLocaleLowerCase(),
+ }),
+ );
+ });
+
+ it('renders confidentiality tooltip with checkbox checked and disabled when parent is confidential', () => {
+ createComponent({ parentConfidential: true });
+
+ const confidentialCheckbox = findConfidentialCheckbox();
+ const confidentialTooltip = wrapper.findComponent(GlTooltip);
+
+ expect(confidentialCheckbox.attributes('disabled')).toBe('true');
+ expect(confidentialCheckbox.attributes('checked')).toBe('true');
+ expect(confidentialTooltip.exists()).toBe(true);
+ expect(confidentialTooltip.text()).toBe(
+ sprintf(I18N_WORK_ITEM_CONFIDENTIALITY_CHECKBOX_TOOLTIP, {
+ workItemType: WORK_ITEM_TYPE_ENUM_TASK.toLocaleLowerCase(),
+ parentWorkItemType: WORK_ITEM_TYPE_VALUE_ISSUE.toLocaleLowerCase(),
+ }),
+ );
+ });
+ });
});
describe('adding an existing work item', () => {
+ const selectAvailableWorkItemTokens = async () => {
+ findTokenSelector().vm.$emit(
+ 'input',
+ availableWorkItemsResponse.data.workspace.workItems.nodes,
+ );
+ findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
+
+ await waitForPromises();
+ };
+
beforeEach(async () => {
await createComponent({ formType: FORM_TYPES.add });
});
@@ -136,6 +191,7 @@ describe('WorkItemLinksForm', () => {
expect(findTokenSelector().exists()).toBe(true);
expect(findAddChildButton().text()).toBe('Add task');
expect(findInput().exists()).toBe(false);
+ expect(findConfidentialCheckbox().exists()).toBe(false);
});
it('searches for available work items as prop when typing in input', async () => {
@@ -147,13 +203,7 @@ describe('WorkItemLinksForm', () => {
});
it('selects and adds children', async () => {
- findTokenSelector().vm.$emit(
- 'input',
- availableWorkItemsResponse.data.workspace.workItems.nodes,
- );
- findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
-
- await waitForPromises();
+ await selectAvailableWorkItemTokens();
expect(findAddChildButton().text()).toBe('Add tasks');
findForm().vm.$emit('submit', {
@@ -162,6 +212,31 @@ describe('WorkItemLinksForm', () => {
await waitForPromises();
expect(updateMutationResolver).toHaveBeenCalled();
});
+
+ it('shows validation error when non-confidential child items are being added to confidential parent', async () => {
+ await createComponent({ formType: FORM_TYPES.add, parentConfidential: true });
+
+ await selectAvailableWorkItemTokens();
+
+ const validationEl = wrapper.findByTestId('work-items-invalid');
+ expect(validationEl.exists()).toBe(true);
+ expect(validationEl.text().trim()).toBe(
+ sprintf(
+ s__(
+ 'WorkItem|%{invalidWorkItemsList} cannot be added: Cannot assign a non-confidential %{childWorkItemType} to a confidential parent %{parentWorkItemType}. Make the selected %{childWorkItemType} confidential and try again.',
+ ),
+ {
+ // Only non-confidential work items are shown in the error message
+ invalidWorkItemsList: availableWorkItemsResponse.data.workspace.workItems.nodes
+ .filter((wi) => !wi.confidential)
+ .map((wi) => wi.title)
+ .join(', '),
+ childWorkItemType: 'Task',
+ parentWorkItemType: 'Issue',
+ },
+ ),
+ );
+ });
});
describe('associate iteration with task', () => {
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 96211e12755..156f06a0d5e 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
@@ -34,6 +34,8 @@ describe('WorkItemTree', () => {
const createComponent = ({
workItemType = 'Objective',
+ parentWorkItemType = 'Objective',
+ confidential = false,
children = childrenWorkItems,
apolloProvider = null,
} = {}) => {
@@ -55,7 +57,9 @@ describe('WorkItemTree', () => {
apolloProvider || createMockApollo([[workItemQuery, getWorkItemQueryHandler]]),
propsData: {
workItemType,
+ parentWorkItemType,
workItemId: 'gid://gitlab/WorkItem/515',
+ confidential,
children,
projectPath: 'test/project',
},
@@ -90,7 +94,11 @@ describe('WorkItemTree', () => {
});
it('renders all hierarchy widget children', () => {
- expect(findWorkItemLinkChildItems()).toHaveLength(4);
+ const workItemLinkChildren = findWorkItemLinkChildItems();
+ expect(workItemLinkChildren).toHaveLength(4);
+ expect(workItemLinkChildren.at(0).props().childItem.confidential).toBe(
+ childrenWorkItems[0].confidential,
+ );
});
it('does not display form by default', () => {
@@ -110,8 +118,12 @@ describe('WorkItemTree', () => {
await nextTick();
expect(findForm().exists()).toBe(true);
- expect(findForm().props('formType')).toBe(formType);
- expect(findForm().props('childrenType')).toBe(childType);
+ expect(findForm().props()).toMatchObject({
+ formType,
+ childrenType: childType,
+ parentWorkItemType: 'Objective',
+ parentConfidential: false,
+ });
},
);
@@ -122,6 +134,17 @@ describe('WorkItemTree', () => {
expect(wrapper.emitted('removeChild')).toEqual([['gid://gitlab/WorkItem/2']]);
});
+ it('emits `show-modal` on `click` event', () => {
+ const firstChild = findWorkItemLinkChildItems().at(0);
+ const event = {
+ childItem: 'gid://gitlab/WorkItem/2',
+ };
+
+ firstChild.vm.$emit('click', event);
+
+ expect(wrapper.emitted('show-modal')).toEqual([[event, event.childItem]]);
+ });
+
it.each`
description | workItemType | prefetch
${'prefetches'} | ${'Issue'} | ${true}
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 ed68d214fc9..23dd2b6bacb 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -1,18 +1,22 @@
import { GlSkeletonLoader } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import Vue 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 SystemNote from '~/work_items/components/notes/system_note.vue';
import WorkItemNotes from '~/work_items/components/work_item_notes.vue';
+import WorkItemCommentForm from '~/work_items/components/work_item_comment_form.vue';
+import ActivityFilter from '~/work_items/components/notes/activity_filter.vue';
import workItemNotesQuery from '~/work_items/graphql/work_item_notes.query.graphql';
import workItemNotesByIidQuery from '~/work_items/graphql/work_item_notes_by_iid.query.graphql';
-import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
+import { DEFAULT_PAGE_SIZE_NOTES, WIDGET_TYPE_NOTES } from '~/work_items/constants';
+import { DESC } from '~/notes/constants';
import {
mockWorkItemNotesResponse,
workItemQueryResponse,
mockWorkItemNotesByIidResponse,
+ mockMoreWorkItemNotesResponse,
} from '../mock_data';
const mockWorkItemId = workItemQueryResponse.data.workItem.id;
@@ -24,6 +28,12 @@ const mockNotesByIidWidgetResponse = mockWorkItemNotesByIidResponse.data.workspa
(widget) => widget.type === WIDGET_TYPE_NOTES,
);
+const mockMoreNotesWidgetResponse = mockMoreWorkItemNotesResponse.data.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_NOTES,
+);
+
+const firstSystemNodeId = mockNotesWidgetResponse.discussions.nodes[0].notes.nodes[0].id;
+
describe('WorkItemNotes component', () => {
let wrapper;
@@ -31,16 +41,24 @@ describe('WorkItemNotes component', () => {
const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote);
const findActivityLabel = () => wrapper.find('label');
+ const findWorkItemCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+ const findSortingFilter = () => wrapper.findComponent(ActivityFilter);
+ const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index);
const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesResponse);
const workItemNotesByIidQueryHandler = jest
.fn()
.mockResolvedValue(mockWorkItemNotesByIidResponse);
+ const workItemMoreNotesQueryHandler = jest.fn().mockResolvedValue(mockMoreWorkItemNotesResponse);
- const createComponent = ({ workItemId = mockWorkItemId, fetchByIid = false } = {}) => {
+ const createComponent = ({
+ workItemId = mockWorkItemId,
+ fetchByIid = false,
+ defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler,
+ } = {}) => {
wrapper = shallowMount(WorkItemNotes, {
apolloProvider: createMockApollo([
- [workItemNotesQuery, workItemNotesQueryHandler],
+ [workItemNotesQuery, defaultWorkItemNotesQueryHandler],
[workItemNotesByIidQuery, workItemNotesByIidQueryHandler],
]),
propsData: {
@@ -50,6 +68,7 @@ describe('WorkItemNotes component', () => {
},
fullPath: 'test-path',
fetchByIid,
+ workItemType: 'task',
},
provide: {
glFeatures: {
@@ -63,14 +82,17 @@ describe('WorkItemNotes component', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders activity label', () => {
expect(findActivityLabel().exists()).toBe(true);
});
+ it('passes correct props to comment form component', async () => {
+ createComponent({ workItemId: mockWorkItemId, fetchByIid: false });
+ await waitForPromises();
+
+ expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(false);
+ });
+
describe('when notes are loading', () => {
it('renders skeleton loader', () => {
expect(findSkeletonLoader().exists()).toBe(true);
@@ -98,10 +120,65 @@ describe('WorkItemNotes component', () => {
await waitForPromises();
});
- it('shows the notes list', () => {
+ it('renders the notes list to the length of the response', () => {
expect(findAllSystemNotes()).toHaveLength(
mockNotesByIidWidgetResponse.discussions.nodes.length,
);
});
+
+ it('passes correct props to comment form component', () => {
+ expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(true);
+ });
+ });
+
+ describe('Pagination', () => {
+ describe('When there is no next page', () => {
+ it('fetch more notes is not called', async () => {
+ createComponent();
+ await nextTick();
+ expect(workItemMoreNotesQueryHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('when there is next page', () => {
+ beforeEach(async () => {
+ createComponent({ defaultWorkItemNotesQueryHandler: workItemMoreNotesQueryHandler });
+ await waitForPromises();
+ });
+
+ it('fetch more notes should be called', async () => {
+ expect(workItemMoreNotesQueryHandler).toHaveBeenCalledWith({
+ pageSize: DEFAULT_PAGE_SIZE_NOTES,
+ id: 'gid://gitlab/WorkItem/1',
+ });
+
+ await nextTick();
+
+ expect(workItemMoreNotesQueryHandler).toHaveBeenCalledWith({
+ pageSize: 45,
+ id: 'gid://gitlab/WorkItem/1',
+ after: mockMoreNotesWidgetResponse.discussions.pageInfo.endCursor,
+ });
+ });
+ });
+ });
+
+ describe('Sorting', () => {
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('filter exists', () => {
+ expect(findSortingFilter().exists()).toBe(true);
+ });
+
+ it('sorts the list when the `changeSortOrder` event is emitted', async () => {
+ expect(findSystemNoteAtIndex(0).props('note').id).toEqual(firstSystemNodeId);
+
+ await findSortingFilter().vm.$emit('changeSortOrder', DESC);
+
+ expect(findSystemNoteAtIndex(0).props('note').id).not.toEqual(firstSystemNodeId);
+ });
});
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 850672b68d0..67b477b6eb0 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -62,6 +62,7 @@ export const workItemQueryResponse = {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
+ archived: false,
},
workItemType: {
__typename: 'WorkItemType',
@@ -156,6 +157,7 @@ export const updateWorkItemMutationResponse = {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
+ archived: false,
},
workItemType: {
__typename: 'WorkItemType',
@@ -268,6 +270,7 @@ export const workItemResponseFactory = ({
milestoneWidgetPresent = true,
iterationWidgetPresent = true,
healthStatusWidgetPresent = true,
+ notesWidgetPresent = true,
confidential = false,
canInviteMembers = false,
allowsScopedLabels = false,
@@ -292,6 +295,7 @@ export const workItemResponseFactory = ({
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
+ archived: false,
},
workItemType,
userPermissions: {
@@ -380,6 +384,23 @@ export const workItemResponseFactory = ({
healthStatus: 'onTrack',
}
: { type: 'MOCK TYPE' },
+ notesWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetNotes',
+ type: 'NOTES',
+ discussions: {
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor:
+ 'eyJjcmVhdGVkX2F0IjoiMjAyMi0xMS0xNCAwNDoxOTowMC4wOTkxMTcwMDAgKzAwMDAiLCJpZCI6IjQyNyIsIl9rZCI6Im4ifQ==',
+ __typename: 'PageInfo',
+ },
+ nodes: [],
+ },
+ }
+ : { type: 'MOCK TYPE' },
{
__typename: 'WorkItemWidgetHierarchy',
type: 'HIERARCHY',
@@ -409,6 +430,12 @@ export const workItemResponseFactory = ({
},
parent,
},
+ notesWidgetPresent
+ ? {
+ __typename: 'WorkItemWidgetNotes',
+ type: 'NOTES',
+ }
+ : { type: 'MOCK TYPE' },
],
},
},
@@ -448,6 +475,7 @@ export const createWorkItemMutationResponse = {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
+ archived: false,
},
workItemType: {
__typename: 'WorkItemType',
@@ -485,6 +513,7 @@ export const createWorkItemFromTaskMutationResponse = {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
+ archived: false,
},
workItemType: {
__typename: 'WorkItemType',
@@ -524,6 +553,7 @@ export const createWorkItemFromTaskMutationResponse = {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
+ archived: false,
},
workItemType: {
__typename: 'WorkItemType',
@@ -698,6 +728,20 @@ export const workItemIterationSubscriptionResponse = {
},
};
+export const workItemHealthStatusSubscriptionResponse = {
+ data: {
+ issuableHealthStatusUpdated: {
+ id: 'gid://gitlab/WorkItem/1',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetHealthStatus',
+ healthStatus: 'needsAttention',
+ },
+ ],
+ },
+ },
+};
+
export const workItemMilestoneSubscriptionResponse = {
data: {
issuableMilestoneUpdated: {
@@ -734,6 +778,7 @@ export const workItemHierarchyEmptyResponse = {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
+ archived: false,
},
userPermissions: {
deleteWorkItem: false,
@@ -780,6 +825,7 @@ export const workItemHierarchyNoUpdatePermissionResponse = {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
+ archived: false,
},
confidential: false,
widgets: [
@@ -920,6 +966,7 @@ export const workItemHierarchyResponse = {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
+ archived: false,
},
widgets: [
{
@@ -942,6 +989,43 @@ export const workItemHierarchyResponse = {
},
};
+export const workItemObjectiveMetadataWidgets = {
+ ASSIGNEES: {
+ type: 'ASSIGNEES',
+ __typename: 'WorkItemWidgetAssignees',
+ canInviteMembers: true,
+ allowsMultipleAssignees: true,
+ assignees: {
+ __typename: 'UserCoreConnection',
+ nodes: mockAssignees,
+ },
+ },
+ HEALTH_STATUS: {
+ type: 'HEALTH_STATUS',
+ __typename: 'WorkItemWidgetHealthStatus',
+ healthStatus: 'onTrack',
+ },
+ LABELS: {
+ type: 'LABELS',
+ __typename: 'WorkItemWidgetLabels',
+ allowsScopedLabels: true,
+ labels: {
+ __typename: 'LabelConnection',
+ nodes: mockLabels,
+ },
+ },
+ MILESTONE: {
+ type: 'MILESTONE',
+ __typename: 'WorkItemWidgetMilestone',
+ milestone: mockMilestone,
+ },
+ PROGRESS: {
+ type: 'PROGRESS',
+ __typename: 'WorkItemWidgetProgress',
+ progress: 10,
+ },
+};
+
export const workItemObjectiveWithChild = {
id: 'gid://gitlab/WorkItem/12',
iid: '12',
@@ -955,6 +1039,7 @@ export const workItemObjectiveWithChild = {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
+ archived: false,
},
userPermissions: {
deleteWorkItem: true,
@@ -976,30 +1061,11 @@ export const workItemObjectiveWithChild = {
},
__typename: 'WorkItemWidgetHierarchy',
},
- {
- type: 'MILESTONE',
- __typename: 'WorkItemWidgetMilestone',
- milestone: mockMilestone,
- },
- {
- type: 'ASSIGNEES',
- __typename: 'WorkItemWidgetAssignees',
- canInviteMembers: true,
- allowsMultipleAssignees: true,
- assignees: {
- __typename: 'UserCoreConnection',
- nodes: mockAssignees,
- },
- },
- {
- type: 'LABELS',
- __typename: 'WorkItemWidgetLabels',
- allowsScopedLabels: true,
- labels: {
- __typename: 'LabelConnection',
- nodes: mockLabels,
- },
- },
+ workItemObjectiveMetadataWidgets.PROGRESS,
+ workItemObjectiveMetadataWidgets.HEALTH_STATUS,
+ workItemObjectiveMetadataWidgets.MILESTONE,
+ workItemObjectiveMetadataWidgets.ASSIGNEES,
+ workItemObjectiveMetadataWidgets.LABELS,
],
__typename: 'WorkItem',
};
@@ -1012,6 +1078,16 @@ export const workItemObjectiveNoMetadata = {
hasChildren: true,
__typename: 'WorkItemWidgetHierarchy',
},
+ {
+ __typename: 'WorkItemWidgetProgress',
+ type: 'PROGRESS',
+ progress: null,
+ },
+ {
+ __typename: 'WorkItemWidgetMilestone',
+ type: 'MILESTONE',
+ milestone: null,
+ },
],
};
@@ -1036,6 +1112,7 @@ export const workItemHierarchyTreeResponse = {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
+ archived: false,
},
widgets: [
{
@@ -1118,6 +1195,7 @@ export const changeWorkItemParentMutationResponse = {
__typename: 'Project',
id: '1',
fullPath: 'test-project-path',
+ archived: false,
},
widgets: [
{
@@ -1149,6 +1227,7 @@ export const availableWorkItemsResponse = {
title: 'Task 1',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
+ confidential: false,
__typename: 'WorkItem',
},
{
@@ -1156,6 +1235,15 @@ export const availableWorkItemsResponse = {
title: 'Task 2',
state: 'OPEN',
createdAt: '2022-08-03T12:41:54Z',
+ confidential: false,
+ __typename: 'WorkItem',
+ },
+ {
+ id: 'gid://gitlab/WorkItem/460',
+ title: 'Task 3',
+ state: 'OPEN',
+ createdAt: '2022-08-03T12:41:54Z',
+ confidential: true,
__typename: 'WorkItem',
},
],
@@ -1514,11 +1602,16 @@ export const mockWorkItemNotesResponse = {
nodes: [
{
id: 'gid://gitlab/Note/2428',
- body: 'added #31 as parent issue',
bodyHtml:
'<p data-sourcepos="1:1-1:25" dir="auto">added <a href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container=body data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue">#31</a> as parent issue</p>',
systemNoteIconName: 'link',
createdAt: '2022-11-14T04:18:59Z',
+ system: true,
+ internal: false,
+ userPermissions: {
+ adminNote: false,
+ __typename: 'NotePermissions',
+ },
author: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
@@ -1541,12 +1634,17 @@ export const mockWorkItemNotesResponse = {
notes: {
nodes: [
{
- id: 'gid://gitlab/MilestoneNote/not-persisted',
- body: 'changed milestone to %5',
+ id: 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
bodyHtml:
'<p data-sourcepos="1:1-1:23" dir="auto">changed milestone to <a href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container=body data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a></p>',
systemNoteIconName: 'clock',
createdAt: '2022-11-14T04:18:59Z',
+ system: true,
+ internal: false,
+ userPermissions: {
+ adminNote: false,
+ __typename: 'NotePermissions',
+ },
author: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
@@ -1569,11 +1667,16 @@ export const mockWorkItemNotesResponse = {
notes: {
nodes: [
{
- id: 'gid://gitlab/WeightNote/not-persisted',
- body: 'changed weight to 89',
+ id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
+ system: true,
+ internal: false,
+ userPermissions: {
+ adminNote: false,
+ __typename: 'NotePermissions',
+ },
author: {
avatarUrl:
'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
@@ -1656,11 +1759,16 @@ export const mockWorkItemNotesByIidResponse = {
nodes: [
{
id: 'gid://gitlab/Note/2428',
- body: 'added #31 as parent issue',
bodyHtml:
'\u003cp data-sourcepos="1:1-1:25" dir="auto"\u003eadded \u003ca href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container="body" data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue"\u003e#31\u003c/a\u003e as parent issue\u003c/p\u003e',
systemNoteIconName: 'link',
createdAt: '2022-11-14T04:18:59Z',
+ system: true,
+ internal: false,
+ userPermissions: {
+ adminNote: false,
+ __typename: 'NotePermissions',
+ },
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@@ -1685,11 +1793,16 @@ export const mockWorkItemNotesByIidResponse = {
{
id:
'gid://gitlab/MilestoneNote/7b08b89a728a5ceb7de8334246837ba1d07270dc',
- body: 'changed milestone to %5',
bodyHtml:
'\u003cp data-sourcepos="1:1-1:23" dir="auto"\u003echanged milestone to \u003ca href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container="body" data-placement="top" title="" class="gfm gfm-milestone has-tooltip"\u003e%v4.0\u003c/a\u003e\u003c/p\u003e',
systemNoteIconName: 'clock',
createdAt: '2022-11-14T04:18:59Z',
+ system: true,
+ internal: false,
+ userPermissions: {
+ adminNote: false,
+ __typename: 'NotePermissions',
+ },
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@@ -1714,11 +1827,16 @@ export const mockWorkItemNotesByIidResponse = {
{
id:
'gid://gitlab/IterationNote/addbc177f7664699a135130ab05ffb78c57e4db3',
- body: 'changed iteration to *iteration:5352',
bodyHtml:
'\u003cp data-sourcepos="1:1-1:36" dir="auto"\u003echanged iteration to \u003ca href="/groups/flightjs/-/iterations/5352" data-reference-type="iteration" data-original="*iteration:5352" data-link="false" data-link-reference="false" data-project="6" data-iteration="5352" data-container="body" data-placement="top" title="Iteration" class="gfm gfm-iteration has-tooltip"\u003eEt autem debitis nam suscipit eos ut. Jul 13, 2022 - Jul 19, 2022\u003c/a\u003e\u003c/p\u003e',
systemNoteIconName: 'iteration',
createdAt: '2022-11-14T04:19:00Z',
+ system: true,
+ internal: false,
+ userPermissions: {
+ adminNote: false,
+ __typename: 'NotePermissions',
+ },
author: {
id: 'gid://gitlab/User/1',
avatarUrl:
@@ -1750,3 +1868,183 @@ export const mockWorkItemNotesByIidResponse = {
},
},
};
+export const mockMoreWorkItemNotesResponse = {
+ data: {
+ workItem: {
+ id: 'gid://gitlab/WorkItem/600',
+ iid: '60',
+ widgets: [
+ {
+ __typename: 'WorkItemWidgetIteration',
+ },
+ {
+ __typename: 'WorkItemWidgetWeight',
+ },
+ {
+ __typename: 'WorkItemWidgetAssignees',
+ },
+ {
+ __typename: 'WorkItemWidgetLabels',
+ },
+ {
+ __typename: 'WorkItemWidgetDescription',
+ },
+ {
+ __typename: 'WorkItemWidgetHierarchy',
+ },
+ {
+ __typename: 'WorkItemWidgetStartAndDueDate',
+ },
+ {
+ __typename: 'WorkItemWidgetMilestone',
+ },
+ {
+ type: 'NOTES',
+ discussions: {
+ pageInfo: {
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: 'endCursor',
+ __typename: 'PageInfo',
+ },
+ nodes: [
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/2428',
+ bodyHtml:
+ '<p data-sourcepos="1:1-1:25" dir="auto">added <a href="/flightjs/Flight/-/issues/31" data-reference-type="issue" data-original="#31" data-link="false" data-link-reference="false" data-project="6" data-issue="224" data-project-path="flightjs/Flight" data-iid="31" data-issue-type="issue" data-container=body data-placement="top" title="Perferendis est quae totam quia laborum tempore ut voluptatem." class="gfm gfm-issue">#31</a> as parent issue</p>',
+ systemNoteIconName: 'link',
+ createdAt: '2022-11-14T04:18:59Z',
+ system: true,
+ internal: false,
+ userPermissions: {
+ adminNote: false,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83823',
+ bodyHtml:
+ '<p data-sourcepos="1:1-1:23" dir="auto">changed milestone to <a href="/flightjs/Flight/-/milestones/5" data-reference-type="milestone" data-original="%5" data-link="false" data-link-reference="false" data-project="6" data-milestone="30" data-container=body data-placement="top" title="" class="gfm gfm-milestone has-tooltip">%v4.0</a></p>',
+ systemNoteIconName: 'clock',
+ createdAt: '2022-11-14T04:18:59Z',
+ system: true,
+ internal: false,
+ userPermissions: {
+ adminNote: false,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id:
+ 'gid://gitlab/IndividualNoteDiscussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
+ systemNoteIconName: 'weight',
+ createdAt: '2022-11-25T07:16:20Z',
+ system: true,
+ internal: false,
+ userPermissions: {
+ adminNote: false,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ ],
+ __typename: 'DiscussionConnection',
+ },
+ __typename: 'WorkItemWidgetNotes',
+ },
+ ],
+ __typename: 'WorkItem',
+ },
+ },
+};
+
+export const createWorkItemNoteResponse = {
+ data: {
+ createNote: {
+ errors: [],
+ __typename: 'CreateNotePayload',
+ },
+ },
+};
+
+export const mockWorkItemCommentNote = {
+ id: 'gid://gitlab/Note/158',
+ bodyHtml:
+ '<p data-sourcepos="1:1-1:76" dir="auto"><gl-emoji title="waving hand sign" data-name="wave" data-unicode-version="6.0">👋</gl-emoji> Hi <a href="/fredda.brekke" data-reference-type="user" data-user="3" data-container="body" data-placement="top" class="gfm gfm-project_member js-user-link" title="Sherie Nitzsche">@fredda.brekke</a> How are you ? what do you think about this ? <gl-emoji title="person with folded hands" data-name="pray" data-unicode-version="6.0">🙏</gl-emoji></p>',
+ systemNoteIconName: false,
+ createdAt: '2022-11-25T07:16:20Z',
+ system: false,
+ internal: false,
+ userPermissions: {
+ adminNote: false,
+ __typename: 'NotePermissions',
+ },
+ author: {
+ avatarUrl: 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ id: 'gid://gitlab/User/1',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+};
diff --git a/spec/frontend/work_items/router_spec.js b/spec/frontend/work_items/router_spec.js
index b503d819435..ef9ae4a2eab 100644
--- a/spec/frontend/work_items/router_spec.js
+++ b/spec/frontend/work_items/router_spec.js
@@ -74,6 +74,7 @@ describe('Work items router', () => {
stubs: {
WorkItemWeight: true,
WorkItemIteration: true,
+ WorkItemHealthStatus: true,
},
});
};