summaryrefslogtreecommitdiff
path: root/spec/frontend
diff options
context:
space:
mode:
Diffstat (limited to 'spec/frontend')
-rw-r--r--spec/frontend/__helpers__/init_vue_mr_page_helper.js3
-rw-r--r--spec/frontend/__helpers__/test_apollo_link.js4
-rw-r--r--spec/frontend/__helpers__/vuex_action_helper_spec.js7
-rw-r--r--spec/frontend/abuse_reports/components/abuse_category_selector_spec.js8
-rw-r--r--spec/frontend/abuse_reports/components/links_to_spam_input_spec.js65
-rw-r--r--spec/frontend/add_context_commits_modal/store/actions_spec.js7
-rw-r--r--spec/frontend/admin/broadcast_messages/components/base_spec.js7
-rw-r--r--spec/frontend/admin/statistics_panel/components/app_spec.js3
-rw-r--r--spec/frontend/admin/statistics_panel/store/actions_spec.js11
-rw-r--r--spec/frontend/admin/statistics_panel/store/mutations_spec.js6
-rw-r--r--spec/frontend/admin/topics/components/topic_select_spec.js122
-rw-r--r--spec/frontend/airflow/dags/components/dags_spec.js115
-rw-r--r--spec/frontend/airflow/dags/components/mock_data.js67
-rw-r--r--spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap1
-rw-r--r--spec/frontend/analytics/shared/components/metric_popover_spec.js66
-rw-r--r--spec/frontend/api/alert_management_alerts_api_spec.js13
-rw-r--r--spec/frontend/api/groups_api_spec.js32
-rw-r--r--spec/frontend/api/projects_api_spec.js83
-rw-r--r--spec/frontend/api/user_api_spec.js9
-rw-r--r--spec/frontend/api_spec.js22
-rw-r--r--spec/frontend/artifacts/components/app_spec.js109
-rw-r--r--spec/frontend/authentication/u2f/register_spec.js3
-rw-r--r--spec/frontend/authentication/webauthn/register_spec.js3
-rw-r--r--spec/frontend/badges/store/actions_spec.js21
-rw-r--r--spec/frontend/batch_comments/components/draft_note_spec.js58
-rw-r--r--spec/frontend/batch_comments/components/preview_dropdown_spec.js29
-rw-r--r--spec/frontend/batch_comments/components/publish_button_spec.js34
-rw-r--r--spec/frontend/batch_comments/components/publish_dropdown_spec.js17
-rw-r--r--spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js23
-rw-r--r--spec/frontend/blob/components/table_contents_spec.js48
-rw-r--r--spec/frontend/blob/notebook/notebook_viever_spec.js7
-rw-r--r--spec/frontend/blob/openapi/index_spec.js6
-rw-r--r--spec/frontend/boards/board_card_inner_spec.js15
-rw-r--r--spec/frontend/boards/board_list_helper.js6
-rw-r--r--spec/frontend/boards/board_list_spec.js33
-rw-r--r--spec/frontend/boards/components/board_app_spec.js2
-rw-r--r--spec/frontend/boards/components/board_card_move_to_position_spec.js52
-rw-r--r--spec/frontend/boards/components/board_card_spec.js1
-rw-r--r--spec/frontend/boards/components/board_column_spec.js4
-rw-r--r--spec/frontend/boards/components/board_content_sidebar_spec.js15
-rw-r--r--spec/frontend/boards/components/board_list_header_spec.js34
-rw-r--r--spec/frontend/boards/components/board_top_bar_spec.js52
-rw-r--r--spec/frontend/boards/components/boards_selector_spec.js33
-rw-r--r--spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js11
-rw-r--r--spec/frontend/boards/mock_data.js23
-rw-r--r--spec/frontend/boards/stores/actions_spec.js18
-rw-r--r--spec/frontend/boards/stores/mutations_spec.js4
-rw-r--r--spec/frontend/branches/components/sort_dropdown_spec.js8
-rw-r--r--spec/frontend/branches/divergence_graph_spec.js3
-rw-r--r--spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js4
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js5
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js5
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js37
-rw-r--r--spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js13
-rw-r--r--spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js109
-rw-r--r--spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js45
-rw-r--r--spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js1
-rw-r--r--spec/frontend/ci/pipeline_editor/mock_data.js62
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js3
-rw-r--r--spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js107
-rw-r--r--spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js98
-rw-r--r--spec/frontend/ci/pipeline_new/mock_data.js1
-rw-r--r--spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js28
-rw-r--r--spec/frontend/ci/reports/codequality_report/store/actions_spec.js19
-rw-r--r--spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js80
-rw-r--r--spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js108
-rw-r--r--spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js18
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js15
-rw-r--r--spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js10
-rw-r--r--spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js9
-rw-r--r--spec/frontend/ci/runner/components/runner_bulk_delete_spec.js32
-rw-r--r--spec/frontend/ci/runner/components/runner_details_tabs_spec.js127
-rw-r--r--spec/frontend/ci/runner/components/runner_form_fields_spec.js87
-rw-r--r--spec/frontend/ci/runner/components/runner_header_spec.js6
-rw-r--r--spec/frontend/ci/runner/components/runner_job_status_badge_spec.js19
-rw-r--r--spec/frontend/ci/runner/components/runner_jobs_table_spec.js8
-rw-r--r--spec/frontend/ci/runner/components/runner_list_empty_state_spec.js71
-rw-r--r--spec/frontend/ci/runner/components/runner_list_spec.js20
-rw-r--r--spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js96
-rw-r--r--spec/frontend/ci/runner/components/runner_platforms_radio_spec.js154
-rw-r--r--spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js10
-rw-r--r--spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js40
-rw-r--r--spec/frontend/ci/runner/group_runners/group_runners_app_spec.js14
-rw-r--r--spec/frontend/ci/runner/mock_data.js1
-rw-r--r--spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap386
-rw-r--r--spec/frontend/ci_secure_files/components/metadata/button_spec.js49
-rw-r--r--spec/frontend/ci_secure_files/components/metadata/modal_spec.js78
-rw-r--r--spec/frontend/ci_secure_files/components/secure_files_list_spec.js17
-rw-r--r--spec/frontend/ci_secure_files/mock_data.js61
-rw-r--r--spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js94
-rw-r--r--spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js2
-rw-r--r--spec/frontend/clusters/clusters_bundle_spec.js9
-rw-r--r--spec/frontend/clusters_list/components/clusters_spec.js7
-rw-r--r--spec/frontend/clusters_list/store/actions_spec.js9
-rw-r--r--spec/frontend/code_navigation/store/actions_spec.js5
-rw-r--r--spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js19
-rw-r--r--spec/frontend/commit/mock_data.js153
-rw-r--r--spec/frontend/commit/pipelines/pipelines_table_spec.js17
-rw-r--r--spec/frontend/commits_spec.js3
-rw-r--r--spec/frontend/confidential_merge_request/components/dropdown_spec.js100
-rw-r--r--spec/frontend/confidential_merge_request/components/project_form_group_spec.js3
-rw-r--r--spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js15
-rw-r--r--spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap46
-rw-r--r--spec/frontend/contributors/component/contributors_spec.js66
-rw-r--r--spec/frontend/contributors/store/actions_spec.js5
-rw-r--r--spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js9
-rw-r--r--spec/frontend/deploy_keys/components/app_spec.js5
-rw-r--r--spec/frontend/deploy_tokens/components/new_deploy_token_spec.js7
-rw-r--r--spec/frontend/design_management/components/__snapshots__/image_spec.js.snap14
-rw-r--r--spec/frontend/design_management/components/design_overlay_spec.js305
-rw-r--r--spec/frontend/design_management/components/image_spec.js10
-rw-r--r--spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap12
-rw-r--r--spec/frontend/design_management/components/list/item_spec.js6
-rw-r--r--spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap243
-rw-r--r--spec/frontend/design_management/components/upload/design_version_dropdown_spec.js30
-rw-r--r--spec/frontend/diffs/components/app_spec.js3
-rw-r--r--spec/frontend/diffs/components/commit_item_spec.js6
-rw-r--r--spec/frontend/diffs/components/diff_row_utils_spec.js525
-rw-r--r--spec/frontend/diffs/components/tree_list_spec.js2
-rw-r--r--spec/frontend/diffs/store/actions_spec.js36
-rw-r--r--spec/frontend/dropzone_input_spec.js4
-rw-r--r--spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js11
-rw-r--r--spec/frontend/emoji/awards_app/store/actions_spec.js17
-rw-r--r--spec/frontend/environments/deploy_board_component_spec.js4
-rw-r--r--spec/frontend/environments/edit_environment_spec.js8
-rw-r--r--spec/frontend/environments/environment_details/components/deployment_actions_spec.js47
-rw-r--r--spec/frontend/environments/environment_details/components/deployment_job_spec.js (renamed from spec/frontend/environments/environment_details/deployment_job_spec.js)0
-rw-r--r--spec/frontend/environments/environment_details/components/deployment_status_link_spec.js (renamed from spec/frontend/environments/environment_details/deployment_status_link_spec.js)0
-rw-r--r--spec/frontend/environments/environment_details/components/deployment_triggerer_spec.js (renamed from spec/frontend/environments/environment_details/deployment_triggerer_spec.js)0
-rw-r--r--spec/frontend/environments/environment_form_spec.js9
-rw-r--r--spec/frontend/environments/environments_app_spec.js30
-rw-r--r--spec/frontend/environments/environments_folder_view_spec.js3
-rw-r--r--spec/frontend/environments/folder/environments_folder_view_spec.js50
-rw-r--r--spec/frontend/environments/graphql/mock_data.js8
-rw-r--r--spec/frontend/environments/graphql/resolvers_spec.js25
-rw-r--r--spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap10
-rw-r--r--spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js31
-rw-r--r--spec/frontend/environments/mixins/environments_pagination_api_mixin_spec.js69
-rw-r--r--spec/frontend/environments/new_environment_spec.js12
-rw-r--r--spec/frontend/environments/stop_stale_environments_modal_spec.js60
-rw-r--r--spec/frontend/error_tracking/store/actions_spec.js5
-rw-r--r--spec/frontend/error_tracking/store/details/actions_spec.js11
-rw-r--r--spec/frontend/error_tracking_settings/store/actions_spec.js9
-rw-r--r--spec/frontend/feature_flags/components/edit_feature_flag_spec.js3
-rw-r--r--spec/frontend/feature_flags/components/feature_flags_spec.js33
-rw-r--r--spec/frontend/feature_flags/components/new_environments_dropdown_spec.js2
-rw-r--r--spec/frontend/feature_flags/store/edit/actions_spec.js15
-rw-r--r--spec/frontend/feature_flags/store/index/actions_spec.js17
-rw-r--r--spec/frontend/feature_flags/store/new/actions_spec.js7
-rw-r--r--spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js3
-rw-r--r--spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js3
-rw-r--r--spec/frontend/filtered_search/visual_token_value_spec.js2
-rw-r--r--spec/frontend/fixtures/jobs.rb1
-rw-r--r--spec/frontend/fixtures/listbox.rb5
-rw-r--r--spec/frontend/fixtures/merge_requests.rb47
-rw-r--r--spec/frontend/fixtures/pipelines.rb11
-rw-r--r--spec/frontend/fixtures/runner.rb2
-rw-r--r--spec/frontend/fixtures/saved_replies.rb46
-rw-r--r--spec/frontend/fixtures/static/project_select_combo_button.html13
-rw-r--r--spec/frontend/flash_spec.js108
-rw-r--r--spec/frontend/frequent_items/components/app_spec.js6
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_list_item_spec.js3
-rw-r--r--spec/frontend/frequent_items/components/frequent_items_search_input_spec.js1
-rw-r--r--spec/frontend/frequent_items/store/actions_spec.js8
-rw-r--r--spec/frontend/gfm_auto_complete_spec.js18
-rw-r--r--spec/frontend/gl_form_spec.js41
-rw-r--r--spec/frontend/gpg_badges_spec.js19
-rw-r--r--spec/frontend/graphql_shared/utils_spec.js26
-rw-r--r--spec/frontend/groups/components/app_spec.js24
-rw-r--r--spec/frontend/groups/components/invite_members_banner_spec.js5
-rw-r--r--spec/frontend/groups_projects/components/transfer_locations_spec.js21
-rw-r--r--spec/frontend/header_search/components/app_spec.js4
-rw-r--r--spec/frontend/header_search/store/actions_spec.js7
-rw-r--r--spec/frontend/helpers/init_simple_app_helper_spec.js61
-rw-r--r--spec/frontend/ide/components/panes/right_spec.js25
-rw-r--r--spec/frontend/ide/components/preview/clientside_spec.js416
-rw-r--r--spec/frontend/ide/components/preview/navigator_spec.js161
-rw-r--r--spec/frontend/ide/components/repo_editor_spec.js3
-rw-r--r--spec/frontend/ide/init_gitlab_web_ide_spec.js13
-rw-r--r--spec/frontend/ide/lib/gitlab_web_ide/handle_tracking_event_spec.js32
-rw-r--r--spec/frontend/ide/lib/mirror_spec.js10
-rw-r--r--spec/frontend/ide/remote/index_spec.js3
-rw-r--r--spec/frontend/ide/services/index_spec.js9
-rw-r--r--spec/frontend/ide/services/terminals_spec.js3
-rw-r--r--spec/frontend/ide/stores/actions/file_spec.js9
-rw-r--r--spec/frontend/ide/stores/actions/merge_request_spec.js13
-rw-r--r--spec/frontend/ide/stores/actions/tree_spec.js5
-rw-r--r--spec/frontend/ide/stores/actions_spec.js5
-rw-r--r--spec/frontend/ide/stores/getters_spec.js12
-rw-r--r--spec/frontend/ide/stores/modules/branches/actions_spec.js9
-rw-r--r--spec/frontend/ide/stores/modules/clientside/actions_spec.js38
-rw-r--r--spec/frontend/ide/stores/modules/commit/actions_spec.js3
-rw-r--r--spec/frontend/ide/stores/modules/file_templates/actions_spec.js13
-rw-r--r--spec/frontend/ide/stores/modules/merge_requests/actions_spec.js9
-rw-r--r--spec/frontend/ide/stores/modules/pipelines/actions_spec.js25
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js19
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_controls_spec.js19
-rw-r--r--spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js5
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_actions_cell_spec.js2
-rw-r--r--spec/frontend/import_entities/import_groups/components/import_table_spec.js78
-rw-r--r--spec/frontend/import_entities/import_groups/services/status_poller_spec.js3
-rw-r--r--spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js67
-rw-r--r--spec/frontend/import_entities/import_projects/store/actions_spec.js43
-rw-r--r--spec/frontend/import_entities/import_projects/store/mutations_spec.js26
-rw-r--r--spec/frontend/import_entities/import_projects/utils_spec.js2
-rw-r--r--spec/frontend/integrations/index/components/integrations_table_spec.js57
-rw-r--r--spec/frontend/invite_members/components/invite_members_modal_spec.js107
-rw-r--r--spec/frontend/invite_members/components/invite_modal_base_spec.js15
-rw-r--r--spec/frontend/invite_members/components/project_select_spec.js56
-rw-r--r--spec/frontend/invite_members/mock_data/api_response_data.js2
-rw-r--r--spec/frontend/issuable/helpers.js18
-rw-r--r--spec/frontend/issuable/issuable_form_spec.js72
-rw-r--r--spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js15
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_block_spec.js6
-rw-r--r--spec/frontend/issuable/related_issues/components/related_issues_root_spec.js17
-rw-r--r--spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js76
-rw-r--r--spec/frontend/issues/dashboard/mock_data.js14
-rw-r--r--spec/frontend/issues/dashboard/utils_spec.js5
-rw-r--r--spec/frontend/issues/issue_spec.js3
-rw-r--r--spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js10
-rw-r--r--spec/frontend/issues/list/components/issue_card_time_info_spec.js6
-rw-r--r--spec/frontend/issues/list/components/issues_list_app_spec.js95
-rw-r--r--spec/frontend/issues/list/components/new_issue_dropdown_spec.js133
-rw-r--r--spec/frontend/issues/list/mock_data.js51
-rw-r--r--spec/frontend/issues/related_merge_requests/components/related_merge_requests_spec.js3
-rw-r--r--spec/frontend/issues/related_merge_requests/store/actions_spec.js7
-rw-r--r--spec/frontend/issues/show/components/app_spec.js40
-rw-r--r--spec/frontend/issues/show/components/description_spec.js237
-rw-r--r--spec/frontend/issues/show/components/fields/type_spec.js62
-rw-r--r--spec/frontend/issues/show/components/header_actions_spec.js20
-rw-r--r--spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js6
-rw-r--r--spec/frontend/issues/show/components/incidents/mock_data.js37
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_form_spec.js49
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js39
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js33
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js4
-rw-r--r--spec/frontend/issues/show/components/incidents/timeline_events_tags_popover_spec.js48
-rw-r--r--spec/frontend/issues/show/components/incidents/utils_spec.js18
-rw-r--r--spec/frontend/issues/show/components/locked_warning_spec.js61
-rw-r--r--spec/frontend/issues/show/components/task_list_item_actions_spec.js54
-rw-r--r--spec/frontend/issues/show/issue_spec.js7
-rw-r--r--spec/frontend/issues/show/mock_data/mock_data.js8
-rw-r--r--spec/frontend/issues/show/utils_spec.js272
-rw-r--r--spec/frontend/jira_connect/subscriptions/api_spec.js2
-rw-r--r--spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js74
-rw-r--r--spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js49
-rw-r--r--spec/frontend/jobs/components/job/job_app_spec.js32
-rw-r--r--spec/frontend/jobs/components/job/manual_variables_form_spec.js19
-rw-r--r--spec/frontend/jobs/components/job/mock_data.js22
-rw-r--r--spec/frontend/jobs/components/table/cells/actions_cell_spec.js19
-rw-r--r--spec/frontend/jobs/store/actions_spec.js22
-rw-r--r--spec/frontend/labels/components/promote_label_modal_spec.js9
-rw-r--r--spec/frontend/language_switcher/components/app_spec.js2
-rw-r--r--spec/frontend/lazy_loader_spec.js6
-rw-r--r--spec/frontend/lib/apollo/mock_data/cache_with_persist_directive.json3089
-rw-r--r--spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json3091
-rw-r--r--spec/frontend/lib/apollo/mock_data/non_persisted_cache.json3089
-rw-r--r--spec/frontend/lib/apollo/persist_link_spec.js74
-rw-r--r--spec/frontend/lib/apollo/persistence_mapper_spec.js163
-rw-r--r--spec/frontend/lib/utils/ajax_cache_spec.js7
-rw-r--r--spec/frontend/lib/utils/apollo_startup_js_link_spec.js11
-rw-r--r--spec/frontend/lib/utils/axios_startup_calls_spec.js17
-rw-r--r--spec/frontend/lib/utils/axios_utils_spec.js9
-rw-r--r--spec/frontend/lib/utils/common_utils_spec.js111
-rw-r--r--spec/frontend/lib/utils/favicon_ci_spec.js5
-rw-r--r--spec/frontend/lib/utils/icon_utils_spec.js9
-rw-r--r--spec/frontend/lib/utils/poll_spec.js22
-rw-r--r--spec/frontend/lib/utils/rails_ujs_spec.js3
-rw-r--r--spec/frontend/lib/utils/scroll_utils_spec.js21
-rw-r--r--spec/frontend/lib/utils/select2_utils_spec.js100
-rw-r--r--spec/frontend/lib/utils/text_markdown_spec.js36
-rw-r--r--spec/frontend/listbox/index_spec.js4
-rw-r--r--spec/frontend/members/components/table/member_action_buttons_spec.js6
-rw-r--r--spec/frontend/members/components/table/members_table_spec.js9
-rw-r--r--spec/frontend/members/utils_spec.js6
-rw-r--r--spec/frontend/merge_conflicts/store/actions_spec.js11
-rw-r--r--spec/frontend/merge_request_spec.js5
-rw-r--r--spec/frontend/merge_requests/components/compare_app_spec.js50
-rw-r--r--spec/frontend/merge_requests/components/compare_dropdown_spec.js (renamed from spec/frontend/merge_requests/components/target_project_dropdown_spec.js)40
-rw-r--r--spec/frontend/milestones/components/delete_milestone_modal_spec.js7
-rw-r--r--spec/frontend/milestones/components/milestone_combobox_spec.js37
-rw-r--r--spec/frontend/milestones/components/promote_milestone_modal_spec.js3
-rw-r--r--spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_candidate_spec.js.snap8
-rw-r--r--spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap761
-rw-r--r--spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js2
-rw-r--r--spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js359
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js110
-rw-r--r--spec/frontend/ml/experiment_tracking/routes/experiments/index/components/mock_data.js21
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap3
-rw-r--r--spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap7
-rw-r--r--spec/frontend/monitoring/requests/index_spec.js7
-rw-r--r--spec/frontend/monitoring/store/actions_spec.js31
-rw-r--r--spec/frontend/mr_notes/stores/actions_spec.js6
-rw-r--r--spec/frontend/nav/components/new_nav_toggle_spec.js5
-rw-r--r--spec/frontend/nav/components/top_nav_app_spec.js2
-rw-r--r--spec/frontend/nav/components/top_nav_container_view_spec.js1
-rw-r--r--spec/frontend/nav/components/top_nav_dropdown_menu_spec.js1
-rw-r--r--spec/frontend/nav/components/top_nav_menu_sections_spec.js23
-rw-r--r--spec/frontend/notes/components/attachments_warning_spec.js16
-rw-r--r--spec/frontend/notes/components/comment_field_layout_spec.js64
-rw-r--r--spec/frontend/notes/components/comment_form_spec.js16
-rw-r--r--spec/frontend/notes/components/discussion_filter_spec.js3
-rw-r--r--spec/frontend/notes/components/note_actions_spec.js54
-rw-r--r--spec/frontend/notes/components/note_awards_list_spec.js3
-rw-r--r--spec/frontend/notes/components/noteable_note_spec.js7
-rw-r--r--spec/frontend/notes/components/notes_app_spec.js3
-rw-r--r--spec/frontend/notes/deprecated_notes_spec.js61
-rw-r--r--spec/frontend/notes/mock_data.js5
-rw-r--r--spec/frontend/notes/stores/actions_spec.js42
-rw-r--r--spec/frontend/packages_and_registries/dependency_proxy/app_spec.js3
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap1
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js5
-rw-r--r--spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap14
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap104
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/package_versions_list_spec.js90
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js109
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js (renamed from spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js)78
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap25
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js43
-rw-r--r--spec/frontend/packages_and_registries/package_registry/components/list/packages_list_spec.js4
-rw-r--r--spec/frontend/packages_and_registries/package_registry/mock_data.js44
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/details_spec.js56
-rw-r--r--spec/frontend/packages_and_registries/package_registry/pages/list_spec.js75
-rw-r--r--spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js64
-rw-r--r--spec/frontend/packages_and_registries/shared/components/registry_list_spec.js28
-rw-r--r--spec/frontend/pager_spec.js3
-rw-r--r--spec/frontend/pages/admin/application_settings/account_and_limits_spec.js67
-rw-r--r--spec/frontend/pages/admin/jobs/index/components/cancel_jobs_modal_spec.js (renamed from spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js)21
-rw-r--r--spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js57
-rw-r--r--spec/frontend/pages/admin/projects/components/namespace_select_spec.js155
-rw-r--r--spec/frontend/pages/dashboard/todos/index/todos_spec.js3
-rw-r--r--spec/frontend/pages/import/bulk_imports/history/components/bulk_imports_history_app_spec.js9
-rw-r--r--spec/frontend/pages/import/history/components/import_error_details_spec.js5
-rw-r--r--spec/frontend/pages/import/history/components/import_history_app_spec.js16
-rw-r--r--spec/frontend/pages/projects/find_file/ref_switcher/ref_switcher_utils_spec.js39
-rw-r--r--spec/frontend/pages/projects/forks/new/components/fork_form_spec.js38
-rw-r--r--spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap109
-rw-r--r--spec/frontend/pages/projects/graphs/code_coverage_spec.js4
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap62
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap409
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_card_spec.js27
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js233
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js113
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_trial_card_spec.js12
-rw-r--r--spec/frontend/pages/projects/learn_gitlab/components/mock_data.js73
-rw-r--r--spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js237
-rw-r--r--spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap7
-rw-r--r--spec/frontend/pages/search/show/refresh_counts_spec.js43
-rw-r--r--spec/frontend/pages/shared/wikis/components/wiki_form_spec.js11
-rw-r--r--spec/frontend/performance_bar/index_spec.js3
-rw-r--r--spec/frontend/persistent_user_callout_spec.js13
-rw-r--r--spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js17
-rw-r--r--spec/frontend/pipelines/graph/action_component_spec.js95
-rw-r--r--spec/frontend/pipelines/graph/graph_component_spec.js49
-rw-r--r--spec/frontend/pipelines/graph/graph_component_wrapper_spec.js19
-rw-r--r--spec/frontend/pipelines/graph/job_item_spec.js308
-rw-r--r--spec/frontend/pipelines/graph/linked_pipeline_spec.js6
-rw-r--r--spec/frontend/pipelines/graph/mock_data.js44
-rw-r--r--spec/frontend/pipelines/linked_pipelines_mock.json127
-rw-r--r--spec/frontend/pipelines/mock_data.js2
-rw-r--r--spec/frontend/pipelines/pipeline_multi_actions_spec.js5
-rw-r--r--spec/frontend/pipelines/pipelines_actions_spec.js9
-rw-r--r--spec/frontend/pipelines/pipelines_spec.js67
-rw-r--r--spec/frontend/pipelines/pipelines_table_spec.js8
-rw-r--r--spec/frontend/pipelines/test_reports/stores/actions_spec.js5
-rw-r--r--spec/frontend/pipelines/utils_spec.js37
-rw-r--r--spec/frontend/profile/account/components/update_username_spec.js12
-rw-r--r--spec/frontend/profile/components/activity_tab_spec.js19
-rw-r--r--spec/frontend/profile/components/contributed_projects_tab_spec.js21
-rw-r--r--spec/frontend/profile/components/followers_tab_spec.js19
-rw-r--r--spec/frontend/profile/components/following_tab_spec.js19
-rw-r--r--spec/frontend/profile/components/groups_tab_spec.js19
-rw-r--r--spec/frontend/profile/components/overview_tab_spec.js19
-rw-r--r--spec/frontend/profile/components/personal_projects_tab_spec.js21
-rw-r--r--spec/frontend/profile/components/profile_tabs_spec.js36
-rw-r--r--spec/frontend/profile/components/snippets_tab_spec.js19
-rw-r--r--spec/frontend/profile/components/starred_projects_tab_spec.js21
-rw-r--r--spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap8
-rw-r--r--spec/frontend/project_select_combo_button_spec.js165
-rw-r--r--spec/frontend/projects/commit/components/branches_dropdown_spec.js133
-rw-r--r--spec/frontend/projects/commit/components/form_modal_spec.js2
-rw-r--r--spec/frontend/projects/commit/components/projects_dropdown_spec.js64
-rw-r--r--spec/frontend/projects/commit/mock_data.js6
-rw-r--r--spec/frontend/projects/commit/store/getters_spec.js8
-rw-r--r--spec/frontend/projects/commit_box/info/load_branches_spec.js7
-rw-r--r--spec/frontend/projects/commits/store/actions_spec.js5
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js7
-rw-r--r--spec/frontend/projects/compare/components/revision_dropdown_spec.js7
-rw-r--r--spec/frontend/projects/project_find_file_spec.js3
-rw-r--r--spec/frontend/projects/project_new_spec.js18
-rw-r--r--spec/frontend/projects/prune_unreachable_objects_button_spec.js72
-rw-r--r--spec/frontend/projects/report_abuse/components/report_abuse_dropdown_item_spec.js (renamed from spec/frontend/projects/merge_requests/components/report_abuse_dropdown_item_spec.js)4
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/index_spec.js73
-rw-r--r--spec/frontend/projects/settings/branch_rules/components/view/mock_data.js42
-rw-r--r--spec/frontend/projects/settings/components/default_branch_selector_spec.js1
-rw-r--r--spec/frontend/projects/settings/components/new_access_dropdown_spec.js8
-rw-r--r--spec/frontend/projects/settings/components/shared_runners_toggle_spec.js9
-rw-r--r--spec/frontend/projects/settings/mock_data.js4
-rw-r--r--spec/frontend/projects/settings/repository/branch_rules/app_spec.js56
-rw-r--r--spec/frontend/prometheus_metrics/custom_metrics_spec.js3
-rw-r--r--spec/frontend/prometheus_metrics/prometheus_metrics_spec.js3
-rw-r--r--spec/frontend/protected_branches/protected_branch_edit_spec.js7
-rw-r--r--spec/frontend/ref/components/ref_selector_spec.js246
-rw-r--r--spec/frontend/ref/format_refs_spec.js38
-rw-r--r--spec/frontend/ref/mock_data.js87
-rw-r--r--spec/frontend/related_issues/components/related_issuable_input_spec.js5
-rw-r--r--spec/frontend/releases/components/app_edit_new_spec.js16
-rw-r--r--spec/frontend/releases/components/app_index_spec.js14
-rw-r--r--spec/frontend/releases/components/app_show_spec.js7
-rw-r--r--spec/frontend/releases/components/evidence_block_spec.js8
-rw-r--r--spec/frontend/releases/components/release_block_assets_spec.js36
-rw-r--r--spec/frontend/releases/components/releases_empty_state_spec.js39
-rw-r--r--spec/frontend/releases/release_notification_service_spec.js57
-rw-r--r--spec/frontend/releases/stores/modules/detail/actions_spec.js5
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js20
-rw-r--r--spec/frontend/repository/components/blob_content_viewer_spec.js2
-rw-r--r--spec/frontend/repository/components/fork_info_spec.js70
-rw-r--r--spec/frontend/repository/components/last_commit_spec.js8
-rw-r--r--spec/frontend/repository/components/preview/index_spec.js24
-rw-r--r--spec/frontend/repository/log_tree_spec.js3
-rw-r--r--spec/frontend/repository/mixins/highlight_mixin_spec.js106
-rw-r--r--spec/frontend/repository/mock_data.js4
-rw-r--r--spec/frontend/repository/utils/ref_switcher_utils_spec.js6
-rw-r--r--spec/frontend/right_sidebar_spec.js3
-rw-r--r--spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap21
-rw-r--r--spec/frontend/saved_replies/components/list_item_spec.js22
-rw-r--r--spec/frontend/saved_replies/components/list_spec.js68
-rw-r--r--spec/frontend/search/mock_data.js461
-rw-r--r--spec/frontend/search/sidebar/components/app_spec.js63
-rw-r--r--spec/frontend/search/sidebar/components/checkbox_filter_spec.js85
-rw-r--r--spec/frontend/search/sidebar/components/confidentiality_filter_spec.js20
-rw-r--r--spec/frontend/search/sidebar/components/language_filters_spec.js152
-rw-r--r--spec/frontend/search/sidebar/components/status_filter_spec.js20
-rw-r--r--spec/frontend/search/sidebar/utils_spec.js10
-rw-r--r--spec/frontend/search/store/actions_spec.js58
-rw-r--r--spec/frontend/search/store/getters_spec.js20
-rw-r--r--spec/frontend/search/store/mutations_spec.js14
-rw-r--r--spec/frontend/search_autocomplete_spec.js3
-rw-r--r--spec/frontend/self_monitor/store/actions_spec.js10
-rw-r--r--spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js4
-rw-r--r--spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js8
-rw-r--r--spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js6
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js9
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js16
-rw-r--r--spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js9
-rw-r--r--spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js4
-rw-r--r--spec/frontend/sidebar/components/move/move_issue_button_spec.js157
-rw-r--r--spec/frontend/sidebar/components/move/move_issues_button_spec.js7
-rw-r--r--spec/frontend/sidebar/components/severity/sidebar_severity_spec.js40
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_spec.js6
-rw-r--r--spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js6
-rw-r--r--spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js6
-rw-r--r--spec/frontend/sidebar/lib/sidebar_move_issue_spec.js162
-rw-r--r--spec/frontend/sidebar/sidebar_mediator_spec.js11
-rw-r--r--spec/frontend/single_file_diff_spec.js7
-rw-r--r--spec/frontend/snippets/components/snippet_blob_edit_spec.js7
-rw-r--r--spec/frontend/super_sidebar/components/counter_spec.js4
-rw-r--r--spec/frontend/super_sidebar/components/create_menu_spec.js39
-rw-r--r--spec/frontend/super_sidebar/components/help_center_spec.js152
-rw-r--r--spec/frontend/super_sidebar/components/merge_request_menu_spec.js46
-rw-r--r--spec/frontend/super_sidebar/components/super_sidebar_spec.js10
-rw-r--r--spec/frontend/super_sidebar/components/user_bar_spec.js21
-rw-r--r--spec/frontend/super_sidebar/mock_data.js70
-rw-r--r--spec/frontend/terms/components/app_spec.js7
-rw-r--r--spec/frontend/token_access/inbound_token_access_spec.js311
-rw-r--r--spec/frontend/token_access/mock_data.js122
-rw-r--r--spec/frontend/token_access/opt_in_jwt_spec.js144
-rw-r--r--spec/frontend/token_access/outbound_token_access_spec.js (renamed from spec/frontend/token_access/token_access_spec.js)8
-rw-r--r--spec/frontend/token_access/token_access_app_spec.js47
-rw-r--r--spec/frontend/token_access/token_projects_table_spec.js7
-rw-r--r--spec/frontend/tracking/get_standard_context_spec.js2
-rw-r--r--spec/frontend/usage_quotas/components/usage_quotas_app_spec.js39
-rw-r--r--spec/frontend/usage_quotas/mock_data.js3
-rw-r--r--spec/frontend/users/profile/components/report_abuse_button_spec.js2
-rw-r--r--spec/frontend/users_select/test_helper.js3
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js44
-rw-r--r--spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js109
-rw-r--r--spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js3
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js3
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js13
-rw-r--r--spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js11
-rw-r--r--spec/frontend/vue_merge_request_widget/components/report_widget_container_spec.js33
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js4
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js23
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js18
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js11
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js6
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js14
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js576
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js2
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js9
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js42
-rw-r--r--spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js182
-rw-r--r--spec/frontend/vue_merge_request_widget/components/widget/widget_spec.js43
-rw-r--r--spec/frontend/vue_merge_request_widget/extensions/security_reports/mr_widget_security_reports_spec.js7
-rw-r--r--spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js15
-rw-r--r--spec/frontend/vue_merge_request_widget/mock_data.js90
-rw-r--r--spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js21
-rw-r--r--spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js15
-rw-r--r--spec/frontend/vue_merge_request_widget/test_extensions.js5
-rw-r--r--spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap1
-rw-r--r--spec/frontend/vue_shared/components/confidentiality_badge_spec.js10
-rw-r--r--spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js17
-rw-r--r--spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js4
-rw-r--r--spec/frontend/vue_shared/components/dismissible_container_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js19
-rw-r--r--spec/frontend/vue_shared/components/entity_select/entity_select_spec.js268
-rw-r--r--spec/frontend/vue_shared/components/entity_select/group_select_spec.js135
-rw-r--r--spec/frontend/vue_shared/components/entity_select/project_select_spec.js248
-rw-r--r--spec/frontend/vue_shared/components/entity_select/utils_spec.js (renamed from spec/frontend/vue_shared/components/group_select/utils_spec.js)4
-rw-r--r--spec/frontend/vue_shared/components/file_icon_spec.js5
-rw-r--r--spec/frontend/vue_shared/components/file_row_spec.js9
-rw-r--r--spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js12
-rw-r--r--spec/frontend/vue_shared/components/group_select/group_select_spec.js322
-rw-r--r--spec/frontend/vue_shared/components/header_ci_component_spec.js6
-rw-r--r--spec/frontend/vue_shared/components/incubation/incubation_alert_spec.js (renamed from spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js)17
-rw-r--r--spec/frontend/vue_shared/components/incubation/pagination_spec.js76
-rw-r--r--spec/frontend/vue_shared/components/markdown/field_spec.js43
-rw-r--r--spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js7
-rw-r--r--spec/frontend/vue_shared/components/new_resource_dropdown/mock_data.js54
-rw-r--r--spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js262
-rw-r--r--spec/frontend/vue_shared/components/notes/system_note_spec.js3
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js72
-rw-r--r--spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js41
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/instructions/runner_aws_instructions_spec.js29
-rw-r--r--spec/frontend/vue_shared/components/runner_instructions/runner_instructions_modal_spec.js2
-rw-r--r--spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap8
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap24
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js123
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js94
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js70
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/mock_data.js24
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js177
-rw-r--r--spec/frontend/vue_shared/components/source_viewer/source_viewer_spec.js156
-rw-r--r--spec/frontend/vue_shared/components/url_sync_spec.js80
-rw-r--r--spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js31
-rw-r--r--spec/frontend/vue_shared/components/user_select_spec.js14
-rw-r--r--spec/frontend/vue_shared/components/web_ide_link_spec.js13
-rw-r--r--spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js17
-rw-r--r--spec/frontend/vue_shared/security_reports/security_reports_app_spec.js9
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js15
-rw-r--r--spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js15
-rw-r--r--spec/frontend/vue_shared/security_reports/store/utils_spec.js63
-rw-r--r--spec/frontend/webhooks/components/test_dropdown_spec.js63
-rw-r--r--spec/frontend/whats_new/components/app_spec.js1
-rw-r--r--spec/frontend/whats_new/store/actions_spec.js7
-rw-r--r--spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap3
-rw-r--r--spec/frontend/work_items/components/notes/system_note_spec.js3
-rw-r--r--spec/frontend/work_items/components/notes/work_item_add_note_spec.js (renamed from spec/frontend/work_items/components/work_item_comment_form_spec.js)123
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_form_spec.js164
-rw-r--r--spec/frontend/work_items/components/notes/work_item_comment_locked_spec.js (renamed from spec/frontend/work_items/components/work_item_comment_locked_spec.js)2
-rw-r--r--spec/frontend/work_items/components/notes/work_item_discussion_spec.js149
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_actions_spec.js52
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_replying_spec.js34
-rw-r--r--spec/frontend/work_items/components/notes/work_item_note_spec.js256
-rw-r--r--spec/frontend/work_items/components/widget_wrapper_spec.js46
-rw-r--r--spec/frontend/work_items/components/work_item_created_updated_spec.js104
-rw-r--r--spec/frontend/work_items/components/work_item_description_spec.js44
-rw-r--r--spec/frontend/work_items/components/work_item_detail_modal_spec.js6
-rw-r--r--spec/frontend/work_items/components/work_item_detail_spec.js32
-rw-r--r--spec/frontend/work_items/components/work_item_labels_spec.js40
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_form_spec.js4
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_links_spec.js103
-rw-r--r--spec/frontend/work_items/components/work_item_links/work_item_tree_spec.js32
-rw-r--r--spec/frontend/work_items/components/work_item_notes_spec.js171
-rw-r--r--spec/frontend/work_items/mock_data.js443
-rw-r--r--spec/frontend/work_items/utils_spec.js27
-rw-r--r--spec/frontend/zen_mode_spec.js3
575 files changed, 25389 insertions, 8962 deletions
diff --git a/spec/frontend/__helpers__/init_vue_mr_page_helper.js b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
index 83ed0a869dc..d01affdaeac 100644
--- a/spec/frontend/__helpers__/init_vue_mr_page_helper.js
+++ b/spec/frontend/__helpers__/init_vue_mr_page_helper.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import initMRPage from '~/mr_notes';
import { getDiffFileMock } from '../diffs/mock_data/diff_file';
import { userDataMock, notesDataMock, noteableDataMock } from '../notes/mock_data';
@@ -37,7 +38,7 @@ export default function initVueMRPage() {
mrTestEl.appendChild(diffsAppEl);
const mock = new MockAdapter(axios);
- mock.onGet(diffsAppEndpoint).reply(200, {
+ mock.onGet(diffsAppEndpoint).reply(HTTP_STATUS_OK, {
branch_name: 'foo',
diff_files: [getDiffFileMock()],
});
diff --git a/spec/frontend/__helpers__/test_apollo_link.js b/spec/frontend/__helpers__/test_apollo_link.js
index eab0c2de212..d9e7f5fc348 100644
--- a/spec/frontend/__helpers__/test_apollo_link.js
+++ b/spec/frontend/__helpers__/test_apollo_link.js
@@ -18,7 +18,7 @@ const FOO_QUERY = gql`
*
* @returns Promise resolving to the resulting operation after running the subjectLink
*/
-export const testApolloLink = (subjectLink, options = {}) =>
+export const testApolloLink = (subjectLink, options = {}, query = FOO_QUERY) =>
new Promise((resolve) => {
const { context = {} } = options;
@@ -38,6 +38,6 @@ export const testApolloLink = (subjectLink, options = {}) =>
// Trigger a query so the ApolloLink chain will be executed.
client.query({
context,
- query: FOO_QUERY,
+ query,
});
});
diff --git a/spec/frontend/__helpers__/vuex_action_helper_spec.js b/spec/frontend/__helpers__/vuex_action_helper_spec.js
index 182aea9c1c5..4bd21ff150a 100644
--- a/spec/frontend/__helpers__/vuex_action_helper_spec.js
+++ b/spec/frontend/__helpers__/vuex_action_helper_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import testActionFn from './vuex_action_helper';
const testActionFnWithOptionsArg = (...args) => {
@@ -101,7 +102,7 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
};
it('returns original data of successful promise while checking actions/mutations', async () => {
- mock.onGet(TEST_HOST).replyOnce(200, 42);
+ mock.onGet(TEST_HOST).replyOnce(HTTP_STATUS_OK, 42);
assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
@@ -110,7 +111,7 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
});
it('returns original error of rejected promise while checking actions/mutations', async () => {
- mock.onGet(TEST_HOST).replyOnce(500, '');
+ mock.onGet(TEST_HOST).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, '');
assertion = { mutations: [{ type: 'ERROR' }], actions: [{ type: 'ACTION' }] };
@@ -137,7 +138,7 @@ describe.each([testActionFn, testActionFnWithOptionsArg])(
});
};
- mock.onGet(TEST_HOST).replyOnce(200, 42);
+ mock.onGet(TEST_HOST).replyOnce(HTTP_STATUS_OK, 42);
assertion = { mutations: [{ type: 'SUCCESS' }], actions: [{ type: 'ACTION' }] };
diff --git a/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
index 6efd9fb1dd0..ec20088c443 100644
--- a/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
+++ b/spec/frontend/abuse_reports/components/abuse_category_selector_spec.js
@@ -13,18 +13,18 @@ describe('AbuseCategorySelector', () => {
let wrapper;
const ACTION_PATH = '/abuse_reports/add_category';
- const USER_ID = '1';
+ const USER_ID = 1;
const REPORTED_FROM_URL = 'http://example.com';
const createComponent = (props) => {
wrapper = shallowMountExtended(AbuseCategorySelector, {
propsData: {
+ reportedUserId: USER_ID,
+ reportedFromUrl: REPORTED_FROM_URL,
...props,
},
provide: {
reportAbusePath: ACTION_PATH,
- reportedUserId: USER_ID,
- reportedFromUrl: REPORTED_FROM_URL,
},
});
};
@@ -106,7 +106,7 @@ describe('AbuseCategorySelector', () => {
expect(findUserId().attributes()).toMatchObject({
type: 'hidden',
name: 'user_id',
- value: USER_ID,
+ value: USER_ID.toString(),
});
});
diff --git a/spec/frontend/abuse_reports/components/links_to_spam_input_spec.js b/spec/frontend/abuse_reports/components/links_to_spam_input_spec.js
new file mode 100644
index 00000000000..c0c87dd1383
--- /dev/null
+++ b/spec/frontend/abuse_reports/components/links_to_spam_input_spec.js
@@ -0,0 +1,65 @@
+import { GlButton, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import LinksToSpamInput from '~/abuse_reports/components/links_to_spam_input.vue';
+
+describe('LinksToSpamInput', () => {
+ let wrapper;
+
+ const createComponent = (props) => {
+ wrapper = shallowMountExtended(LinksToSpamInput, {
+ propsData: {
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findAllFormGroups = () => wrapper.findAllComponents(GlFormGroup);
+ const findLinkInput = () => wrapper.findComponent(GlFormInput);
+ const findAddAnotherButton = () => wrapper.findComponent(GlButton);
+
+ describe('Form Input', () => {
+ it('renders only one input field initially', () => {
+ expect(findAllFormGroups()).toHaveLength(1);
+ });
+
+ it('is of type URL and has a name attribute', () => {
+ expect(findLinkInput().attributes()).toMatchObject({
+ type: 'url',
+ name: 'abuse_report[links_to_spam][]',
+ value: '',
+ });
+ });
+
+ describe('when add another link button is clicked', () => {
+ it('adds another input', async () => {
+ findAddAnotherButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(findAllFormGroups()).toHaveLength(2);
+ });
+ });
+
+ describe('when previously added links are passed to the form as props', () => {
+ beforeEach(() => {
+ createComponent({ previousLinks: ['https://gitlab.com'] });
+ });
+
+ it('renders the input field with the value of the link pre-filled', () => {
+ expect(findAllFormGroups()).toHaveLength(1);
+
+ expect(findLinkInput().attributes()).toMatchObject({
+ type: 'url',
+ name: 'abuse_report[links_to_spam][]',
+ value: 'https://gitlab.com',
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/add_context_commits_modal/store/actions_spec.js b/spec/frontend/add_context_commits_modal/store/actions_spec.js
index 4b58a69c2b8..27c8d760a96 100644
--- a/spec/frontend/add_context_commits_modal/store/actions_spec.js
+++ b/spec/frontend/add_context_commits_modal/store/actions_spec.js
@@ -16,6 +16,7 @@ import {
} from '~/add_context_commits_modal/store/actions';
import * as types from '~/add_context_commits_modal/store/mutation_types';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_NO_CONTENT, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('AddContextCommitsModalStoreActions', () => {
const contextCommitEndpoint =
@@ -99,7 +100,7 @@ describe('AddContextCommitsModalStoreActions', () => {
describe('createContextCommits', () => {
it('calls API to create context commits', async () => {
- mock.onPost(contextCommitEndpoint).reply(200, {});
+ mock.onPost(contextCommitEndpoint).reply(HTTP_STATUS_OK, {});
await testAction(createContextCommits, { commits: [] }, {}, [], []);
@@ -116,7 +117,7 @@ describe('AddContextCommitsModalStoreActions', () => {
.onGet(
`/api/${gon.api_version}/projects/gitlab-org%2Fgitlab/merge_requests/1/context_commits`,
)
- .reply(200, [dummyCommit]);
+ .reply(HTTP_STATUS_OK, [dummyCommit]);
});
it('commits FETCH_CONTEXT_COMMITS', () => {
const contextCommit = { ...dummyCommit, isSelected: true };
@@ -156,7 +157,7 @@ describe('AddContextCommitsModalStoreActions', () => {
beforeEach(() => {
mock
.onDelete('/api/v4/projects/gitlab-org%2Fgitlab/merge_requests/1/context_commits')
- .reply(204);
+ .reply(HTTP_STATUS_NO_CONTENT);
});
it('calls API to remove context commits', () => {
return testAction(
diff --git a/spec/frontend/admin/broadcast_messages/components/base_spec.js b/spec/frontend/admin/broadcast_messages/components/base_spec.js
index 020e1c1d7c1..d69bf4a22bf 100644
--- a/spec/frontend/admin/broadcast_messages/components/base_spec.js
+++ b/spec/frontend/admin/broadcast_messages/components/base_spec.js
@@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import BroadcastMessagesBase from '~/admin/broadcast_messages/components/base.vue';
import MessagesTable from '~/admin/broadcast_messages/components/messages_table.vue';
@@ -70,7 +71,7 @@ describe('BroadcastMessagesBase', () => {
it('does not remove a deleted message if the request fails', async () => {
createComponent();
const { id, delete_path } = MOCK_MESSAGES[0];
- axiosMock.onDelete(delete_path).replyOnce(500);
+ axiosMock.onDelete(delete_path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
findTable().vm.$emit('delete-message', id);
await waitForPromises();
@@ -86,7 +87,7 @@ describe('BroadcastMessagesBase', () => {
it('removes a deleted message from visibleMessages on success', async () => {
createComponent();
const { id, delete_path } = MOCK_MESSAGES[0];
- axiosMock.onDelete(delete_path).replyOnce(200);
+ axiosMock.onDelete(delete_path).replyOnce(HTTP_STATUS_OK);
findTable().vm.$emit('delete-message', id);
await waitForPromises();
@@ -102,7 +103,7 @@ describe('BroadcastMessagesBase', () => {
const { id, delete_path } = messages[0];
createComponent({ messages, messagesCount: messages.length });
- axiosMock.onDelete(delete_path).replyOnce(200);
+ axiosMock.onDelete(delete_path).replyOnce(HTTP_STATUS_OK);
findTable().vm.$emit('delete-message', id);
await waitForPromises();
diff --git a/spec/frontend/admin/statistics_panel/components/app_spec.js b/spec/frontend/admin/statistics_panel/components/app_spec.js
index 190f0eb94a0..4c362a31068 100644
--- a/spec/frontend/admin/statistics_panel/components/app_spec.js
+++ b/spec/frontend/admin/statistics_panel/components/app_spec.js
@@ -8,6 +8,7 @@ import statisticsLabels from '~/admin/statistics_panel/constants';
import createStore from '~/admin/statistics_panel/store';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import mockStatistics from '../mock_data';
Vue.use(Vuex);
@@ -25,7 +26,7 @@ describe('Admin statistics app', () => {
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
- axiosMock.onGet(/api\/(.*)\/application\/statistics/).reply(200);
+ axiosMock.onGet(/api\/(.*)\/application\/statistics/).reply(HTTP_STATUS_OK);
store = createStore();
});
diff --git a/spec/frontend/admin/statistics_panel/store/actions_spec.js b/spec/frontend/admin/statistics_panel/store/actions_spec.js
index e7cdb5feb6a..20d5860a459 100644
--- a/spec/frontend/admin/statistics_panel/store/actions_spec.js
+++ b/spec/frontend/admin/statistics_panel/store/actions_spec.js
@@ -5,6 +5,7 @@ import * as actions from '~/admin/statistics_panel/store/actions';
import * as types from '~/admin/statistics_panel/store/mutation_types';
import getInitialState from '~/admin/statistics_panel/store/state';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import mockStatistics from '../mock_data';
describe('Admin statistics panel actions', () => {
@@ -19,7 +20,7 @@ describe('Admin statistics panel actions', () => {
describe('fetchStatistics', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(200, mockStatistics);
+ mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(HTTP_STATUS_OK, mockStatistics);
});
it('dispatches success with received data', () => {
@@ -43,7 +44,9 @@ describe('Admin statistics panel actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(/api\/(.*)\/application\/statistics/).replyOnce(500);
+ mock
+ .onGet(/api\/(.*)\/application\/statistics/)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
@@ -99,12 +102,12 @@ describe('Admin statistics panel actions', () => {
it('should commit error', () => {
return testAction(
actions.receiveStatisticsError,
- 500,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
state,
[
{
type: types.RECEIVE_STATISTICS_ERROR,
- payload: 500,
+ payload: HTTP_STATUS_INTERNAL_SERVER_ERROR,
},
],
[],
diff --git a/spec/frontend/admin/statistics_panel/store/mutations_spec.js b/spec/frontend/admin/statistics_panel/store/mutations_spec.js
index 0a3dad09c9a..70c1e723f08 100644
--- a/spec/frontend/admin/statistics_panel/store/mutations_spec.js
+++ b/spec/frontend/admin/statistics_panel/store/mutations_spec.js
@@ -1,6 +1,7 @@
import * as types from '~/admin/statistics_panel/store/mutation_types';
import mutations from '~/admin/statistics_panel/store/mutations';
import getInitialState from '~/admin/statistics_panel/store/state';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import mockStatistics from '../mock_data';
describe('Admin statistics panel mutations', () => {
@@ -30,11 +31,10 @@ describe('Admin statistics panel mutations', () => {
describe(`${types.RECEIVE_STATISTICS_ERROR}`, () => {
it('sets error and clears data', () => {
- const error = 500;
- mutations[types.RECEIVE_STATISTICS_ERROR](state, error);
+ mutations[types.RECEIVE_STATISTICS_ERROR](state, HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(state.isLoading).toBe(false);
- expect(state.error).toBe(error);
+ expect(state.error).toBe(HTTP_STATUS_INTERNAL_SERVER_ERROR);
expect(state.statistics).toEqual(null);
});
});
diff --git a/spec/frontend/admin/topics/components/topic_select_spec.js b/spec/frontend/admin/topics/components/topic_select_spec.js
index f61af6203f0..738cbd88c4c 100644
--- a/spec/frontend/admin/topics/components/topic_select_spec.js
+++ b/spec/frontend/admin/topics/components/topic_select_spec.js
@@ -1,39 +1,66 @@
-import { GlAvatarLabeled, GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlAvatarLabeled, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
import TopicSelect from '~/admin/topics/components/topic_select.vue';
+import searchProjectTopics from '~/graphql_shared/queries/project_topics_search.query.graphql';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
const mockTopics = [
- { id: 1, name: 'topic1', title: 'Topic 1', avatarUrl: 'avatar.com/topic1.png' },
- { id: 2, name: 'GitLab', title: 'GitLab', avatarUrl: 'avatar.com/GitLab.png' },
+ {
+ id: 'gid://gitlab/Projects::Topic/6',
+ name: 'topic1',
+ title: 'Topic 1',
+ avatarUrl: 'avatar.com/topic1.png',
+ __typename: 'Topic',
+ },
+ {
+ id: 'gid://gitlab/Projects::Topic/5',
+ name: 'gitlab',
+ title: 'GitLab',
+ avatarUrl: 'avatar.com/GitLab.png',
+ __typename: 'Topic',
+ },
];
+const mockTopicsQueryResponse = {
+ data: {
+ topics: {
+ nodes: mockTopics,
+ __typename: 'TopicConnection',
+ },
+ },
+};
+
describe('TopicSelect', () => {
let wrapper;
+ const mockSearchTopicsSuccess = jest.fn().mockResolvedValue(mockTopicsQueryResponse);
+
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findAllDropdownItems = () => wrapper.findAllComponents(GlDropdownItem);
+ function createMockApolloProvider({ mockSearchTopicsQuery = mockSearchTopicsSuccess } = {}) {
+ Vue.use(VueApollo);
+
+ return createMockApollo([[searchProjectTopics, mockSearchTopicsQuery]]);
+ }
- function createComponent(props = {}) {
- wrapper = shallowMount(TopicSelect, {
+ function createComponent({ props = {}, mockApollo } = {}) {
+ wrapper = mount(TopicSelect, {
+ apolloProvider: mockApollo || createMockApolloProvider(),
propsData: props,
data() {
return {
topics: mockTopics,
- search: '',
};
},
- mocks: {
- $apollo: {
- queries: {
- topics: { loading: false },
- },
- },
- },
});
}
afterEach(() => {
wrapper.destroy();
+ jest.clearAllMocks();
});
it('mounts', () => {
@@ -57,17 +84,27 @@ describe('TopicSelect', () => {
it('renders default text if no selected topic', () => {
createComponent();
- expect(findDropdown().props('text')).toBe('Select a topic');
+ expect(findListbox().props('toggleText')).toBe('Select a topic');
});
it('renders selected topic', () => {
- createComponent({ selectedTopic: mockTopics[0] });
+ const mockTopic = mockTopics[0];
- expect(findDropdown().props('text')).toBe('topic1');
+ createComponent({
+ props: {
+ selectedTopic: mockTopic,
+ },
+ });
+
+ expect(findListbox().props('toggleText')).toBe(mockTopic.name);
});
it('renders label', () => {
- createComponent({ labelText: 'my label' });
+ createComponent({
+ props: {
+ labelText: 'my label',
+ },
+ });
expect(wrapper.find('label').text()).toBe('my label');
});
@@ -75,17 +112,52 @@ describe('TopicSelect', () => {
it('renders dropdown items', () => {
createComponent();
- const dropdownItems = findAllDropdownItems();
+ const listboxItems = findAllListboxItems();
+
+ expect(listboxItems.at(0).findComponent(GlAvatarLabeled).props('label')).toBe('Topic 1');
+ expect(listboxItems.at(1).findComponent(GlAvatarLabeled).props('label')).toBe('GitLab');
+ });
+
+ it('dropdown `toggledAriaLabelledBy` prop is not set if `labelText` prop is null', () => {
+ createComponent();
- expect(dropdownItems.at(0).findComponent(GlAvatarLabeled).props('label')).toBe('Topic 1');
- expect(dropdownItems.at(1).findComponent(GlAvatarLabeled).props('label')).toBe('GitLab');
+ expect(findListbox().props('toggle-aria-labelled-by')).toBe(undefined);
});
- it('emits `click` event when topic selected', () => {
+ it('emits `click` event when topic selected', async () => {
createComponent();
- findAllDropdownItems().at(0).vm.$emit('click');
+ await findAllListboxItems().at(0).trigger('click');
expect(wrapper.emitted('click')).toEqual([[mockTopics[0]]]);
});
+
+ describe('when searching a topic', () => {
+ const searchTopic = (searchTerm) => findListbox().vm.$emit('search', searchTerm);
+ const mockSearchTerm = 'gitl';
+
+ it('toggles loading state', async () => {
+ createComponent();
+ jest.runOnlyPendingTimers();
+
+ await searchTopic(mockSearchTerm);
+
+ expect(findListbox().props('searching')).toBe(true);
+
+ await waitForPromises();
+
+ expect(findListbox().props('searching')).toBe(false);
+ });
+
+ it('fetches topics matching search string', async () => {
+ createComponent();
+
+ await searchTopic(mockSearchTerm);
+ jest.runOnlyPendingTimers();
+
+ expect(mockSearchTopicsSuccess).toHaveBeenCalledWith({
+ search: mockSearchTerm,
+ });
+ });
+ });
});
diff --git a/spec/frontend/airflow/dags/components/dags_spec.js b/spec/frontend/airflow/dags/components/dags_spec.js
new file mode 100644
index 00000000000..f9cf4fc87af
--- /dev/null
+++ b/spec/frontend/airflow/dags/components/dags_spec.js
@@ -0,0 +1,115 @@
+import { GlAlert, GlPagination, GlTableLite } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { TEST_HOST } from 'helpers/test_constants';
+import AirflowDags from '~/airflow/dags/components/dags.vue';
+import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
+import { mockDags } from './mock_data';
+
+describe('AirflowDags', () => {
+ let wrapper;
+
+ const createWrapper = (
+ dags = [],
+ pagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 0 },
+ ) => {
+ wrapper = mountExtended(AirflowDags, {
+ propsData: {
+ dags,
+ pagination,
+ },
+ });
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findEmptyState = () => wrapper.findByText('There are no DAGs to show');
+ const findPagination = () => wrapper.findComponent(GlPagination);
+
+ describe('default (no dags)', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('shows incubation warning', () => {
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ it('shows empty state', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('does not show pagination', () => {
+ expect(findPagination().exists()).toBe(false);
+ });
+ });
+
+ describe('with dags', () => {
+ const createWrapperWithDags = (pagination = {}) => {
+ createWrapper(mockDags, {
+ page: 1,
+ isLastPage: false,
+ per_page: 2,
+ totalItems: 5,
+ ...pagination,
+ });
+ };
+
+ const findDagsData = () => {
+ return wrapper
+ .findComponent(GlTableLite)
+ .findAll('tbody tr')
+ .wrappers.map((tr) => {
+ return tr.findAll('td').wrappers.map((td) => {
+ const timeAgo = td.findComponent(TimeAgo);
+
+ if (timeAgo.exists()) {
+ return {
+ type: 'time',
+ value: timeAgo.props('time'),
+ };
+ }
+
+ return {
+ type: 'text',
+ value: td.text(),
+ };
+ });
+ });
+ };
+
+ it('renders the table of Dags with data', () => {
+ createWrapperWithDags();
+
+ expect(findDagsData()).toEqual(
+ mockDags.map((x) => [
+ { type: 'text', value: x.dag_name },
+ { type: 'text', value: x.schedule },
+ { type: 'time', value: x.next_run },
+ { type: 'text', value: String(x.is_active) },
+ { type: 'text', value: String(x.is_paused) },
+ { type: 'text', value: x.fileloc },
+ ]),
+ );
+ });
+
+ describe('Pagination behaviour', () => {
+ it.each`
+ pagination | expected
+ ${{}} | ${{ value: 1, prevPage: null, nextPage: 2 }}
+ ${{ page: 2 }} | ${{ value: 2, prevPage: 1, nextPage: 3 }}
+ ${{ isLastPage: true, page: 2 }} | ${{ value: 2, prevPage: 1, nextPage: null }}
+ `('with $pagination, sets pagination props', ({ pagination, expected }) => {
+ createWrapperWithDags({ ...pagination });
+
+ expect(findPagination().props()).toMatchObject(expected);
+ });
+
+ it('generates link for each page', () => {
+ createWrapperWithDags();
+
+ const generateLink = findPagination().props('linkGen');
+
+ expect(generateLink(3)).toBe(`${TEST_HOST}/?page=3`);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/airflow/dags/components/mock_data.js b/spec/frontend/airflow/dags/components/mock_data.js
new file mode 100644
index 00000000000..9547282517d
--- /dev/null
+++ b/spec/frontend/airflow/dags/components/mock_data.js
@@ -0,0 +1,67 @@
+export const mockDags = [
+ {
+ id: 1,
+ project_id: 7,
+ created_at: '2023-01-05T14:07:02.975Z',
+ updated_at: '2023-01-05T14:07:02.975Z',
+ has_import_errors: false,
+ is_active: false,
+ is_paused: true,
+ next_run: '2023-01-05T14:07:02.975Z',
+ dag_name: 'Dag number 1',
+ schedule: 'Manual',
+ fileloc: '/opt/dag.py',
+ },
+ {
+ id: 2,
+ project_id: 7,
+ created_at: '2023-01-05T14:07:02.975Z',
+ updated_at: '2023-01-05T14:07:02.975Z',
+ has_import_errors: false,
+ is_active: false,
+ is_paused: true,
+ next_run: '2023-01-05T14:07:02.975Z',
+ dag_name: 'Dag number 2',
+ schedule: 'Manual',
+ fileloc: '/opt/dag.py',
+ },
+ {
+ id: 3,
+ project_id: 7,
+ created_at: '2023-01-05T14:07:02.975Z',
+ updated_at: '2023-01-05T14:07:02.975Z',
+ has_import_errors: false,
+ is_active: false,
+ is_paused: true,
+ next_run: '2023-01-05T14:07:02.975Z',
+ dag_name: 'Dag number 3',
+ schedule: 'Manual',
+ fileloc: '/opt/dag.py',
+ },
+ {
+ id: 4,
+ project_id: 7,
+ created_at: '2023-01-05T14:07:02.975Z',
+ updated_at: '2023-01-05T14:07:02.975Z',
+ has_import_errors: false,
+ is_active: false,
+ is_paused: true,
+ next_run: '2023-01-05T14:07:02.975Z',
+ dag_name: 'Dag number 4',
+ schedule: 'Manual',
+ fileloc: '/opt/dag.py',
+ },
+ {
+ id: 5,
+ project_id: 7,
+ created_at: '2023-01-05T14:07:02.975Z',
+ updated_at: '2023-01-05T14:07:02.975Z',
+ has_import_errors: false,
+ is_active: false,
+ is_paused: true,
+ next_run: '2023-01-05T14:07:02.975Z',
+ dag_name: 'Dag number 5',
+ schedule: 'Manual',
+ fileloc: '/opt/dag.py',
+ },
+];
diff --git a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
index bff4905a12c..0e402e61bcc 100644
--- a/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
+++ b/spec/frontend/alerts_settings/components/__snapshots__/alerts_form_spec.js.snap
@@ -89,6 +89,7 @@ exports[`Alert integration settings form default state should match the default
optionaltext="(optional)"
>
<gl-form-checkbox-stub
+ data-qa-selector="enable_email_notification_checkbox"
id="3"
>
<span>
diff --git a/spec/frontend/analytics/shared/components/metric_popover_spec.js b/spec/frontend/analytics/shared/components/metric_popover_spec.js
index 6a58f8c6d29..e0bfff3e664 100644
--- a/spec/frontend/analytics/shared/components/metric_popover_spec.js
+++ b/spec/frontend/analytics/shared/components/metric_popover_spec.js
@@ -1,6 +1,7 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import MetricPopover from '~/analytics/shared/components/metric_popover.vue';
+import { METRIC_POPOVER_LABEL } from '~/analytics/shared/constants';
const MOCK_METRIC = {
key: 'deployment-frequency',
@@ -27,10 +28,11 @@ describe('MetricPopover', () => {
};
const findMetricLabel = () => wrapper.findByTestId('metric-label');
- const findAllMetricLinks = () => wrapper.findAll('[data-testid="metric-link"]');
+ const findMetricLink = () => wrapper.find('[data-testid="metric-link"]');
const findMetricDescription = () => wrapper.findByTestId('metric-description');
const findMetricDocsLink = () => wrapper.findByTestId('metric-docs-link');
const findMetricDocsLinkIcon = () => findMetricDocsLink().findComponent(GlIcon);
+ const findMetricDetailsIcon = () => findMetricLink().findComponent(GlIcon);
afterEach(() => {
wrapper.destroy();
@@ -47,17 +49,14 @@ describe('MetricPopover', () => {
});
describe('with links', () => {
+ const METRIC_NAME = 'Deployment frequency';
+ const LINK_URL = '/groups/gitlab-org/-/analytics/ci_cd?tab=deployment-frequency';
const links = [
{
- name: 'Deployment frequency',
- url: '/groups/gitlab-org/-/analytics/ci_cd?tab=deployment-frequency',
+ name: METRIC_NAME,
+ url: LINK_URL,
label: 'Dashboard',
},
- {
- name: 'Another link',
- url: '/groups/gitlab-org/-/analytics/another-link',
- label: 'Another link',
- },
];
const docsLink = {
name: 'Deployment frequency',
@@ -68,37 +67,34 @@ describe('MetricPopover', () => {
const linksWithDocs = [...links, docsLink];
describe.each`
- hasDocsLink | allLinks | displayedMetricLinks
- ${true} | ${linksWithDocs} | ${links}
- ${false} | ${links} | ${links}
- `(
- 'when one link has docs_link=$hasDocsLink',
- ({ hasDocsLink, allLinks, displayedMetricLinks }) => {
- beforeEach(() => {
- wrapper = createComponent({ metric: { ...MOCK_METRIC, links: allLinks } });
- });
+ hasDocsLink | allLinks
+ ${true} | ${linksWithDocs}
+ ${false} | ${links}
+ `('when one link has docs_link=$hasDocsLink', ({ hasDocsLink, allLinks }) => {
+ beforeEach(() => {
+ wrapper = createComponent({ metric: { ...MOCK_METRIC, links: allLinks } });
+ });
- displayedMetricLinks.forEach((link, idx) => {
- it(`renders a link for "${link.name}"`, () => {
- const allLinkContainers = findAllMetricLinks();
+ describe('Metric title row', () => {
+ it(`renders a link for "${METRIC_NAME}"`, () => {
+ expect(findMetricLink().text()).toContain(METRIC_POPOVER_LABEL);
+ expect(findMetricLink().findComponent(GlLink).attributes('href')).toBe(LINK_URL);
+ });
- expect(allLinkContainers.at(idx).text()).toContain(link.name);
- expect(allLinkContainers.at(idx).findComponent(GlLink).attributes('href')).toBe(
- link.url,
- );
- });
+ it('renders the chart icon', () => {
+ expect(findMetricDetailsIcon().attributes('name')).toBe('chart');
});
+ });
- it(`${hasDocsLink ? 'renders' : "doesn't render"} a docs link`, () => {
- expect(findMetricDocsLink().exists()).toBe(hasDocsLink);
+ it(`${hasDocsLink ? 'renders' : "doesn't render"} a docs link`, () => {
+ expect(findMetricDocsLink().exists()).toBe(hasDocsLink);
- if (hasDocsLink) {
- expect(findMetricDocsLink().attributes('href')).toBe(docsLink.url);
- expect(findMetricDocsLink().text()).toBe(docsLink.label);
- expect(findMetricDocsLinkIcon().attributes('name')).toBe('external-link');
- }
- });
- },
- );
+ if (hasDocsLink) {
+ expect(findMetricDocsLink().attributes('href')).toBe(docsLink.url);
+ expect(findMetricDocsLink().text()).toBe(docsLink.label);
+ expect(findMetricDocsLinkIcon().attributes('name')).toBe('external-link');
+ }
+ });
+ });
});
});
diff --git a/spec/frontend/api/alert_management_alerts_api_spec.js b/spec/frontend/api/alert_management_alerts_api_spec.js
index aac14e64286..507f659a170 100644
--- a/spec/frontend/api/alert_management_alerts_api_spec.js
+++ b/spec/frontend/api/alert_management_alerts_api_spec.js
@@ -1,6 +1,11 @@
import MockAdapter from 'axios-mock-adapter';
import * as alertManagementAlertsApi from '~/api/alert_management_alerts_api';
import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_CREATED,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
describe('~/api/alert_management_alerts_api.js', () => {
let mock;
@@ -33,7 +38,7 @@ describe('~/api/alert_management_alerts_api.js', () => {
const expectedData = [imageData];
const options = { alertIid, id: projectId };
- mock.onGet(expectedUrl).reply(200, { data: expectedData });
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedData });
return alertManagementAlertsApi.fetchAlertMetricImages(options).then(({ data }) => {
expect(axios.get).toHaveBeenCalledWith(expectedUrl);
@@ -60,7 +65,7 @@ describe('~/api/alert_management_alerts_api.js', () => {
expectedFormData.append('url', url);
expectedFormData.append('url_text', urlText);
- mock.onPost(expectedUrl).reply(201, { data: expectedData });
+ mock.onPost(expectedUrl).reply(HTTP_STATUS_CREATED, { data: expectedData });
return alertManagementAlertsApi
.uploadAlertMetricImage({
@@ -96,7 +101,7 @@ describe('~/api/alert_management_alerts_api.js', () => {
expectedFormData.append('url', url);
expectedFormData.append('url_text', urlText);
- mock.onPut(expectedUrl).reply(200, { data: expectedData });
+ mock.onPut(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedData });
return alertManagementAlertsApi
.updateAlertMetricImage({
@@ -123,7 +128,7 @@ describe('~/api/alert_management_alerts_api.js', () => {
const expectedUrl = `/api/v4/projects/${projectId}/alert_management_alerts/${alertIid}/metric_images/${imageIid}`;
const expectedData = [imageData];
- mock.onDelete(expectedUrl).reply(204, { data: expectedData });
+ mock.onDelete(expectedUrl).reply(HTTP_STATUS_NO_CONTENT, { data: expectedData });
return alertManagementAlertsApi
.deleteAlertMetricImage({
diff --git a/spec/frontend/api/groups_api_spec.js b/spec/frontend/api/groups_api_spec.js
index c354d8a9416..0315db02cf2 100644
--- a/spec/frontend/api/groups_api_spec.js
+++ b/spec/frontend/api/groups_api_spec.js
@@ -3,7 +3,7 @@ import getGroupTransferLocationsResponse from 'test_fixtures/api/groups/transfer
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';
+import { updateGroup, getGroupTransferLocations, getGroupMembers } from '~/api/groups_api';
const mockApiVersion = 'v4';
const mockUrlRoot = '/gitlab';
@@ -55,7 +55,9 @@ describe('GroupsApi', () => {
const params = { page: 1 };
const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}/transfer_locations`;
- mock.onGet(expectedUrl).replyOnce(200, { data: getGroupTransferLocationsResponse });
+ mock
+ .onGet(expectedUrl)
+ .replyOnce(HTTP_STATUS_OK, { data: getGroupTransferLocationsResponse });
await expect(getGroupTransferLocations(mockGroupId, params)).resolves.toMatchObject({
data: { data: getGroupTransferLocationsResponse },
@@ -66,4 +68,30 @@ describe('GroupsApi', () => {
});
});
});
+
+ describe('getGroupMembers', () => {
+ it('requests members of a group', async () => {
+ const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}/members`;
+
+ const response = [{ id: 0, username: 'root' }];
+
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, response);
+
+ await expect(getGroupMembers(mockGroupId)).resolves.toMatchObject({
+ data: response,
+ });
+ });
+
+ it('requests inherited members of a group when requested', async () => {
+ const expectedUrl = `${mockUrlRoot}/api/${mockApiVersion}/groups/${mockGroupId}/members/all`;
+
+ const response = [{ id: 0, username: 'root' }];
+
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, response);
+
+ await expect(getGroupMembers(mockGroupId, true)).resolves.toMatchObject({
+ data: response,
+ });
+ });
+ });
});
diff --git a/spec/frontend/api/projects_api_spec.js b/spec/frontend/api/projects_api_spec.js
index 8459021421f..2d4ed39dad0 100644
--- a/spec/frontend/api/projects_api_spec.js
+++ b/spec/frontend/api/projects_api_spec.js
@@ -3,18 +3,22 @@ import getTransferLocationsResponse from 'test_fixtures/api/projects/transfer_lo
import * as projectsApi from '~/api/projects_api';
import { DEFAULT_PER_PAGE } from '~/api';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('~/api/projects_api.js', () => {
let mock;
let originalGon;
const projectId = 1;
+ const setfullPathProjectSearch = (value) => {
+ window.gon.features.fullPathProjectSearch = value;
+ };
beforeEach(() => {
mock = new MockAdapter(axios);
originalGon = window.gon;
- window.gon = { api_version: 'v7' };
+ window.gon = { api_version: 'v7', features: { fullPathProjectSearch: true } };
});
afterEach(() => {
@@ -27,14 +31,53 @@ describe('~/api/projects_api.js', () => {
jest.spyOn(axios, 'get');
});
+ const expectedUrl = '/api/v7/projects.json';
+ const expectedProjects = [{ name: 'project 1' }];
+ const options = {};
+
it('retrieves projects from the correct URL and returns them in the response data', () => {
- const expectedUrl = '/api/v7/projects.json';
const expectedParams = { params: { per_page: 20, search: '', simple: true } };
- const expectedProjects = [{ name: 'project 1' }];
const query = '';
- const options = {};
- mock.onGet(expectedUrl).reply(200, { data: expectedProjects });
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedProjects });
+
+ return projectsApi.getProjects(query, options).then(({ data }) => {
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams);
+ expect(data.data).toEqual(expectedProjects);
+ });
+ });
+
+ it('omits search param if query is undefined', () => {
+ const expectedParams = { params: { per_page: 20, simple: true } };
+
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedProjects });
+
+ return projectsApi.getProjects(undefined, options).then(({ data }) => {
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams);
+ expect(data.data).toEqual(expectedProjects);
+ });
+ });
+
+ it('searches namespaces if query contains a slash', () => {
+ const expectedParams = {
+ params: { per_page: 20, search: 'group/project1', search_namespaces: true, simple: true },
+ };
+ const query = 'group/project1';
+
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedProjects });
+
+ return projectsApi.getProjects(query, options).then(({ data }) => {
+ expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams);
+ expect(data.data).toEqual(expectedProjects);
+ });
+ });
+
+ it('does not search namespaces if fullPathProjectSearch is disabled', () => {
+ setfullPathProjectSearch(false);
+ const expectedParams = { params: { per_page: 20, search: 'group/project1', simple: true } };
+ const query = 'group/project1';
+
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, { data: expectedProjects });
return projectsApi.getProjects(query, options).then(({ data }) => {
expect(axios.get).toHaveBeenCalledWith(expectedUrl, expectedParams);
@@ -53,7 +96,7 @@ describe('~/api/projects_api.js', () => {
const expectedUrl = '/api/v7/projects/1/import_project_members/2';
const expectedMessage = 'Successfully imported';
- mock.onPost(expectedUrl).replyOnce(200, expectedMessage);
+ mock.onPost(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedMessage);
return projectsApi.importProjectMembers(projectId, targetId).then(({ data }) => {
expect(axios.post).toHaveBeenCalledWith(expectedUrl);
@@ -71,7 +114,7 @@ describe('~/api/projects_api.js', () => {
const params = { page: 1 };
const expectedUrl = '/api/v7/projects/1/transfer_locations';
- mock.onGet(expectedUrl).replyOnce(200, { data: getTransferLocationsResponse });
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, { data: getTransferLocationsResponse });
await expect(projectsApi.getTransferLocations(projectId, params)).resolves.toMatchObject({
data: { data: getTransferLocationsResponse },
@@ -82,4 +125,30 @@ describe('~/api/projects_api.js', () => {
});
});
});
+
+ describe('getProjectMembers', () => {
+ it('requests members of a project', async () => {
+ const expectedUrl = `/api/v7/projects/1/members`;
+
+ const response = [{ id: 0, username: 'root' }];
+
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, response);
+
+ await expect(projectsApi.getProjectMembers(projectId)).resolves.toMatchObject({
+ data: response,
+ });
+ });
+
+ it('requests inherited members of a project when requested', async () => {
+ const expectedUrl = `/api/v7/projects/1/members/all`;
+
+ const response = [{ id: 0, username: 'root' }];
+
+ mock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, response);
+
+ await expect(projectsApi.getProjectMembers(projectId, true)).resolves.toMatchObject({
+ data: response,
+ });
+ });
+ });
});
diff --git a/spec/frontend/api/user_api_spec.js b/spec/frontend/api/user_api_spec.js
index 9e901cf0f71..4d0252aad23 100644
--- a/spec/frontend/api/user_api_spec.js
+++ b/spec/frontend/api/user_api_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { followUser, unfollowUser, associationsCount, updateUserStatus } from '~/api/user_api';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
associationsCount as associationsCountData,
userStatus as mockUserStatus,
@@ -31,7 +32,7 @@ describe('~/api/user_api', () => {
const expectedUrl = '/api/v4/users/1/follow';
const expectedResponse = { message: 'Success' };
- axiosMock.onPost(expectedUrl).replyOnce(200, expectedResponse);
+ axiosMock.onPost(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse);
await expect(followUser(1)).resolves.toEqual(
expect.objectContaining({ data: expectedResponse }),
@@ -45,7 +46,7 @@ describe('~/api/user_api', () => {
const expectedUrl = '/api/v4/users/1/unfollow';
const expectedResponse = { message: 'Success' };
- axiosMock.onPost(expectedUrl).replyOnce(200, expectedResponse);
+ axiosMock.onPost(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse);
await expect(unfollowUser(1)).resolves.toEqual(
expect.objectContaining({ data: expectedResponse }),
@@ -59,7 +60,7 @@ describe('~/api/user_api', () => {
const expectedUrl = '/api/v4/users/1/associations_count';
const expectedResponse = { data: associationsCountData };
- axiosMock.onGet(expectedUrl).replyOnce(200, expectedResponse);
+ axiosMock.onGet(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse);
await expect(associationsCount(1)).resolves.toEqual(
expect.objectContaining({ data: expectedResponse }),
@@ -79,7 +80,7 @@ describe('~/api/user_api', () => {
};
const expectedResponse = { data: mockUserStatus };
- axiosMock.onPatch(expectedUrl).replyOnce(200, expectedResponse);
+ axiosMock.onPatch(expectedUrl).replyOnce(HTTP_STATUS_OK, expectedResponse);
await expect(
updateUserStatus({
diff --git a/spec/frontend/api_spec.js b/spec/frontend/api_spec.js
index 39fbe02480d..6fd106502c4 100644
--- a/spec/frontend/api_spec.js
+++ b/spec/frontend/api_spec.js
@@ -206,7 +206,7 @@ describe('Api', () => {
expires_at: undefined,
};
- mock.onPost(expectedUrl).reply(200, {
+ mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, {
status: 'success',
});
@@ -478,7 +478,7 @@ describe('Api', () => {
jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl).reply(200, {
+ mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, {
status: 'success',
});
@@ -494,7 +494,7 @@ describe('Api', () => {
const projectId = 1;
const options = { state: 'active' };
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/milestones`;
- mock.onGet(expectedUrl).reply(200, [
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, [
{
id: 1,
title: 'milestone1',
@@ -514,7 +514,7 @@ describe('Api', () => {
const projectId = 1;
const issueIid = 11;
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/projects/1/issues/11/todo`;
- mock.onPost(expectedUrl).reply(200, {
+ mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, {
id: 112,
project: {
id: 1,
@@ -541,7 +541,7 @@ describe('Api', () => {
expires_at: undefined,
};
- mock.onPost(expectedUrl).reply(200, {
+ mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, {
status: 'success',
});
@@ -625,7 +625,7 @@ describe('Api', () => {
const query = 'dummy query';
const expectedUrl = `${dummyUrlRoot}/api/${dummyApiVersion}/groups/${groupId}/projects.json`;
- mock.onGet(expectedUrl).reply(500, null);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, null);
const apiCall = Api.groupProjects(groupId, query, {});
await expect(apiCall).rejects.toThrow();
});
@@ -644,7 +644,7 @@ describe('Api', () => {
jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl).reply(200, {
+ mock.onPost(expectedUrl).reply(HTTP_STATUS_OK, {
status: 'success',
});
@@ -958,7 +958,7 @@ describe('Api', () => {
jest.spyOn(axios, 'post');
- mock.onPost(expectedUrl).replyOnce(200, [
+ mock.onPost(expectedUrl).replyOnce(HTTP_STATUS_OK, [
{
id: 'abcdefghijklmnop',
short_id: 'abcdefg',
@@ -984,7 +984,9 @@ describe('Api', () => {
mock
.onGet(expectedUrl)
- .replyOnce(200, [{ id: 'abcdef', short_id: 'abcdefghi', title: 'Dummy commit title' }]);
+ .replyOnce(HTTP_STATUS_OK, [
+ { id: 'abcdef', short_id: 'abcdefghi', title: 'Dummy commit title' },
+ ]);
return Api.allContextCommits(projectPath, mergeRequestId).then(({ data }) => {
expect(data[0].title).toBe('Dummy commit title');
@@ -1004,7 +1006,7 @@ describe('Api', () => {
jest.spyOn(axios, 'delete');
- mock.onDelete(expectedUrl).replyOnce(204);
+ mock.onDelete(expectedUrl).replyOnce(HTTP_STATUS_NO_CONTENT);
return Api.removeContextCommits(projectPath, mergeRequestId, expectedData).then(() => {
expect(axios.delete).toHaveBeenCalledWith(expectedUrl, { data: expectedData });
diff --git a/spec/frontend/artifacts/components/app_spec.js b/spec/frontend/artifacts/components/app_spec.js
new file mode 100644
index 00000000000..931c4703e95
--- /dev/null
+++ b/spec/frontend/artifacts/components/app_spec.js
@@ -0,0 +1,109 @@
+import { GlSkeletonLoader } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import { numberToHumanSize } from '~/lib/utils/number_utils';
+import ArtifactsApp from '~/artifacts/components/app.vue';
+import JobArtifactsTable from '~/artifacts/components/job_artifacts_table.vue';
+import getBuildArtifactsSizeQuery from '~/artifacts/graphql/queries/get_build_artifacts_size.query.graphql';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { PAGE_TITLE, TOTAL_ARTIFACTS_SIZE, SIZE_UNKNOWN } from '~/artifacts/constants';
+
+const TEST_BUILD_ARTIFACTS_SIZE = 1024;
+const TEST_PROJECT_PATH = 'project/path';
+const TEST_PROJECT_ID = 'gid://gitlab/Project/22';
+
+const createBuildArtifactsSizeResponse = (buildArtifactsSize) => ({
+ data: {
+ project: {
+ __typename: 'Project',
+ id: TEST_PROJECT_ID,
+ statistics: {
+ __typename: 'ProjectStatistics',
+ buildArtifactsSize,
+ },
+ },
+ },
+});
+
+Vue.use(VueApollo);
+
+describe('ArtifactsApp component', () => {
+ let wrapper;
+ let apolloProvider;
+ let getBuildArtifactsSizeSpy;
+
+ const findTitle = () => wrapper.findByTestId('artifacts-page-title');
+ const findBuildArtifactsSize = () => wrapper.findByTestId('build-artifacts-size');
+ const findJobArtifactsTable = () => wrapper.findComponent(JobArtifactsTable);
+ const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ArtifactsApp, {
+ provide: { projectPath: 'project/path' },
+ apolloProvider,
+ });
+ };
+
+ beforeEach(() => {
+ getBuildArtifactsSizeSpy = jest.fn();
+
+ apolloProvider = createMockApollo([[getBuildArtifactsSizeQuery, getBuildArtifactsSizeSpy]]);
+ });
+
+ describe('when loading', () => {
+ beforeEach(() => {
+ // Promise that never resolves so it's always loading
+ getBuildArtifactsSizeSpy.mockReturnValue(new Promise(() => {}));
+
+ createComponent();
+ });
+
+ it('shows the page title', () => {
+ expect(findTitle().text()).toBe(PAGE_TITLE);
+ });
+
+ it('shows a skeleton while loading the artifacts size', () => {
+ expect(findSkeletonLoader().exists()).toBe(true);
+ });
+
+ it('shows the job artifacts table', () => {
+ expect(findJobArtifactsTable().exists()).toBe(true);
+ });
+
+ it('does not show message', () => {
+ expect(findBuildArtifactsSize().text()).toBe('');
+ });
+
+ it('calls apollo query', () => {
+ expect(getBuildArtifactsSizeSpy).toHaveBeenCalledWith({ projectPath: TEST_PROJECT_PATH });
+ });
+ });
+
+ describe.each`
+ buildArtifactsSize | expectedText
+ ${TEST_BUILD_ARTIFACTS_SIZE} | ${numberToHumanSize(TEST_BUILD_ARTIFACTS_SIZE)}
+ ${null} | ${SIZE_UNKNOWN}
+ `('when buildArtifactsSize is $buildArtifactsSize', ({ buildArtifactsSize, expectedText }) => {
+ beforeEach(async () => {
+ getBuildArtifactsSizeSpy.mockResolvedValue(
+ createBuildArtifactsSizeResponse(buildArtifactsSize),
+ );
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('hides loader', () => {
+ expect(findSkeletonLoader().exists()).toBe(false);
+ });
+
+ it('shows the size', () => {
+ expect(findBuildArtifactsSize().text()).toMatchInterpolatedText(
+ `${TOTAL_ARTIFACTS_SIZE} ${expectedText}`,
+ );
+ });
+ });
+});
diff --git a/spec/frontend/authentication/u2f/register_spec.js b/spec/frontend/authentication/u2f/register_spec.js
index 7ae3a2734cb..23d1e5c7dee 100644
--- a/spec/frontend/authentication/u2f/register_spec.js
+++ b/spec/frontend/authentication/u2f/register_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { trimText } from 'helpers/text_helper';
import U2FRegister from '~/authentication/u2f/register';
import 'vendor/u2f';
import MockU2FDevice from './mock_u2f_device';
@@ -24,7 +25,7 @@ describe('U2FRegister', () => {
it('allows registering a U2F device', () => {
const setupButton = container.find('#js-setup-token-2fa-device');
- expect(setupButton.text()).toBe('Set up new device');
+ expect(trimText(setupButton.text())).toBe('Set up new device');
setupButton.trigger('click');
const inProgressMessage = container.children('p');
diff --git a/spec/frontend/authentication/webauthn/register_spec.js b/spec/frontend/authentication/webauthn/register_spec.js
index 95cb993fc70..773481346fc 100644
--- a/spec/frontend/authentication/webauthn/register_spec.js
+++ b/spec/frontend/authentication/webauthn/register_spec.js
@@ -1,5 +1,6 @@
import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { trimText } from 'helpers/text_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import WebAuthnRegister from '~/authentication/webauthn/register';
@@ -52,7 +53,7 @@ describe('WebAuthnRegister', () => {
const findRetryButton = () => container.find('#js-token-2fa-try-again');
it('shows setup button', () => {
- expect(findSetupButton().text()).toBe('Set up new device');
+ expect(trimText(findSetupButton().text())).toBe('Set up new device');
});
describe('when unsupported', () => {
diff --git a/spec/frontend/badges/store/actions_spec.js b/spec/frontend/badges/store/actions_spec.js
index b799273ff63..5ca199357f9 100644
--- a/spec/frontend/badges/store/actions_spec.js
+++ b/spec/frontend/badges/store/actions_spec.js
@@ -5,6 +5,7 @@ import actions, { transformBackendBadge } from '~/badges/store/actions';
import mutationTypes from '~/badges/store/mutation_types';
import createState from '~/badges/store/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { createDummyBadge, createDummyBadgeResponse } from '../dummy_badge';
describe('Badges store actions', () => {
@@ -98,7 +99,7 @@ describe('Badges store actions', () => {
expect(dispatch.mock.calls).toEqual([['requestNewBadge']]);
dispatch.mockClear();
- return [200, dummyResponse];
+ return [HTTP_STATUS_OK, dummyResponse];
});
const dummyBadge = transformBackendBadge(dummyResponse);
@@ -119,7 +120,7 @@ describe('Badges store actions', () => {
expect(dispatch.mock.calls).toEqual([['requestNewBadge']]);
dispatch.mockClear();
- return [500, ''];
+ return [HTTP_STATUS_INTERNAL_SERVER_ERROR, ''];
});
await expect(actions.addBadge({ state, dispatch })).rejects.toThrow();
@@ -176,7 +177,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestDeleteBadge', badgeId]]);
dispatch.mockClear();
- return [200, ''];
+ return [HTTP_STATUS_OK, ''];
});
await actions.deleteBadge({ state, dispatch }, { id: badgeId });
@@ -187,7 +188,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestDeleteBadge', badgeId]]);
dispatch.mockClear();
- return [500, ''];
+ return [HTTP_STATUS_INTERNAL_SERVER_ERROR, ''];
});
await expect(actions.deleteBadge({ state, dispatch }, { id: badgeId })).rejects.toThrow();
@@ -265,7 +266,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestLoadBadges', dummyData]]);
dispatch.mockClear();
- return [200, dummyReponse];
+ return [HTTP_STATUS_OK, dummyReponse];
});
await actions.loadBadges({ state, dispatch }, dummyData);
@@ -279,7 +280,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestLoadBadges', dummyData]]);
dispatch.mockClear();
- return [500, ''];
+ return [HTTP_STATUS_INTERNAL_SERVER_ERROR, ''];
});
await expect(actions.loadBadges({ state, dispatch }, dummyData)).rejects.toThrow();
@@ -380,7 +381,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestRenderedBadge']]);
dispatch.mockClear();
- return [200, dummyReponse];
+ return [HTTP_STATUS_OK, dummyReponse];
});
await actions.renderBadge({ state, dispatch });
@@ -393,7 +394,7 @@ describe('Badges store actions', () => {
endpointMock.replyOnce(() => {
expect(dispatch.mock.calls).toEqual([['requestRenderedBadge']]);
dispatch.mockClear();
- return [500, ''];
+ return [HTTP_STATUS_INTERNAL_SERVER_ERROR, ''];
});
await expect(actions.renderBadge({ state, dispatch })).rejects.toThrow();
@@ -467,7 +468,7 @@ describe('Badges store actions', () => {
expect(dispatch.mock.calls).toEqual([['requestUpdatedBadge']]);
dispatch.mockClear();
- return [200, dummyResponse];
+ return [HTTP_STATUS_OK, dummyResponse];
});
const updatedBadge = transformBackendBadge(dummyResponse);
@@ -487,7 +488,7 @@ describe('Badges store actions', () => {
expect(dispatch.mock.calls).toEqual([['requestUpdatedBadge']]);
dispatch.mockClear();
- return [500, ''];
+ return [HTTP_STATUS_INTERNAL_SERVER_ERROR, ''];
});
await expect(actions.saveBadge({ state, dispatch })).rejects.toThrow();
diff --git a/spec/frontend/batch_comments/components/draft_note_spec.js b/spec/frontend/batch_comments/components/draft_note_spec.js
index 2dfcdd551a1..924d88866ee 100644
--- a/spec/frontend/batch_comments/components/draft_note_spec.js
+++ b/spec/frontend/batch_comments/components/draft_note_spec.js
@@ -1,9 +1,8 @@
import { nextTick } from 'vue';
-import { GlButton, GlBadge } from '@gitlab/ui';
+import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { stubComponent } from 'helpers/stub_component';
import DraftNote from '~/batch_comments/components/draft_note.vue';
-import PublishButton from '~/batch_comments/components/publish_button.vue';
import { createStore } from '~/batch_comments/stores';
import NoteableNote from '~/notes/components/noteable_note.vue';
import { createDraft } from '../mock_data';
@@ -30,9 +29,6 @@ describe('Batch comments draft note component', () => {
},
};
- const findSubmitReviewButton = () => wrapper.findComponent(PublishButton);
- const findAddCommentButton = () => wrapper.findComponent(GlButton);
-
const createComponent = (propsData = { draft }, glFeatures = {}) => {
wrapper = shallowMount(DraftNote, {
store,
@@ -67,58 +63,6 @@ describe('Batch comments draft note component', () => {
expect(note.props().note).toEqual(draft);
});
- describe('add comment now', () => {
- it('dispatches publishSingleDraft when clicking', () => {
- createComponent();
- const publishNowButton = findAddCommentButton();
- publishNowButton.vm.$emit('click');
-
- expect(wrapper.vm.$store.dispatch).toHaveBeenCalledWith(
- 'batchComments/publishSingleDraft',
- 1,
- );
- });
-
- it('sets as loading when draft is publishing', async () => {
- createComponent();
- wrapper.vm.$store.state.batchComments.currentlyPublishingDrafts.push(1);
-
- await nextTick();
- const publishNowButton = findAddCommentButton();
-
- expect(publishNowButton.props().loading).toBe(true);
- });
-
- it('sets as disabled when review is publishing', async () => {
- createComponent();
- wrapper.vm.$store.state.batchComments.isPublishing = true;
-
- await nextTick();
- const publishNowButton = findAddCommentButton();
-
- expect(publishNowButton.props().disabled).toBe(true);
- expect(publishNowButton.props().loading).toBe(false);
- });
-
- it('hides button when mr_review_submit_comment is enabled', () => {
- createComponent({ draft }, { mrReviewSubmitComment: true });
-
- expect(findAddCommentButton().exists()).toBe(false);
- });
- });
-
- describe('submit review', () => {
- it('sets as disabled when draft is publishing', async () => {
- createComponent();
- wrapper.vm.$store.state.batchComments.currentlyPublishingDrafts.push(1);
-
- await nextTick();
- const publishNowButton = findSubmitReviewButton();
-
- expect(publishNowButton.attributes().disabled).toBe('true');
- });
- });
-
describe('update', () => {
it('dispatches updateDraft', async () => {
createComponent();
diff --git a/spec/frontend/batch_comments/components/preview_dropdown_spec.js b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
index 283632cb560..f86e003ab5f 100644
--- a/spec/frontend/batch_comments/components/preview_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/preview_dropdown_spec.js
@@ -1,9 +1,11 @@
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
import { visitUrl } from '~/lib/utils/url_utility';
import PreviewDropdown from '~/batch_comments/components/preview_dropdown.vue';
+import PreviewItem from '~/batch_comments/components/preview_item.vue';
jest.mock('~/lib/utils/url_utility', () => ({
visitUrl: jest.fn(),
@@ -17,6 +19,8 @@ let wrapper;
const setCurrentFileHash = jest.fn();
const scrollToDraft = jest.fn();
+const findPreviewItem = () => wrapper.findComponent(PreviewItem);
+
function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts = [] } = {}) {
const store = new Vuex.Store({
modules: {
@@ -42,16 +46,13 @@ function factory({ viewDiffsFileByFile = false, draftsCount = 1, sortedDrafts =
},
});
- wrapper = shallowMountExtended(PreviewDropdown, {
+ wrapper = shallowMount(PreviewDropdown, {
store,
+ stubs: { GlDisclosureDropdown },
});
}
describe('Batch comments preview dropdown', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('clicking draft', () => {
it('toggles active file when viewDiffsFileByFile is true', async () => {
factory({
@@ -59,12 +60,15 @@ describe('Batch comments preview dropdown', () => {
sortedDrafts: [{ id: 1, file_hash: 'hash' }],
});
- wrapper.findByTestId('preview-item').vm.$emit('click');
+ findPreviewItem().vm.$emit('click');
await nextTick();
expect(setCurrentFileHash).toHaveBeenCalledWith(expect.anything(), 'hash');
- expect(scrollToDraft).toHaveBeenCalledWith(expect.anything(), { id: 1, file_hash: 'hash' });
+ expect(scrollToDraft).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ id: 1, file_hash: 'hash' }),
+ );
});
it('calls scrollToDraft', async () => {
@@ -73,11 +77,14 @@ describe('Batch comments preview dropdown', () => {
sortedDrafts: [{ id: 1 }],
});
- wrapper.findByTestId('preview-item').vm.$emit('click');
+ findPreviewItem().vm.$emit('click');
await nextTick();
- expect(scrollToDraft).toHaveBeenCalledWith(expect.anything(), { id: 1 });
+ expect(scrollToDraft).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.objectContaining({ id: 1 }),
+ );
});
it('changes window location to navigate to commit', async () => {
@@ -86,7 +93,7 @@ describe('Batch comments preview dropdown', () => {
sortedDrafts: [{ id: 1, position: { head_sha: '1234' } }],
});
- wrapper.findByTestId('preview-item').vm.$emit('click');
+ findPreviewItem().vm.$emit('click');
await nextTick();
diff --git a/spec/frontend/batch_comments/components/publish_button_spec.js b/spec/frontend/batch_comments/components/publish_button_spec.js
deleted file mode 100644
index 5e3fa3e9446..00000000000
--- a/spec/frontend/batch_comments/components/publish_button_spec.js
+++ /dev/null
@@ -1,34 +0,0 @@
-import { nextTick } from 'vue';
-import { mount } from '@vue/test-utils';
-import PublishButton from '~/batch_comments/components/publish_button.vue';
-import { createStore } from '~/batch_comments/stores';
-
-describe('Batch comments publish button component', () => {
- let wrapper;
- let store;
-
- beforeEach(() => {
- store = createStore();
-
- wrapper = mount(PublishButton, { store, propsData: { shouldPublish: true } });
-
- jest.spyOn(store, 'dispatch').mockImplementation();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('dispatches publishReview on click', async () => {
- await wrapper.trigger('click');
-
- expect(store.dispatch).toHaveBeenCalledWith('batchComments/publishReview', undefined);
- });
-
- it('sets loading when isPublishing is true', async () => {
- store.state.batchComments.isPublishing = true;
-
- await nextTick();
- expect(wrapper.attributes('disabled')).toBe('disabled');
- });
-});
diff --git a/spec/frontend/batch_comments/components/publish_dropdown_spec.js b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
index e89934c0192..44d7b56c14f 100644
--- a/spec/frontend/batch_comments/components/publish_dropdown_spec.js
+++ b/spec/frontend/batch_comments/components/publish_dropdown_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue from 'vue';
import Vuex from 'vuex';
@@ -12,29 +12,30 @@ Vue.use(Vuex);
describe('Batch comments publish dropdown component', () => {
let wrapper;
+ const draft = createDraft();
function createComponent() {
const store = createStore();
- store.state.batchComments.drafts.push(createDraft(), { ...createDraft(), id: 2 });
+ store.state.batchComments.drafts.push(draft, { ...draft, id: 2 });
wrapper = shallowMount(PreviewDropdown, {
store,
+ stubs: { GlDisclosureDropdown },
});
}
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders list of drafts', () => {
createComponent();
- expect(wrapper.findAllComponents(GlDropdownItem).length).toBe(2);
+ expect(wrapper.findComponent(GlDisclosureDropdown).props('items')).toMatchObject([
+ draft,
+ { ...draft, id: 2 },
+ ]);
});
it('renders draft count in dropdown title', () => {
createComponent();
- expect(wrapper.findComponent(GlDropdown).props('headerText')).toEqual('2 pending comments');
+ expect(wrapper.findComponent(GlDisclosureDropdown).text()).toEqual('2 pending comments');
});
});
diff --git a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
index 6369ea9aa15..20eedcbb25b 100644
--- a/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
+++ b/spec/frontend/batch_comments/stores/modules/batch_comments/actions_spec.js
@@ -4,6 +4,7 @@ import testAction from 'helpers/vuex_action_helper';
import service from '~/batch_comments/services/drafts_service';
import * as actions from '~/batch_comments/stores/modules/batch_comments/actions';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Batch comments store actions', () => {
let res = {};
@@ -31,7 +32,7 @@ describe('Batch comments store actions', () => {
describe('addDraftToDiscussion', () => {
it('commits ADD_NEW_DRAFT if no errors returned', () => {
res = { id: 1 };
- mock.onAny().reply(200, res);
+ mock.onAny().reply(HTTP_STATUS_OK, res);
return testAction(
actions.addDraftToDiscussion,
@@ -43,7 +44,7 @@ describe('Batch comments store actions', () => {
});
it('does not commit ADD_NEW_DRAFT if errors returned', () => {
- mock.onAny().reply(500);
+ mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
actions.addDraftToDiscussion,
@@ -58,7 +59,7 @@ describe('Batch comments store actions', () => {
describe('createNewDraft', () => {
it('commits ADD_NEW_DRAFT if no errors returned', () => {
res = { id: 1 };
- mock.onAny().reply(200, res);
+ mock.onAny().reply(HTTP_STATUS_OK, res);
return testAction(
actions.createNewDraft,
@@ -70,7 +71,7 @@ describe('Batch comments store actions', () => {
});
it('does not commit ADD_NEW_DRAFT if errors returned', () => {
- mock.onAny().reply(500);
+ mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
actions.createNewDraft,
@@ -100,7 +101,7 @@ describe('Batch comments store actions', () => {
commit,
};
res = { id: 1 };
- mock.onAny().reply(200);
+ mock.onAny().reply(HTTP_STATUS_OK);
return actions.deleteDraft(context, { id: 1 }).then(() => {
expect(commit).toHaveBeenCalledWith('DELETE_DRAFT', 1);
@@ -113,7 +114,7 @@ describe('Batch comments store actions', () => {
getters,
commit,
};
- mock.onAny().reply(500);
+ mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return actions.deleteDraft(context, { id: 1 }).then(() => {
expect(commit).not.toHaveBeenCalledWith('DELETE_DRAFT', 1);
@@ -144,7 +145,7 @@ describe('Batch comments store actions', () => {
},
};
res = { id: 1 };
- mock.onAny().reply(200, res);
+ mock.onAny().reply(HTTP_STATUS_OK, res);
return actions.fetchDrafts(context).then(() => {
expect(commit).toHaveBeenCalledWith('SET_BATCH_COMMENTS_DRAFTS', { id: 1 });
@@ -169,7 +170,7 @@ describe('Batch comments store actions', () => {
});
it('dispatches actions & commits', () => {
- mock.onAny().reply(200);
+ mock.onAny().reply(HTTP_STATUS_OK);
return actions.publishReview({ dispatch, commit, getters, rootGetters }).then(() => {
expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']);
@@ -180,7 +181,7 @@ describe('Batch comments store actions', () => {
});
it('calls service with notes data', () => {
- mock.onAny().reply(200);
+ mock.onAny().reply(HTTP_STATUS_OK);
jest.spyOn(axios, 'post');
return actions
@@ -191,7 +192,7 @@ describe('Batch comments store actions', () => {
});
it('dispatches error commits', () => {
- mock.onAny().reply(500);
+ mock.onAny().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return actions.publishReview({ dispatch, commit, getters, rootGetters }).catch(() => {
expect(commit.mock.calls[0]).toEqual(['REQUEST_PUBLISH_REVIEW']);
@@ -221,7 +222,7 @@ describe('Batch comments store actions', () => {
commit,
};
res = { id: 1 };
- mock.onAny().reply(200, res);
+ mock.onAny().reply(HTTP_STATUS_OK, res);
params = { note: { id: 1 }, noteText: 'test' };
});
diff --git a/spec/frontend/blob/components/table_contents_spec.js b/spec/frontend/blob/components/table_contents_spec.js
index 5fe328b65ff..6af9cdcae7d 100644
--- a/spec/frontend/blob/components/table_contents_spec.js
+++ b/spec/frontend/blob/components/table_contents_spec.js
@@ -1,4 +1,4 @@
-import { GlDropdownItem } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
@@ -10,6 +10,8 @@ function createComponent() {
wrapper = shallowMount(TableContents);
}
+const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+
async function setLoaded(loaded) {
document.querySelector('.blob-viewer').dataset.loaded = loaded;
@@ -20,10 +22,10 @@ describe('Markdown table of contents component', () => {
beforeEach(() => {
setHTMLFixture(`
<div class="blob-viewer" data-type="rich" data-loaded="false">
- <h1><a href="#1"></a>Hello</h1>
- <h2><a href="#2"></a>World</h2>
- <h3><a href="#3"></a>Testing</h3>
- <h2><a href="#4"></a>GitLab</h2>
+ <h1><a id="hello">$</a> Hello</h1>
+ <h2><a id="world">$</a> World</h2>
+ <h3><a id="hakuna">$</a> Hakuna</h3>
+ <h2><a id="matata">$</a> Matata</h2>
</div>
`);
});
@@ -34,12 +36,10 @@ describe('Markdown table of contents component', () => {
});
describe('not loaded', () => {
- const findDropdownItem = () => wrapper.findComponent(GlDropdownItem);
-
it('does not populate dropdown', () => {
createComponent();
- expect(findDropdownItem().exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(false);
});
it('does not show dropdown when loading blob content', async () => {
@@ -47,7 +47,7 @@ describe('Markdown table of contents component', () => {
await setLoaded(false);
- expect(findDropdownItem().exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(false);
});
it('does not show dropdown when viewing non-rich content', async () => {
@@ -57,7 +57,7 @@ describe('Markdown table of contents component', () => {
await setLoaded(true);
- expect(findDropdownItem().exists()).toBe(false);
+ expect(findDropdown().exists()).toBe(false);
});
});
@@ -67,15 +67,25 @@ describe('Markdown table of contents component', () => {
await setLoaded(true);
- const dropdownItems = wrapper.findAllComponents(GlDropdownItem);
+ const dropdown = findDropdown();
- expect(dropdownItems.exists()).toBe(true);
- expect(dropdownItems.length).toBe(4);
+ expect(dropdown.exists()).toBe(true);
+ expect(dropdown.props('items').length).toBe(4);
// make sure that this only happens once
await setLoaded(true);
- expect(wrapper.findAllComponents(GlDropdownItem).length).toBe(4);
+ expect(dropdown.props('items').length).toBe(4);
+ });
+
+ it('generates proper anchor links', async () => {
+ createComponent();
+ await setLoaded(true);
+
+ const dropdown = findDropdown();
+ const items = dropdown.props('items');
+ const hrefs = items.map((item) => item.href);
+ expect(hrefs).toEqual(['#hello', '#world', '#hakuna', '#matata']);
});
it('sets padding for dropdown items', async () => {
@@ -83,12 +93,12 @@ describe('Markdown table of contents component', () => {
await setLoaded(true);
- const dropdownLinks = wrapper.findAll('[data-testid="tableContentsLink"]');
+ const items = findDropdown().props('items');
- expect(dropdownLinks.at(0).element.style.paddingLeft).toBe('0px');
- expect(dropdownLinks.at(1).element.style.paddingLeft).toBe('8px');
- expect(dropdownLinks.at(2).element.style.paddingLeft).toBe('16px');
- expect(dropdownLinks.at(3).element.style.paddingLeft).toBe('8px');
+ expect(items[0].extraAttrs.style.paddingLeft).toBe('16px');
+ expect(items[1].extraAttrs.style.paddingLeft).toBe('24px');
+ expect(items[2].extraAttrs.style.paddingLeft).toBe('32px');
+ expect(items[3].extraAttrs.style.paddingLeft).toBe('24px');
});
});
});
diff --git a/spec/frontend/blob/notebook/notebook_viever_spec.js b/spec/frontend/blob/notebook/notebook_viever_spec.js
index ea4badc03fb..2e7eadc912d 100644
--- a/spec/frontend/blob/notebook/notebook_viever_spec.js
+++ b/spec/frontend/blob/notebook/notebook_viever_spec.js
@@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import component from '~/blob/notebook/notebook_viewer.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import NotebookLab from '~/notebook/index.vue';
describe('iPython notebook renderer', () => {
@@ -54,7 +55,7 @@ describe('iPython notebook renderer', () => {
describe('successful response', () => {
beforeEach(() => {
- mock.onGet(endpoint).reply(200, mockNotebook);
+ mock.onGet(endpoint).reply(HTTP_STATUS_OK, mockNotebook);
mountComponent();
return waitForPromises();
});
@@ -72,7 +73,7 @@ describe('iPython notebook renderer', () => {
beforeEach(() => {
mock.onGet(endpoint).reply(() =>
// eslint-disable-next-line prefer-promise-reject-errors
- Promise.reject({ status: 200 }),
+ Promise.reject({ status: HTTP_STATUS_OK }),
);
mountComponent();
@@ -90,7 +91,7 @@ describe('iPython notebook renderer', () => {
describe('error getting file', () => {
beforeEach(() => {
- mock.onGet(endpoint).reply(500, '');
+ mock.onGet(endpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, '');
mountComponent();
return waitForPromises();
diff --git a/spec/frontend/blob/openapi/index_spec.js b/spec/frontend/blob/openapi/index_spec.js
index d9d65258516..95e86398ab8 100644
--- a/spec/frontend/blob/openapi/index_spec.js
+++ b/spec/frontend/blob/openapi/index_spec.js
@@ -1,7 +1,9 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
+import { TEST_HOST } from 'helpers/test_constants';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import renderOpenApi from '~/blob/openapi';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('OpenAPI blob viewer', () => {
const id = 'js-openapi-viewer';
@@ -10,7 +12,7 @@ describe('OpenAPI blob viewer', () => {
beforeEach(async () => {
setHTMLFixture(`<div id="${id}" data-endpoint="${mockEndpoint}"></div>`);
- mock = new MockAdapter(axios).onGet().reply(200);
+ mock = new MockAdapter(axios).onGet().reply(HTTP_STATUS_OK);
await renderOpenApi();
});
@@ -21,7 +23,7 @@ describe('OpenAPI blob viewer', () => {
it('initializes SwaggerUI with the correct configuration', () => {
expect(document.body.innerHTML).toContain(
- '<iframe src="/-/sandbox/swagger" sandbox="allow-scripts allow-popups allow-forms" frameborder="0" width="100%" height="1000"></iframe>',
+ `<iframe src="${TEST_HOST}/-/sandbox/swagger" sandbox="allow-scripts allow-popups allow-forms" frameborder="0" width="100%" height="1000"></iframe>`,
);
});
});
diff --git a/spec/frontend/boards/board_card_inner_spec.js b/spec/frontend/boards/board_card_inner_spec.js
index 2c8e6306431..1e823e3321a 100644
--- a/spec/frontend/boards/board_card_inner_spec.js
+++ b/spec/frontend/boards/board_card_inner_spec.js
@@ -1,16 +1,18 @@
import { GlLabel, GlLoadingIcon, GlTooltip } from '@gitlab/ui';
import { range } from 'lodash';
import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
+import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import BoardCardInner from '~/boards/components/board_card_inner.vue';
import WorkItemTypeIcon from '~/work_items/components/work_item_type_icon.vue';
-import { issuableTypes } from '~/boards/constants';
import eventHub from '~/boards/eventhub';
import defaultStore from '~/boards/stores';
+import { TYPE_ISSUE } from '~/issues/constants';
import { updateHistory } from '~/lib/utils/url_utility';
import { mockLabelList, mockIssue, mockIssueFullPath } from './mock_data';
@@ -18,6 +20,7 @@ jest.mock('~/lib/utils/url_utility');
jest.mock('~/boards/eventhub');
Vue.use(Vuex);
+Vue.use(VueApollo);
describe('Board card component', () => {
const user = {
@@ -69,6 +72,7 @@ describe('Board card component', () => {
const createWrapper = ({ props = {}, isEpicBoard = false, isGroupBoard = true } = {}) => {
wrapper = mountExtended(BoardCardInner, {
store,
+ apolloProvider: createMockApollo(),
propsData: {
list,
item: issue,
@@ -82,18 +86,11 @@ describe('Board card component', () => {
directives: {
GlTooltip: createMockDirective(),
},
- mocks: {
- $apollo: {
- queries: {
- blockingIssuables: { loading: false },
- },
- },
- },
provide: {
rootPath: '/',
scopedLabelsAvailable: false,
isEpicBoard,
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
isGroupBoard,
},
});
diff --git a/spec/frontend/boards/board_list_helper.js b/spec/frontend/boards/board_list_helper.js
index 1ba546f24a8..d882ff071b7 100644
--- a/spec/frontend/boards/board_list_helper.js
+++ b/spec/frontend/boards/board_list_helper.js
@@ -22,6 +22,7 @@ export default function createComponent({
listIssueProps = {},
componentProps = {},
listProps = {},
+ apolloQueryHandlers = [],
actions = {},
getters = {},
provide = {},
@@ -39,6 +40,7 @@ export default function createComponent({
const fakeApollo = createMockApollo([
[listQuery, jest.fn().mockResolvedValue(boardListQueryResponse(issuesCount))],
+ ...apolloQueryHandlers,
]);
const store = new Vuex.Store({
@@ -89,6 +91,7 @@ export default function createComponent({
list,
boardItems: [issue],
canAdminList: true,
+ boardId: 'gid://gitlab/Board/1',
...componentProps,
},
provide: {
@@ -104,6 +107,9 @@ export default function createComponent({
isGroupBoard: false,
isProjectBoard: true,
disabled: false,
+ boardType: 'group',
+ issuableType: 'issue',
+ isApolloBoard: false,
...provide,
},
stubs,
diff --git a/spec/frontend/boards/board_list_spec.js b/spec/frontend/boards/board_list_spec.js
index abe8c230bd8..fc8dbf8dc3a 100644
--- a/spec/frontend/boards/board_list_spec.js
+++ b/spec/frontend/boards/board_list_spec.js
@@ -1,6 +1,6 @@
import Draggable from 'vuedraggable';
import { nextTick } from 'vue';
-import { DraggableItemTypes } from 'ee_else_ce/boards/constants';
+import { DraggableItemTypes, ListType } from 'ee_else_ce/boards/constants';
import { useFakeRequestAnimationFrame } from 'helpers/fake_request_animation_frame';
import waitForPromises from 'helpers/wait_for_promises';
import createComponent from 'jest/boards/board_list_helper';
@@ -107,6 +107,20 @@ describe('Board list component', () => {
});
});
+ describe('when ListType is Closed', () => {
+ beforeEach(() => {
+ wrapper = createComponent({
+ listProps: {
+ listType: ListType.closed,
+ },
+ });
+ });
+
+ it('Board card move to position is not visible', () => {
+ expect(findMoveToPositionComponent().exists()).toBe(false);
+ });
+ });
+
describe('load more issues', () => {
const actions = {
fetchItemsForList: jest.fn(),
@@ -159,27 +173,32 @@ describe('Board list component', () => {
});
describe('when issue count exceeds max issue count', () => {
- it('sets background to bg-danger-100', async () => {
+ it('sets background to gl-bg-red-100', async () => {
wrapper.setProps({ list: { issuesCount: 4, maxIssueCount: 3 } });
await nextTick();
- expect(wrapper.find('.bg-danger-100').exists()).toBe(true);
+ const block = wrapper.find('.gl-bg-red-100');
+
+ expect(block.exists()).toBe(true);
+ expect(block.attributes('class')).toContain(
+ 'gl-rounded-bottom-left-base gl-rounded-bottom-right-base',
+ );
});
});
describe('when list issue count does NOT exceed list max issue count', () => {
- it('does not sets background to bg-danger-100', () => {
+ it('does not sets background to gl-bg-red-100', () => {
wrapper.setProps({ list: { issuesCount: 2, maxIssueCount: 3 } });
- expect(wrapper.find('.bg-danger-100').exists()).toBe(false);
+ expect(wrapper.find('.gl-bg-red-100').exists()).toBe(false);
});
});
describe('when list max issue count is 0', () => {
- it('does not sets background to bg-danger-100', () => {
+ it('does not sets background to gl-bg-red-100', () => {
wrapper.setProps({ list: { maxIssueCount: 0 } });
- expect(wrapper.find('.bg-danger-100').exists()).toBe(false);
+ expect(wrapper.find('.gl-bg-red-100').exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/boards/components/board_app_spec.js b/spec/frontend/boards/components/board_app_spec.js
index 872a67a71fb..12318fb5d16 100644
--- a/spec/frontend/boards/components/board_app_spec.js
+++ b/spec/frontend/boards/components/board_app_spec.js
@@ -27,7 +27,7 @@ describe('BoardApp', () => {
wrapper = shallowMount(BoardApp, {
store,
provide: {
- fullBoardId: 'gid://gitlab/Board/1',
+ initialBoardId: 'gid://gitlab/Board/1',
},
});
};
diff --git a/spec/frontend/boards/components/board_card_move_to_position_spec.js b/spec/frontend/boards/components/board_card_move_to_position_spec.js
index 8dee3c77787..8af772ba6d0 100644
--- a/spec/frontend/boards/components/board_card_move_to_position_spec.js
+++ b/spec/frontend/boards/components/board_card_move_to_position_spec.js
@@ -1,8 +1,11 @@
import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import Vue from 'vue';
import Vuex from 'vuex';
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-
+import { GlDisclosureDropdown, GlDisclosureDropdownItem } from '@gitlab/ui';
+import {
+ BOARD_CARD_MOVE_TO_POSITIONS_START_OPTION,
+ BOARD_CARD_MOVE_TO_POSITIONS_END_OPTION,
+} from '~/boards/constants';
import BoardCardMoveToPosition from '~/boards/components/board_card_move_to_position.vue';
import { mockList, mockIssue2, mockIssue, mockIssue3, mockIssue4 } from 'jest/boards/mock_data';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
@@ -10,8 +13,14 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
Vue.use(Vuex);
const dropdownOptions = [
- BoardCardMoveToPosition.i18n.moveToStartText,
- BoardCardMoveToPosition.i18n.moveToEndText,
+ {
+ text: BOARD_CARD_MOVE_TO_POSITIONS_START_OPTION,
+ action: jest.fn(),
+ },
+ {
+ text: BOARD_CARD_MOVE_TO_POSITIONS_END_OPTION,
+ action: jest.fn(),
+ },
];
describe('Board Card Move to position', () => {
@@ -53,8 +62,8 @@ describe('Board Card Move to position', () => {
...propsData,
},
stubs: {
- GlDropdown,
- GlDropdownItem,
+ GlDisclosureDropdown,
+ GlDisclosureDropdownItem,
},
});
};
@@ -64,12 +73,9 @@ describe('Board Card Move to position', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
- const findMoveToPositionDropdown = () => wrapper.findComponent(GlDropdown);
- const findDropdownItems = () => findMoveToPositionDropdown().findAllComponents(GlDropdownItem);
+ const findMoveToPositionDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findDropdownItems = () =>
+ findMoveToPositionDropdown().findAllComponents(GlDisclosureDropdownItem);
const findDropdownItemAtIndex = (index) => findDropdownItems().at(index);
describe('Dropdown', () => {
@@ -80,7 +86,7 @@ describe('Board Card Move to position', () => {
});
it('is opened on the click of vertical ellipsis and has 2 dropdown items when number of list items < 10', () => {
- findMoveToPositionDropdown().vm.$emit('click');
+ findMoveToPositionDropdown().vm.$emit('shown');
expect(findDropdownItems()).toHaveLength(dropdownOptions.length);
});
});
@@ -97,26 +103,24 @@ describe('Board Card Move to position', () => {
});
it.each`
- dropdownIndex | dropdownLabel | trackLabel | positionInList
- ${0} | ${BoardCardMoveToPosition.i18n.moveToStartText} | ${'move_to_start'} | ${0}
- ${1} | ${BoardCardMoveToPosition.i18n.moveToEndText} | ${'move_to_end'} | ${-1}
+ dropdownIndex | dropdownItem | trackLabel | positionInList
+ ${0} | ${dropdownOptions[0]} | ${'move_to_start'} | ${0}
+ ${1} | ${dropdownOptions[1]} | ${'move_to_end'} | ${-1}
`(
'on click of dropdown index $dropdownIndex with label $dropdownLabel should call moveItem action with tracking label $trackLabel',
- async ({ dropdownIndex, dropdownLabel, trackLabel, positionInList }) => {
- await findMoveToPositionDropdown().vm.$emit('click');
+ async ({ dropdownIndex, dropdownItem, trackLabel, positionInList }) => {
+ await findMoveToPositionDropdown().vm.$emit('shown');
- expect(findDropdownItemAtIndex(dropdownIndex).text()).toBe(dropdownLabel);
- await findDropdownItemAtIndex(dropdownIndex).vm.$emit('click', {
- stopPropagation: () => {},
- });
+ expect(findDropdownItemAtIndex(dropdownIndex).text()).toBe(dropdownItem.text);
- await nextTick();
+ await findMoveToPositionDropdown().vm.$emit('action', dropdownItem);
expect(trackingSpy).toHaveBeenCalledWith('boards:list', 'click_toggle_button', {
category: 'boards:list',
label: trackLabel,
property: 'type_card',
});
+
expect(dispatch).toHaveBeenCalledWith('moveItem', {
fromListId: mockList.id,
itemId: mockIssue2.id,
diff --git a/spec/frontend/boards/components/board_card_spec.js b/spec/frontend/boards/components/board_card_spec.js
index f8ad7c468c1..84e6318d98e 100644
--- a/spec/frontend/boards/components/board_card_spec.js
+++ b/spec/frontend/boards/components/board_card_spec.js
@@ -61,6 +61,7 @@ describe('Board card', () => {
isProjectBoard: false,
isGroupBoard: true,
disabled: false,
+ isApolloBoard: false,
...provide,
},
});
diff --git a/spec/frontend/boards/components/board_column_spec.js b/spec/frontend/boards/components/board_column_spec.js
index d34e228a2d7..c0bb51620f2 100644
--- a/spec/frontend/boards/components/board_column_spec.js
+++ b/spec/frontend/boards/components/board_column_spec.js
@@ -35,6 +35,10 @@ describe('Board Column Component', () => {
store,
propsData: {
list: listMock,
+ boardId: 'gid://gitlab/Board/1',
+ },
+ provide: {
+ isApolloBoard: false,
},
});
};
diff --git a/spec/frontend/boards/components/board_content_sidebar_spec.js b/spec/frontend/boards/components/board_content_sidebar_spec.js
index 51c42b48535..955267a415c 100644
--- a/spec/frontend/boards/components/board_content_sidebar_spec.js
+++ b/spec/frontend/boards/components/board_content_sidebar_spec.js
@@ -7,9 +7,10 @@ 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, issuableTypes } from '~/boards/constants';
+import { ISSUABLE } from '~/boards/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import SidebarDateWidget from '~/sidebar/components/date/sidebar_date_widget.vue';
-import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
+import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severity_widget.vue';
import SidebarSubscriptionsWidget from '~/sidebar/components/subscriptions/sidebar_subscriptions_widget.vue';
import SidebarTodoWidget from '~/sidebar/components/todo_toggle/sidebar_todo_widget.vue';
import SidebarLabelsWidget from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
@@ -53,7 +54,7 @@ describe('BoardContentSidebar', () => {
canUpdate: true,
rootPath: '/',
groupId: 1,
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
isGroupBoard: false,
},
store,
@@ -142,8 +143,8 @@ describe('BoardContentSidebar', () => {
);
});
- it('does not render SidebarSeverity', () => {
- expect(wrapper.findComponent(SidebarSeverity).exists()).toBe(false);
+ it('does not render SidebarSeverityWidget', () => {
+ expect(wrapper.findComponent(SidebarSeverityWidget).exists()).toBe(false);
});
it('does not render SidebarHealthStatusWidget', async () => {
@@ -188,8 +189,8 @@ describe('BoardContentSidebar', () => {
createComponent();
});
- it('renders SidebarSeverity', () => {
- expect(wrapper.findComponent(SidebarSeverity).exists()).toBe(true);
+ it('renders SidebarSeverityWidget', () => {
+ expect(wrapper.findComponent(SidebarSeverityWidget).exists()).toBe(true);
});
});
});
diff --git a/spec/frontend/boards/components/board_list_header_spec.js b/spec/frontend/boards/components/board_list_header_spec.js
index a16b99728c3..9e65e900440 100644
--- a/spec/frontend/boards/components/board_list_header_spec.js
+++ b/spec/frontend/boards/components/board_list_header_spec.js
@@ -35,6 +35,7 @@ describe('Board List Header Component', () => {
withLocalStorage = true,
currentUserId = 1,
listQueryHandler = jest.fn().mockResolvedValue(boardListQueryResponse()),
+ injectedProps = {},
} = {}) => {
const boardId = '1';
@@ -76,6 +77,7 @@ describe('Board List Header Component', () => {
currentUserId,
isEpicBoard: false,
disabled: false,
+ ...injectedProps,
},
}),
);
@@ -86,6 +88,7 @@ describe('Board List Header Component', () => {
const findAddIssueButton = () => wrapper.findComponent({ ref: 'newIssueBtn' });
const findTitle = () => wrapper.find('.board-title');
const findCaret = () => wrapper.findByTestId('board-title-caret');
+ const findSettingsButton = () => wrapper.findComponent({ ref: 'settingsBtn' });
describe('Add issue button', () => {
const hasNoAddButton = [ListType.closed];
@@ -126,13 +129,40 @@ describe('Board List Header Component', () => {
});
});
+ describe('Settings Button', () => {
+ describe('with disabled=true', () => {
+ const hasSettings = [
+ ListType.assignee,
+ ListType.milestone,
+ ListType.iteration,
+ ListType.label,
+ ];
+ const hasNoSettings = [ListType.backlog, ListType.closed];
+
+ it.each(hasSettings)('does render for List Type `%s` when disabled=true', (listType) => {
+ createComponent({ listType, injectedProps: { disabled: true } });
+
+ expect(findSettingsButton().exists()).toBe(true);
+ });
+
+ it.each(hasNoSettings)(
+ 'does not render for List Type `%s` when disabled=true',
+ (listType) => {
+ createComponent({ listType });
+
+ expect(findSettingsButton().exists()).toBe(false);
+ },
+ );
+ });
+ });
+
describe('expanding / collapsing the column', () => {
it('should display collapse icon when column is expanded', async () => {
createComponent();
const icon = findCaret();
- expect(icon.props('icon')).toBe('chevron-down');
+ expect(icon.props('icon')).toBe('chevron-lg-down');
});
it('should display expand icon when column is collapsed', async () => {
@@ -140,7 +170,7 @@ describe('Board List Header Component', () => {
const icon = findCaret();
- expect(icon.props('icon')).toBe('chevron-right');
+ expect(icon.props('icon')).toBe('chevron-lg-right');
});
it('should dispatch toggleListCollapse when clicking the collapse icon', async () => {
diff --git a/spec/frontend/boards/components/board_top_bar_spec.js b/spec/frontend/boards/components/board_top_bar_spec.js
index af492145eb0..8258d9fe7f4 100644
--- a/spec/frontend/boards/components/board_top_bar_spec.js
+++ b/spec/frontend/boards/components/board_top_bar_spec.js
@@ -1,6 +1,8 @@
import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
import Vuex from 'vuex';
+import createMockApollo from 'helpers/mock_apollo_helper';
import BoardTopBar from '~/boards/components/board_top_bar.vue';
import BoardAddNewColumnTrigger from '~/boards/components/board_add_new_column_trigger.vue';
@@ -9,11 +11,18 @@ import ConfigToggle from '~/boards/components/config_toggle.vue';
import IssueBoardFilteredSearch from '~/boards/components/issue_board_filtered_search.vue';
import NewBoardButton from '~/boards/components/new_board_button.vue';
import ToggleFocus from '~/boards/components/toggle_focus.vue';
+import { BoardType } from '~/boards/constants';
+
+import groupBoardQuery from '~/boards/graphql/group_board.query.graphql';
+import projectBoardQuery from '~/boards/graphql/project_board.query.graphql';
+import { mockProjectBoardResponse, mockGroupBoardResponse } from '../mock_data';
+
+Vue.use(VueApollo);
+Vue.use(Vuex);
describe('BoardTopBar', () => {
let wrapper;
-
- Vue.use(Vuex);
+ let mockApollo;
const createStore = () => {
return new Vuex.Store({
@@ -21,10 +30,22 @@ describe('BoardTopBar', () => {
});
};
+ const projectBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockProjectBoardResponse);
+ const groupBoardQueryHandlerSuccess = jest.fn().mockResolvedValue(mockGroupBoardResponse);
+
const createComponent = ({ provide = {} } = {}) => {
const store = createStore();
+ mockApollo = createMockApollo([
+ [projectBoardQuery, projectBoardQueryHandlerSuccess],
+ [groupBoardQuery, groupBoardQueryHandlerSuccess],
+ ]);
+
wrapper = shallowMount(BoardTopBar, {
store,
+ apolloProvider: mockApollo,
+ props: {
+ boardId: 'gid://gitlab/Board/1',
+ },
provide: {
swimlanesFeatureAvailable: false,
canAdminList: false,
@@ -33,7 +54,9 @@ describe('BoardTopBar', () => {
boardType: 'group',
releasesFetchPath: '/releases',
isIssueBoard: true,
+ isEpicBoard: false,
isGroupBoard: true,
+ isApolloBoard: false,
...provide,
},
stubs: { IssueBoardFilteredSearch },
@@ -42,6 +65,7 @@ describe('BoardTopBar', () => {
afterEach(() => {
wrapper.destroy();
+ mockApollo = null;
});
describe('base template', () => {
@@ -83,4 +107,26 @@ describe('BoardTopBar', () => {
expect(wrapper.findComponent(BoardAddNewColumnTrigger).exists()).toBe(true);
});
});
+
+ describe('Apollo boards', () => {
+ it.each`
+ boardType | queryHandler | notCalledHandler
+ ${BoardType.group} | ${groupBoardQueryHandlerSuccess} | ${projectBoardQueryHandlerSuccess}
+ ${BoardType.project} | ${projectBoardQueryHandlerSuccess} | ${groupBoardQueryHandlerSuccess}
+ `('fetches $boardType boards', async ({ boardType, queryHandler, notCalledHandler }) => {
+ createComponent({
+ provide: {
+ boardType,
+ isProjectBoard: boardType === BoardType.project,
+ isGroupBoard: boardType === BoardType.group,
+ isApolloBoard: true,
+ },
+ });
+
+ await nextTick();
+
+ expect(queryHandler).toHaveBeenCalled();
+ expect(notCalledHandler).not.toHaveBeenCalled();
+ });
+ });
});
diff --git a/spec/frontend/boards/components/boards_selector_spec.js b/spec/frontend/boards/components/boards_selector_spec.js
index 7b61ca5e6fd..28f51e0ecbf 100644
--- a/spec/frontend/boards/components/boards_selector_spec.js
+++ b/spec/frontend/boards/components/boards_selector_spec.js
@@ -82,6 +82,7 @@ describe('BoardsSelector', () => {
projectRecentBoardsQueryHandler = projectRecentBoardsQueryHandlerSuccess,
isGroupBoard = false,
isProjectBoard = false,
+ provide = {},
} = {}) => {
fakeApollo = createMockApollo([
[projectBoardsQuery, projectBoardsQueryHandler],
@@ -108,6 +109,8 @@ describe('BoardsSelector', () => {
boardType: isGroupBoard ? 'group' : 'project',
isGroupBoard,
isProjectBoard,
+ isApolloBoard: false,
+ ...provide,
},
});
};
@@ -245,4 +248,34 @@ describe('BoardsSelector', () => {
expect(notCalledHandler).not.toHaveBeenCalled();
});
});
+
+ describe('dropdown visibility', () => {
+ describe('when multipleIssueBoardsAvailable is enabled', () => {
+ it('show dropdown', async () => {
+ createStore();
+ createComponent({ provide: { multipleIssueBoardsAvailable: true } });
+ expect(findDropdown().exists()).toBe(true);
+ });
+ });
+
+ describe('when multipleIssueBoardsAvailable is disabled but it hasMissingBoards', () => {
+ it('show dropdown', async () => {
+ createStore();
+ createComponent({
+ provide: { multipleIssueBoardsAvailable: false, hasMissingBoards: true },
+ });
+ expect(findDropdown().exists()).toBe(true);
+ });
+ });
+
+ describe("when multipleIssueBoardsAvailable is disabled and it dosn't hasMissingBoards", () => {
+ it('hide dropdown', async () => {
+ createStore();
+ createComponent({
+ provide: { multipleIssueBoardsAvailable: false, hasMissingBoards: false },
+ });
+ expect(findDropdown().exists()).toBe(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
index cc1e5de15c1..bc66a0515aa 100644
--- a/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
+++ b/spec/frontend/boards/components/sidebar/board_sidebar_title_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlFormInput, GlForm } from '@gitlab/ui';
+import { GlAlert, GlFormInput, GlForm, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import BoardEditableItem from '~/boards/components/sidebar/board_editable_item.vue';
@@ -11,12 +11,14 @@ const TEST_ISSUE_A = {
iid: 8,
title: 'Issue 1',
referencePath: 'h/b#1',
+ webUrl: 'webUrl',
};
const TEST_ISSUE_B = {
id: 'gid://gitlab/Issue/2',
iid: 9,
title: 'Issue 2',
referencePath: 'h/b#2',
+ webUrl: 'webUrl',
};
describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
@@ -49,6 +51,7 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
const findForm = () => wrapper.findComponent(GlForm);
const findAlert = () => wrapper.findComponent(GlAlert);
const findFormInput = () => wrapper.findComponent(GlFormInput);
+ const findGlLink = () => wrapper.findComponent(GlLink);
const findEditableItem = () => wrapper.findComponent(BoardEditableItem);
const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
const findTitle = () => wrapper.find('[data-testid="item-title"]');
@@ -67,6 +70,12 @@ describe('~/boards/components/sidebar/board_sidebar_title.vue', () => {
expect(findAlert().exists()).toBe(false);
});
+ it('links title to the corresponding issue', () => {
+ createWrapper();
+
+ expect(findGlLink().attributes('href')).toBe('webUrl');
+ });
+
describe('when new title is submitted', () => {
beforeEach(async () => {
createWrapper();
diff --git a/spec/frontend/boards/mock_data.js b/spec/frontend/boards/mock_data.js
index df41eb05eae..1d011eacf1c 100644
--- a/spec/frontend/boards/mock_data.js
+++ b/spec/frontend/boards/mock_data.js
@@ -1,6 +1,7 @@
import { GlFilteredSearchToken } from '@gitlab/ui';
import { keyBy } from 'lodash';
import { ListType } from '~/boards/constants';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
OPERATORS_IS,
OPERATORS_IS_NOT,
@@ -50,6 +51,26 @@ export const mockBoard = {
weight: 2,
};
+export const mockProjectBoardResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/114',
+ board: mockBoard,
+ __typename: 'Project',
+ },
+ },
+};
+
+export const mockGroupBoardResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Group/114',
+ board: mockBoard,
+ __typename: 'Group',
+ },
+ },
+};
+
export const mockBoardConfig = {
milestoneId: 'gid://gitlab/Milestone/114',
milestoneTitle: '14.9',
@@ -440,7 +461,7 @@ export const BoardsMockData = {
export const boardsMockInterceptor = (config) => {
const body = BoardsMockData[config.method.toUpperCase()][config.url];
- return [200, body];
+ return [HTTP_STATUS_OK, body];
};
export const mockList = {
diff --git a/spec/frontend/boards/stores/actions_spec.js b/spec/frontend/boards/stores/actions_spec.js
index b3e90e34161..ab959abaa99 100644
--- a/spec/frontend/boards/stores/actions_spec.js
+++ b/spec/frontend/boards/stores/actions_spec.js
@@ -6,7 +6,6 @@ import {
inactiveId,
ISSUABLE,
ListType,
- issuableTypes,
BoardType,
DraggableItemTypes,
} from 'ee_else_ce/boards/constants';
@@ -27,6 +26,7 @@ import actions from '~/boards/stores/actions';
import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import { TYPE_ISSUE } from '~/issues/constants';
import projectBoardMilestones from '~/boards/graphql/project_board_milestones.query.graphql';
import groupBoardMilestones from '~/boards/graphql/group_board_milestones.query.graphql';
@@ -167,7 +167,7 @@ describe('setFilters', () => {
])('should commit mutation SET_FILTERS %s', (_, { filters, filterVariables }) => {
const state = {
filters: {},
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
};
testAction(
@@ -299,9 +299,9 @@ describe('fetchLists', () => {
});
it.each`
- issuableType | boardType | fullBoardId | isGroup | isProject
- ${issuableTypes.issue} | ${BoardType.group} | ${'gid://gitlab/Board/1'} | ${true} | ${false}
- ${issuableTypes.issue} | ${BoardType.project} | ${'gid://gitlab/Board/1'} | ${false} | ${true}
+ issuableType | boardType | fullBoardId | isGroup | isProject
+ ${TYPE_ISSUE} | ${BoardType.group} | ${'gid://gitlab/Board/1'} | ${true} | ${false}
+ ${TYPE_ISSUE} | ${BoardType.project} | ${'gid://gitlab/Board/1'} | ${false} | ${true}
`(
'calls $issuableType query with correct variables',
async ({ issuableType, boardType, fullBoardId, isGroup, isProject }) => {
@@ -719,7 +719,7 @@ describe('updateList', () => {
boardType: 'group',
disabled: false,
boardLists: [{ type: 'closed' }],
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
boardItemsByListId,
});
@@ -835,7 +835,7 @@ describe('removeList', () => {
beforeEach(() => {
state = {
boardLists: mockListsById,
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
};
getters = {
getListByTitle: jest.fn().mockReturnValue(mockList),
@@ -1747,7 +1747,7 @@ describe('setActiveItemSubscribed', () => {
[mockActiveIssue.id]: mockActiveIssue,
},
fullPath: 'gitlab-org',
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
};
const getters = { activeBoardItem: mockActiveIssue, isEpicBoard: false };
const subscribedState = true;
@@ -1800,7 +1800,7 @@ describe('setActiveItemSubscribed', () => {
describe('setActiveItemTitle', () => {
const state = {
boardItems: { [mockIssue.id]: mockIssue },
- issuableType: issuableTypes.issue,
+ issuableType: TYPE_ISSUE,
fullPath: 'path/f',
};
const getters = { activeBoardItem: mockIssue, isEpicBoard: false };
diff --git a/spec/frontend/boards/stores/mutations_spec.js b/spec/frontend/boards/stores/mutations_spec.js
index 87a183c0441..2d68c070b83 100644
--- a/spec/frontend/boards/stores/mutations_spec.js
+++ b/spec/frontend/boards/stores/mutations_spec.js
@@ -1,8 +1,8 @@
import { cloneDeep } from 'lodash';
-import { issuableTypes } from '~/boards/constants';
import * as types from '~/boards/stores/mutation_types';
import mutations from '~/boards/stores/mutations';
import defaultState from '~/boards/stores/state';
+import { TYPE_ISSUE } from '~/issues/constants';
import {
mockBoard,
mockLists,
@@ -70,7 +70,7 @@ describe('Board Store Mutations', () => {
const fullPath = 'gitlab-org';
const boardType = 'group';
const disabled = false;
- const issuableType = issuableTypes.issue;
+ const issuableType = TYPE_ISSUE;
mutations[types.SET_INITIAL_BOARD_DATA](state, {
allowSubEpics,
diff --git a/spec/frontend/branches/components/sort_dropdown_spec.js b/spec/frontend/branches/components/sort_dropdown_spec.js
index 16ed02bfa88..bd41b0daaaa 100644
--- a/spec/frontend/branches/components/sort_dropdown_spec.js
+++ b/spec/frontend/branches/components/sort_dropdown_spec.js
@@ -18,6 +18,8 @@ describe('Branches Sort Dropdown', () => {
updated_asc: 'Oldest updated',
updated_desc: 'Last updated',
},
+ showDropdown: false,
+ sortedBy: 'updated_desc',
...props,
},
}),
@@ -54,7 +56,7 @@ describe('Branches Sort Dropdown', () => {
describe('when in All branches mode', () => {
beforeEach(() => {
- wrapper = createWrapper({ mode: 'all' });
+ wrapper = createWrapper({ mode: 'all', showDropdown: true });
});
it('should have a search box with a placeholder', () => {
@@ -64,7 +66,7 @@ describe('Branches Sort Dropdown', () => {
expect(searchBox.find('input').attributes('placeholder')).toBe('Filter by branch name');
});
- it('should have a branches dropdown when in all branches mode', () => {
+ it('should have a branches dropdown', () => {
const branchesDropdown = findBranchesDropdown();
expect(branchesDropdown.exists()).toBe(true);
@@ -84,7 +86,7 @@ describe('Branches Sort Dropdown', () => {
searchBox.vm.$emit('submit');
expect(urlUtils.visitUrl).toHaveBeenCalledWith(
- '/root/ci-cd-project-demo/-/branches?state=all',
+ '/root/ci-cd-project-demo/-/branches?state=all&sort=updated_desc',
);
});
});
diff --git a/spec/frontend/branches/divergence_graph_spec.js b/spec/frontend/branches/divergence_graph_spec.js
index 7c367f83add..c3a0f6436c5 100644
--- a/spec/frontend/branches/divergence_graph_spec.js
+++ b/spec/frontend/branches/divergence_graph_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import init from '~/branches/divergence_graph';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Divergence graph', () => {
let mock;
@@ -8,7 +9,7 @@ describe('Divergence graph', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet('/-/diverging_counts').reply(200, {
+ mock.onGet('/-/diverging_counts').reply(HTTP_STATUS_OK, {
main: { ahead: 1, behind: 1 },
'test/hello-world': { ahead: 1, behind: 1 },
});
diff --git a/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js b/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
index 002fe7c6e71..a4eecabcf28 100644
--- a/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
+++ b/spec/frontend/captcha/captcha_modal_axios_interceptor_spec.js
@@ -34,8 +34,8 @@ describe('registerCaptchaModalInterceptor', () => {
waitForCaptchaToBeSolved.mockRejectedValue(new UnsolvedCaptchaError());
mock = new MockAdapter(axios);
- mock.onAny('/endpoint-without-captcha').reply(200, AXIOS_RESPONSE);
- mock.onAny('/endpoint-with-unrelated-error').reply(404, AXIOS_RESPONSE);
+ mock.onAny('/endpoint-without-captcha').reply(HTTP_STATUS_OK, AXIOS_RESPONSE);
+ mock.onAny('/endpoint-with-unrelated-error').reply(HTTP_STATUS_NOT_FOUND, AXIOS_RESPONSE);
mock.onAny('/endpoint-with-captcha').reply((config) => {
if (!supportedMethods.includes(config.method)) {
return [HTTP_STATUS_METHOD_NOT_ALLOWED, { method: config.method }];
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
index 3f1eebbc6a5..c0fb133b9b1 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_group_variables_spec.js
@@ -1,11 +1,10 @@
import { shallowMount } from '@vue/test-utils';
+import { TYPENAME_GROUP } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
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/ci_variable_list/constants';
-
const mockProvide = {
glFeatures: {
groupScopedCiVariables: false,
@@ -36,7 +35,7 @@ describe('Ci Group Variable wrapper', () => {
it('are passed down the correctly to ci_variable_shared', () => {
expect(findCiShared().props()).toEqual({
- id: convertToGraphQLId(GRAPHQL_GROUP_TYPE, mockProvide.groupId),
+ id: convertToGraphQLId(TYPENAME_GROUP, mockProvide.groupId),
areScopedVariablesAvailable: false,
componentName: 'GroupVariables',
entity: 'group',
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
index 7230017c560..bd1e6b17d6b 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_project_variables_spec.js
@@ -1,11 +1,10 @@
import { shallowMount } from '@vue/test-utils';
+import { TYPENAME_PROJECT } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
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/ci_variable_list/constants';
-
const mockProvide = {
projectFullPath: '/namespace/project',
projectId: 1,
@@ -32,7 +31,7 @@ describe('Ci Project Variable wrapper', () => {
it('Passes down the correct props to ci_variable_shared', () => {
expect(findCiShared().props()).toEqual({
- id: convertToGraphQLId(GRAPHQL_PROJECT_TYPE, mockProvide.projectId),
+ id: convertToGraphQLId(TYPENAME_PROJECT, mockProvide.projectId),
areScopedVariablesAvailable: true,
componentName: 'ProjectVariables',
entity: 'project',
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
index 7838e4884d8..508af964ca3 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_modal_spec.js
@@ -21,6 +21,8 @@ describe('Ci variable modal', () => {
let trackingSpy;
const maskableRegex = '^[a-zA-Z0-9_+=/@:.~-]{8,}$';
+ const maskableRawRegex = '^\\S{8,}$';
+
const mockVariables = mockVariablesWithScopes(instanceString);
const defaultProvide = {
@@ -30,10 +32,13 @@ describe('Ci variable modal', () => {
awsTipLearnLink: '/learn-link',
containsVariableReferenceLink: '/reference',
environmentScopeLink: '/help/environments',
+ glFeatures: {
+ ciRemoveCharacterLimitationRawMaskedVar: true,
+ },
isProtectedByDefault: false,
maskedEnvironmentVariablesLink: '/variables-link',
+ maskableRawRegex,
maskableRegex,
- protectedEnvironmentVariablesLink: '/protected-link',
};
const defaultProps = {
@@ -424,6 +429,36 @@ describe('Ci variable modal', () => {
describe('Validations', () => {
const maskError = 'This variable can not be masked.';
+ describe('when the variable is raw', () => {
+ const [variable] = mockVariables;
+ const validRawMaskedVariable = {
+ ...variable,
+ value: 'd$%^asdsadas',
+ masked: false,
+ raw: true,
+ };
+
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: { selectedVariable: validRawMaskedVariable },
+ });
+ });
+
+ it('should not show an error with symbols', async () => {
+ await findMaskedVariableCheckbox().trigger('click');
+
+ expect(findModal().text()).not.toContain(maskError);
+ });
+
+ it('should not show an error when length is less than 8', async () => {
+ await findValueField().vm.$emit('input', 'a');
+ await findMaskedVariableCheckbox().trigger('click');
+
+ expect(findModal().text()).toContain(maskError);
+ });
+ });
+
describe('when the mask state is invalid', () => {
beforeEach(async () => {
const [variable] = mockVariables;
diff --git a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
index 2d39bff8ce0..c977ae773db 100644
--- a/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
+++ b/spec/frontend/ci/ci_variable_list/components/ci_variable_shared_spec.js
@@ -6,6 +6,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { resolvers } from '~/ci/ci_variable_list/graphql/settings';
+import { TYPENAME_GROUP } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import ciVariableShared from '~/ci/ci_variable_list/components/ci_variable_shared.vue';
@@ -227,7 +228,7 @@ describe('Ci Variable Shared Component', () => {
variables: {
endpoint: mockProvide.endpoint,
fullPath: groupProps.fullPath,
- id: convertToGraphQLId('Group', groupProps.id),
+ id: convertToGraphQLId(TYPENAME_GROUP, groupProps.id),
variable: newVariable,
},
});
diff --git a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
index d7f0ce838d6..dc72694d26f 100644
--- a/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/editor/ci_editor_header_spec.js
@@ -11,11 +11,12 @@ describe('CI Editor Header', () => {
let wrapper;
let trackingSpy = null;
- const createComponent = ({ showDrawer = false } = {}) => {
+ const createComponent = ({ showDrawer = false, showJobAssistantDrawer = false } = {}) => {
wrapper = extendedWrapper(
shallowMount(CiEditorHeader, {
propsData: {
showDrawer,
+ showJobAssistantDrawer,
},
}),
);
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
index 6f28362e478..7bf955012c7 100644
--- a/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/header/pipeline_editor_mini_graph_spec.js
@@ -47,11 +47,6 @@ describe('Pipeline Status', () => {
mockLinkedPipelinesQuery = jest.fn();
});
- afterEach(() => {
- mockLinkedPipelinesQuery.mockReset();
- wrapper.destroy();
- });
-
describe('when there are stages', () => {
beforeEach(() => {
createComponent();
@@ -74,9 +69,11 @@ describe('Pipeline Status', () => {
describe('when querying upstream and downstream pipelines', () => {
describe('when query succeeds', () => {
- beforeEach(() => {
+ beforeEach(async () => {
mockLinkedPipelinesQuery.mockResolvedValue(mockLinkedPipelines());
createComponentWithApollo();
+
+ await waitForPromises();
});
it('should call the query with the correct variables', () => {
@@ -86,6 +83,10 @@ describe('Pipeline Status', () => {
iid: mockProjectPipeline().pipeline.iid,
});
});
+
+ it('renders only the latest downstream pipelines', () => {
+ expect(findPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
+ });
});
describe('when query fails', () => {
diff --git a/spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js b/spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
deleted file mode 100644
index 6f28362e478..00000000000
--- a/spec/frontend/ci/pipeline_editor/components/header/pipline_editor_mini_graph_spec.js
+++ /dev/null
@@ -1,109 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import PipelineEditorMiniGraph from '~/ci/pipeline_editor/components/header/pipeline_editor_mini_graph.vue';
-import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
-import getLinkedPipelinesQuery from '~/projects/commit_box/info/graphql/queries/get_linked_pipelines.query.graphql';
-import { PIPELINE_FAILURE } from '~/ci/pipeline_editor/constants';
-import { mockLinkedPipelines, mockProjectFullPath, mockProjectPipeline } from '../../mock_data';
-
-Vue.use(VueApollo);
-
-describe('Pipeline Status', () => {
- let wrapper;
- let mockApollo;
- let mockLinkedPipelinesQuery;
-
- const createComponent = ({ hasStages = true, options } = {}) => {
- wrapper = shallowMount(PipelineEditorMiniGraph, {
- provide: {
- dataMethod: 'graphql',
- projectFullPath: mockProjectFullPath,
- },
- propsData: {
- pipeline: mockProjectPipeline({ hasStages }).pipeline,
- },
- ...options,
- });
- };
-
- const createComponentWithApollo = (hasStages = true) => {
- const handlers = [[getLinkedPipelinesQuery, mockLinkedPipelinesQuery]];
- mockApollo = createMockApollo(handlers);
-
- createComponent({
- hasStages,
- options: {
- apolloProvider: mockApollo,
- },
- });
- };
-
- const findPipelineMiniGraph = () => wrapper.findComponent(PipelineMiniGraph);
-
- beforeEach(() => {
- mockLinkedPipelinesQuery = jest.fn();
- });
-
- afterEach(() => {
- mockLinkedPipelinesQuery.mockReset();
- wrapper.destroy();
- });
-
- describe('when there are stages', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('renders pipeline mini graph', () => {
- expect(findPipelineMiniGraph().exists()).toBe(true);
- });
- });
-
- describe('when there are no stages', () => {
- beforeEach(() => {
- createComponent({ hasStages: false });
- });
-
- it('does not render pipeline mini graph', () => {
- expect(findPipelineMiniGraph().exists()).toBe(false);
- });
- });
-
- describe('when querying upstream and downstream pipelines', () => {
- describe('when query succeeds', () => {
- beforeEach(() => {
- mockLinkedPipelinesQuery.mockResolvedValue(mockLinkedPipelines());
- createComponentWithApollo();
- });
-
- it('should call the query with the correct variables', () => {
- expect(mockLinkedPipelinesQuery).toHaveBeenCalledTimes(1);
- expect(mockLinkedPipelinesQuery).toHaveBeenCalledWith({
- fullPath: mockProjectFullPath,
- iid: mockProjectPipeline().pipeline.iid,
- });
- });
- });
-
- describe('when query fails', () => {
- beforeEach(async () => {
- mockLinkedPipelinesQuery.mockRejectedValue(new Error());
- createComponentWithApollo();
- await waitForPromises();
- });
-
- it('should emit an error event when query fails', async () => {
- expect(wrapper.emitted('showError')).toHaveLength(1);
- expect(wrapper.emitted('showError')[0]).toEqual([
- {
- type: PIPELINE_FAILURE,
- reasons: [wrapper.vm.$options.i18n.linkedPipelinesFetchError],
- },
- ]);
- });
- });
- });
-});
diff --git a/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
new file mode 100644
index 00000000000..79200d92598
--- /dev/null
+++ b/spec/frontend/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer_spec.js
@@ -0,0 +1,45 @@
+import { GlDrawer } from '@gitlab/ui';
+import VueApollo from 'vue-apollo';
+import Vue from 'vue';
+import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+
+Vue.use(VueApollo);
+
+describe('Job assistant drawer', () => {
+ let wrapper;
+
+ const findDrawer = () => wrapper.findComponent(GlDrawer);
+
+ const findCancelButton = () => wrapper.findByTestId('cancel-button');
+
+ const createComponent = () => {
+ wrapper = mountExtended(JobAssistantDrawer, {
+ propsData: {
+ isVisible: true,
+ },
+ });
+ };
+
+ beforeEach(async () => {
+ createComponent();
+ await waitForPromises();
+ });
+
+ it('should emit close job assistant drawer event when closing the drawer', () => {
+ expect(wrapper.emitted('close-job-assistant-drawer')).toBeUndefined();
+
+ findDrawer().vm.$emit('close');
+
+ expect(wrapper.emitted('close-job-assistant-drawer')).toHaveLength(1);
+ });
+
+ it('should emit close job assistant drawer event when click cancel button', () => {
+ expect(wrapper.emitted('close-job-assistant-drawer')).toBeUndefined();
+
+ findCancelButton().trigger('click');
+
+ expect(wrapper.emitted('close-job-assistant-drawer')).toHaveLength(1);
+ });
+});
diff --git a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
index 70310cbdb10..f40db50aab7 100644
--- a/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
+++ b/spec/frontend/ci/pipeline_editor/components/pipeline_editor_tabs_spec.js
@@ -56,6 +56,7 @@ describe('Pipeline editor tabs component', () => {
currentTab: CREATE_TAB,
isNewCiConfigFile: true,
showDrawer: false,
+ showJobAssistantDrawer: false,
...props,
},
data() {
diff --git a/spec/frontend/ci/pipeline_editor/mock_data.js b/spec/frontend/ci/pipeline_editor/mock_data.js
index 176dc24f169..541123d7efc 100644
--- a/spec/frontend/ci/pipeline_editor/mock_data.js
+++ b/spec/frontend/ci/pipeline_editor/mock_data.js
@@ -373,13 +373,64 @@ export const mockLinkedPipelines = ({ hasDownstream = true, hasUpstream = true }
{
id: 'gid://gitlab/Ci::Pipeline/612',
path: '/root/job-log-sections/-/pipelines/612',
- project: { name: 'job-log-sections', __typename: 'Project' },
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-612-612',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/532',
+ retried: false,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/611',
+ path: '/root/job-log-sections/-/pipelines/611',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
detailedStatus: {
+ id: 'success-611-611',
group: 'success',
icon: 'status_success',
label: 'passed',
__typename: 'DetailedStatus',
},
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/531',
+ retried: true,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/609',
+ path: '/root/job-log-sections/-/pipelines/609',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-609-609',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/530',
+ retried: true,
+ },
__typename: 'Pipeline',
},
],
@@ -391,8 +442,13 @@ export const mockLinkedPipelines = ({ hasDownstream = true, hasUpstream = true }
upstream = {
id: 'gid://gitlab/Ci::Pipeline/610',
path: '/root/trigger-downstream/-/pipelines/610',
- project: { name: 'trigger-downstream', __typename: 'Project' },
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'trigger-downstream',
+ __typename: 'Project',
+ },
detailedStatus: {
+ id: 'success-610-610',
group: 'success',
icon: 'status_success',
label: 'passed',
@@ -405,7 +461,9 @@ export const mockLinkedPipelines = ({ hasDownstream = true, hasUpstream = true }
return {
data: {
project: {
+ id: 'gid://gitlab/Project/21',
pipeline: {
+ id: 'gid://gitlab/Ci::Pipeline/790',
path: '/root/ci-project/-/pipelines/790',
downstream,
upstream,
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
index 2246d0bbf7e..a103acb33bc 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_app_spec.js
@@ -5,6 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { objectToQuery, redirectTo } from '~/lib/utils/url_utility';
import { resolvers } from '~/ci/pipeline_editor/graphql/resolvers';
import PipelineEditorTabs from '~/ci/pipeline_editor/components/pipeline_editor_tabs.vue';
@@ -343,7 +344,7 @@ describe('Pipeline editor app component', () => {
describe('when the lint query returns a 500 error', () => {
beforeEach(async () => {
- mockCiConfigData.mockRejectedValueOnce(new Error(500));
+ mockCiConfigData.mockRejectedValueOnce(new Error(HTTP_STATUS_INTERNAL_SERVER_ERROR));
await createComponentWithApollo({
stubs: { PipelineEditorHome, PipelineEditorHeader, ValidationSegment },
});
diff --git a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
index 621e015e825..4f8f2112abe 100644
--- a/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
+++ b/spec/frontend/ci/pipeline_editor/pipeline_editor_home_spec.js
@@ -6,6 +6,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import CiEditorHeader from '~/ci/pipeline_editor/components/editor/ci_editor_header.vue';
import CommitSection from '~/ci/pipeline_editor/components/commit/commit_section.vue';
import PipelineEditorDrawer from '~/ci/pipeline_editor/components/drawer/pipeline_editor_drawer.vue';
+import JobAssistantDrawer from '~/ci/pipeline_editor/components/job_assistant_drawer/job_assistant_drawer.vue';
import PipelineEditorFileNav from '~/ci/pipeline_editor/components/file_nav/pipeline_editor_file_nav.vue';
import PipelineEditorFileTree from '~/ci/pipeline_editor/components/file_tree/container.vue';
import BranchSwitcher from '~/ci/pipeline_editor/components/file_nav/branch_switcher.vue';
@@ -56,11 +57,13 @@ describe('Pipeline editor home wrapper', () => {
const findFileNav = () => wrapper.findComponent(PipelineEditorFileNav);
const findModal = () => wrapper.findComponent(GlModal);
const findPipelineEditorDrawer = () => wrapper.findComponent(PipelineEditorDrawer);
+ const findJobAssistantDrawer = () => wrapper.findComponent(JobAssistantDrawer);
const findPipelineEditorFileTree = () => wrapper.findComponent(PipelineEditorFileTree);
const findPipelineEditorHeader = () => wrapper.findComponent(PipelineEditorHeader);
const findPipelineEditorTabs = () => wrapper.findComponent(PipelineEditorTabs);
const findFileTreeBtn = () => wrapper.findByTestId('file-tree-toggle');
const findHelpBtn = () => wrapper.findByTestId('drawer-toggle');
+ const findJobAssistantBtn = () => wrapper.findByTestId('job-assistant-drawer-toggle');
afterEach(() => {
localStorage.clear();
@@ -261,6 +264,110 @@ describe('Pipeline editor home wrapper', () => {
});
});
+ describe('job assistant drawer', () => {
+ const clickHelpBtn = async () => {
+ findHelpBtn().vm.$emit('click');
+ await nextTick();
+ };
+ const clickJobAssistantBtn = async () => {
+ findJobAssistantBtn().vm.$emit('click');
+ await nextTick();
+ };
+
+ const stubs = {
+ CiEditorHeader,
+ GlButton,
+ GlDrawer,
+ PipelineEditorTabs,
+ JobAssistantDrawer,
+ };
+
+ it('hides the job assistant drawer by default', () => {
+ createComponent({
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(false);
+ });
+
+ it('toggles the job assistant drawer on button click', async () => {
+ createComponent({
+ stubs,
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ await clickJobAssistantBtn();
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(true);
+
+ await clickJobAssistantBtn();
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(false);
+ });
+
+ it("closes the job assistant drawer through the drawer's close button", async () => {
+ createComponent({
+ stubs,
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ await clickJobAssistantBtn();
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(true);
+
+ findJobAssistantDrawer().findComponent(GlDrawer).vm.$emit('close');
+ await nextTick();
+
+ expect(findJobAssistantDrawer().props('isVisible')).toBe(false);
+ });
+
+ it('covers helper drawer when opened last', async () => {
+ createComponent({
+ stubs: {
+ ...stubs,
+ PipelineEditorDrawer,
+ },
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ await clickHelpBtn();
+ await clickJobAssistantBtn();
+
+ const jobAssistantIndex = Number(findJobAssistantDrawer().props().zIndex);
+ const pipelineEditorDrawerIndex = Number(findPipelineEditorDrawer().props().zIndex);
+
+ expect(jobAssistantIndex).toBeGreaterThan(pipelineEditorDrawerIndex);
+ });
+
+ it('covered by helper drawer when opened first', async () => {
+ createComponent({
+ stubs: {
+ ...stubs,
+ PipelineEditorDrawer,
+ },
+ glFeatures: {
+ ciJobAssistantDrawer: true,
+ },
+ });
+
+ await clickJobAssistantBtn();
+ await clickHelpBtn();
+
+ const jobAssistantIndex = Number(findJobAssistantDrawer().props().zIndex);
+ const pipelineEditorDrawerIndex = Number(findPipelineEditorDrawer().props().zIndex);
+
+ expect(jobAssistantIndex).toBeLessThan(pipelineEditorDrawerIndex);
+ });
+ });
+
describe('file tree', () => {
const toggleFileTree = async () => {
findFileTreeBtn().vm.$emit('click');
diff --git a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
index cd16045f92d..6f18899ebac 100644
--- a/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
+++ b/spec/frontend/ci/pipeline_new/components/pipeline_new_form_spec.js
@@ -14,7 +14,9 @@ import {
HTTP_STATUS_OK,
} from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
-import PipelineNewForm from '~/ci/pipeline_new/components/pipeline_new_form.vue';
+import PipelineNewForm, {
+ POLLING_INTERVAL,
+} 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';
@@ -24,6 +26,7 @@ import {
mockCiConfigVariablesResponseWithoutDesc,
mockEmptyCiConfigVariablesResponse,
mockError,
+ mockNoCachedCiConfigVariablesResponse,
mockQueryParams,
mockPostParams,
mockProjectId,
@@ -69,6 +72,10 @@ describe('Pipeline New Form', () => {
const findCCAlert = () => wrapper.findComponent(CreditCardValidationRequiredAlert);
const getFormPostParams = () => JSON.parse(mock.history.post[0].data);
+ const advanceToNextFetch = (milliseconds) => {
+ jest.advanceTimersByTime(milliseconds);
+ };
+
const selectBranch = async (branch) => {
// Select a branch in the dropdown
findRefsDropdown().vm.$emit('input', {
@@ -266,17 +273,98 @@ describe('Pipeline New Form', () => {
});
});
- describe('when yml defines a variable', () => {
- it('loading icon is shown when content is requested and hidden when received', async () => {
- mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
- createComponentWithApollo({ props: mockQueryParams, method: mountExtended });
+ describe('When there are no variables in the API cache', () => {
+ beforeEach(async () => {
+ mockCiConfigVariables.mockResolvedValue(mockNoCachedCiConfigVariablesResponse);
+ createComponentWithApollo({ method: mountExtended });
+ await waitForPromises();
+ });
+ it('stops polling after CONFIG_VARIABLES_TIMEOUT ms have passed', async () => {
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(3);
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(3);
+ });
+
+ it('shows loading icon while query polls for updated values', async () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(1);
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
+ });
+
+ it('hides loading icon and stops polling after query fetches the updated values', async () => {
expect(findLoadingIcon().exists()).toBe(true);
+ mockCiConfigVariables.mockResolvedValue(mockCiConfigVariablesResponse);
+ advanceToNextFetch(POLLING_INTERVAL);
await waitForPromises();
expect(findLoadingIcon().exists()).toBe(false);
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(2);
});
+ });
+
+ const testBehaviorWhenCacheIsPopulated = (queryResponse) => {
+ beforeEach(async () => {
+ mockCiConfigVariables.mockResolvedValue(queryResponse);
+ createComponentWithApollo({ method: mountExtended });
+ });
+
+ it('does not poll for new values', async () => {
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(1);
+
+ advanceToNextFetch(POLLING_INTERVAL);
+ await waitForPromises();
+
+ expect(mockCiConfigVariables).toHaveBeenCalledTimes(1);
+ });
+
+ it('loading icon is shown when content is requested and hidden when received', async () => {
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ };
+
+ describe('When no variables are defined in the CI configuration and the cache is updated', () => {
+ testBehaviorWhenCacheIsPopulated(mockEmptyCiConfigVariablesResponse);
+
+ it('displays an empty form', async () => {
+ mockCiConfigVariables.mockResolvedValue(mockEmptyCiConfigVariablesResponse);
+ createComponentWithApollo({ method: mountExtended });
+ await waitForPromises();
+
+ expect(findKeyInputs().at(0).element.value).toBe('');
+ expect(findValueInputs().at(0).element.value).toBe('');
+ expect(findVariableTypes().at(0).props('text')).toBe('Variable');
+ });
+ });
+
+ describe('When CI configuration has defined variables and they are stored in the cache', () => {
+ testBehaviorWhenCacheIsPopulated(mockCiConfigVariablesResponse);
describe('with different predefined values', () => {
beforeEach(async () => {
diff --git a/spec/frontend/ci/pipeline_new/mock_data.js b/spec/frontend/ci/pipeline_new/mock_data.js
index dfb643a0ba4..5b935c0c819 100644
--- a/spec/frontend/ci/pipeline_new/mock_data.js
+++ b/spec/frontend/ci/pipeline_new/mock_data.js
@@ -132,3 +132,4 @@ export const mockEmptyCiConfigVariablesResponse = mockCiConfigVariablesQueryResp
export const mockCiConfigVariablesResponseWithoutDesc = mockCiConfigVariablesQueryResponse(
mockYamlVariablesWithoutDesc,
);
+export const mockNoCachedCiConfigVariablesResponse = mockCiConfigVariablesQueryResponse(null);
diff --git a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
index 5ca4b25da9b..90ca2a07266 100644
--- a/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/components/codequality_issue_body_spec.js
@@ -38,20 +38,20 @@ describe('code quality issue body issue body', () => {
describe('severity rating', () => {
it.each`
severity | iconClass | iconName
- ${'INFO'} | ${'text-primary-400'} | ${'severity-info'}
- ${'MINOR'} | ${'text-warning-200'} | ${'severity-low'}
- ${'CRITICAL'} | ${'text-danger-600'} | ${'severity-high'}
- ${'BLOCKER'} | ${'text-danger-800'} | ${'severity-critical'}
- ${'UNKNOWN'} | ${'text-secondary-400'} | ${'severity-unknown'}
- ${'INVALID'} | ${'text-secondary-400'} | ${'severity-unknown'}
- ${'info'} | ${'text-primary-400'} | ${'severity-info'}
- ${'minor'} | ${'text-warning-200'} | ${'severity-low'}
- ${'major'} | ${'text-warning-400'} | ${'severity-medium'}
- ${'critical'} | ${'text-danger-600'} | ${'severity-high'}
- ${'blocker'} | ${'text-danger-800'} | ${'severity-critical'}
- ${'unknown'} | ${'text-secondary-400'} | ${'severity-unknown'}
- ${'invalid'} | ${'text-secondary-400'} | ${'severity-unknown'}
- ${undefined} | ${'text-secondary-400'} | ${'severity-unknown'}
+ ${'INFO'} | ${'gl-text-blue-400'} | ${'severity-info'}
+ ${'MINOR'} | ${'gl-text-orange-200'} | ${'severity-low'}
+ ${'CRITICAL'} | ${'gl-text-red-600'} | ${'severity-high'}
+ ${'BLOCKER'} | ${'gl-text-red-800'} | ${'severity-critical'}
+ ${'UNKNOWN'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
+ ${'INVALID'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
+ ${'info'} | ${'gl-text-blue-400'} | ${'severity-info'}
+ ${'minor'} | ${'gl-text-orange-200'} | ${'severity-low'}
+ ${'major'} | ${'gl-text-orange-400'} | ${'severity-medium'}
+ ${'critical'} | ${'gl-text-red-600'} | ${'severity-high'}
+ ${'blocker'} | ${'gl-text-red-800'} | ${'severity-critical'}
+ ${'unknown'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
+ ${'invalid'} | ${'gl-text-gray-400'} | ${'severity-unknown'}
+ ${undefined} | ${'gl-text-gray-400'} | ${'severity-unknown'}
`(
'renders correct icon for "$severity" severity rating',
({ severity, iconClass, iconName }) => {
diff --git a/spec/frontend/ci/reports/codequality_report/store/actions_spec.js b/spec/frontend/ci/reports/codequality_report/store/actions_spec.js
index 88628210793..a606bce3d78 100644
--- a/spec/frontend/ci/reports/codequality_report/store/actions_spec.js
+++ b/spec/frontend/ci/reports/codequality_report/store/actions_spec.js
@@ -2,6 +2,11 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { TEST_HOST } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import createStore from '~/ci/reports/codequality_report/store';
import * as actions from '~/ci/reports/codequality_report/store/actions';
import * as types from '~/ci/reports/codequality_report/store/mutation_types';
@@ -55,7 +60,7 @@ describe('Codequality Reports actions', () => {
describe('on success', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsSuccess', () => {
- mock.onGet(endpoint).reply(200, reportIssues);
+ mock.onGet(endpoint).reply(HTTP_STATUS_OK, reportIssues);
return testAction(
actions.fetchReports,
@@ -74,7 +79,7 @@ describe('Codequality Reports actions', () => {
describe('on error', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsError', () => {
- mock.onGet(endpoint).reply(500);
+ mock.onGet(endpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
actions.fetchReports,
@@ -89,7 +94,7 @@ describe('Codequality Reports actions', () => {
describe('when base report is not found', () => {
it('commits REQUEST_REPORTS and dispatches receiveReportsError', () => {
const data = { status: STATUS_NOT_FOUND };
- mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(200, data);
+ mock.onGet(`${TEST_HOST}/codequality_reports.json`).reply(HTTP_STATUS_OK, data);
return testAction(
actions.fetchReports,
@@ -105,9 +110,9 @@ describe('Codequality Reports actions', () => {
it('continues polling until it receives data', () => {
mock
.onGet(endpoint)
- .replyOnce(204, undefined, pollIntervalHeader)
+ .replyOnce(HTTP_STATUS_NO_CONTENT, undefined, pollIntervalHeader)
.onGet(endpoint)
- .reply(200, reportIssues);
+ .reply(HTTP_STATUS_OK, reportIssues);
return Promise.all([
testAction(
@@ -134,9 +139,9 @@ describe('Codequality Reports actions', () => {
it('continues polling until it receives an error', () => {
mock
.onGet(endpoint)
- .replyOnce(204, undefined, pollIntervalHeader)
+ .replyOnce(HTTP_STATUS_NO_CONTENT, undefined, pollIntervalHeader)
.onGet(endpoint)
- .reply(500);
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return Promise.all([
testAction(
diff --git a/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
new file mode 100644
index 00000000000..edf3d1706cc
--- /dev/null
+++ b/spec/frontend/ci/runner/admin_new_runner_app/admin_new_runner_app_spec.js
@@ -0,0 +1,80 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+
+import AdminNewRunnerApp from '~/ci/runner/admin_new_runner/admin_new_runner_app.vue';
+import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
+import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
+import { DEFAULT_PLATFORM } from '~/ci/runner/constants';
+
+const mockLegacyRegistrationToken = 'LEGACY_REGISTRATION_TOKEN';
+
+Vue.use(VueApollo);
+
+describe('AdminNewRunnerApp', () => {
+ let wrapper;
+
+ const findLegacyInstructionsLink = () => wrapper.findByTestId('legacy-instructions-link');
+ const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
+ const findRunnerPlatformsRadioGroup = () => wrapper.findComponent(RunnerPlatformsRadioGroup);
+ const findRunnerFormFields = () => wrapper.findComponent(RunnerFormFields);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(AdminNewRunnerApp, {
+ propsData: {
+ legacyRegistrationToken: mockLegacyRegistrationToken,
+ ...props,
+ },
+ directives: {
+ GlModal: createMockDirective(),
+ },
+ stubs: {
+ GlSprintf,
+ },
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ describe('Shows legacy modal', () => {
+ it('passes legacy registration to modal', () => {
+ expect(findRunnerInstructionsModal().props('registrationToken')).toEqual(
+ mockLegacyRegistrationToken,
+ );
+ });
+
+ it('opens a modal with the legacy instructions', () => {
+ const modalId = getBinding(findLegacyInstructionsLink().element, 'gl-modal').value;
+
+ expect(findRunnerInstructionsModal().props('modalId')).toBe(modalId);
+ });
+ });
+
+ describe('New runner form fields', () => {
+ describe('Platform', () => {
+ it('shows the platforms radio group', () => {
+ expect(findRunnerPlatformsRadioGroup().props('value')).toBe(DEFAULT_PLATFORM);
+ });
+ });
+
+ describe('Runner', () => {
+ it('shows the runners fields', () => {
+ expect(findRunnerFormFields().props('value')).toEqual({
+ accessLevel: 'NOT_PROTECTED',
+ paused: false,
+ description: '',
+ maintenanceNote: '',
+ maximumTimeout: ' ',
+ runUntagged: false,
+ tagList: '',
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
index e233268b756..ed4f43c12d8 100644
--- a/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runner_show/admin_runner_show_app_spec.js
@@ -1,8 +1,6 @@
import Vue from 'vue';
-import { GlTab, GlTabs } from '@gitlab/ui';
import VueRouter from 'vue-router';
import VueApollo from 'vue-apollo';
-import setWindowLocation from 'helpers/set_window_location_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -15,6 +13,7 @@ import RunnerDetails from '~/ci/runner/components/runner_details.vue';
import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue';
import RunnersJobs from '~/ci/runner/components/runner_jobs.vue';
import runnerQuery from '~/ci/runner/graphql/show/runner.query.graphql';
@@ -42,14 +41,12 @@ describe('AdminRunnerShowApp', () => {
let mockRunnerQuery;
const findRunnerHeader = () => wrapper.findComponent(RunnerHeader);
- const findTabs = () => wrapper.findComponent(GlTabs);
- const findTabAt = (i) => wrapper.findAllComponents(GlTab).at(i);
const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+ const findRunnerDetailsTabs = () => wrapper.findComponent(RunnerDetailsTabs);
const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
- const findJobCountBadge = () => wrapper.findByTestId('job-count-badge');
const mockRunnerQueryResult = (runner = {}) => {
mockRunnerQuery = jest.fn().mockResolvedValue({
@@ -89,16 +86,20 @@ describe('AdminRunnerShowApp', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
- it('displays the runner header', async () => {
+ it('displays the runner header', () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
it('displays the runner edit and pause buttons', async () => {
- expect(findRunnerEditButton().exists()).toBe(true);
+ expect(findRunnerEditButton().attributes('href')).toBe(mockRunner.editAdminUrl);
expect(findRunnerPauseButton().exists()).toBe(true);
expect(findRunnerDeleteButton().exists()).toBe(true);
});
+ it('shows runner details', () => {
+ expect(findRunnerDetailsTabs().props('runner')).toEqual(mockRunner);
+ });
+
it('shows basic runner details', async () => {
const expected = `Description My Runner
Last contact Never contacted
@@ -118,20 +119,11 @@ describe('AdminRunnerShowApp', () => {
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
});
- it.each(['#/', '#/unknown-tab'])('shows details when location hash is `%s`', async (hash) => {
- setWindowLocation(hash);
-
- await createComponent({ mountFn: mountExtended });
-
- expect(findTabs().props('value')).toBe(0);
- expect(findRunnerDetails().exists()).toBe(true);
- expect(findRunnersJobs().exists()).toBe(false);
- });
-
describe('when runner cannot be updated', () => {
beforeEach(async () => {
mockRunnerQueryResult({
userPermissions: {
+ ...mockRunner.userPermissions,
updateRunner: false,
},
});
@@ -145,12 +137,17 @@ describe('AdminRunnerShowApp', () => {
expect(findRunnerEditButton().exists()).toBe(false);
expect(findRunnerPauseButton().exists()).toBe(false);
});
+
+ it('displays delete button', () => {
+ expect(findRunnerDeleteButton().exists()).toBe(true);
+ });
});
describe('when runner cannot be deleted', () => {
beforeEach(async () => {
mockRunnerQueryResult({
userPermissions: {
+ ...mockRunner.userPermissions,
deleteRunner: false,
},
});
@@ -160,9 +157,14 @@ describe('AdminRunnerShowApp', () => {
});
});
- it('does not display the runner edit and pause buttons', () => {
+ it('does not display the delete button', () => {
expect(findRunnerDeleteButton().exists()).toBe(false);
});
+
+ it('displays edit and pause buttons', () => {
+ expect(findRunnerEditButton().exists()).toBe(true);
+ expect(findRunnerPauseButton().exists()).toBe(true);
+ });
});
describe('when runner is deleted', () => {
@@ -240,74 +242,4 @@ describe('AdminRunnerShowApp', () => {
expect(createAlert).toHaveBeenCalled();
});
});
-
- describe('When showing jobs', () => {
- const stubs = {
- GlTab,
- GlTabs,
- };
-
- it('without a runner, shows no jobs', () => {
- mockRunnerQuery = jest.fn().mockResolvedValue({
- data: {
- runner: null,
- },
- });
-
- createComponent({ stubs });
-
- expect(findJobCountBadge().exists()).toBe(false);
- expect(findRunnersJobs().exists()).toBe(false);
- });
-
- it('when URL hash links to jobs tab', async () => {
- mockRunnerQueryResult();
- setWindowLocation('#/jobs');
-
- await createComponent({ mountFn: mountExtended });
-
- expect(findTabs().props('value')).toBe(1);
- expect(findRunnerDetails().exists()).toBe(false);
- expect(findRunnersJobs().exists()).toBe(true);
- });
-
- it('without a job count, shows no jobs count', async () => {
- mockRunnerQueryResult({ jobCount: null });
-
- await createComponent({ stubs });
-
- expect(findJobCountBadge().exists()).toBe(false);
- });
-
- it('with a job count, shows jobs count', async () => {
- const runner = { jobCount: 3 };
- mockRunnerQueryResult(runner);
-
- await createComponent({ stubs });
-
- expect(findJobCountBadge().text()).toBe('3');
- });
- });
-
- describe('When navigating to another tab', () => {
- let routerPush;
-
- beforeEach(async () => {
- mockRunnerQueryResult();
-
- await createComponent({ mountFn: mountExtended });
-
- routerPush = jest.spyOn(wrapper.vm.$router, 'push').mockImplementation(() => {});
- });
-
- it('navigates to details', () => {
- findTabAt(0).vm.$emit('click');
- expect(routerPush).toHaveBeenLastCalledWith({ name: 'details' });
- });
-
- it('navigates to job', () => {
- findTabAt(1).vm.$emit('click');
- expect(routerPush).toHaveBeenLastCalledWith({ name: 'jobs' });
- });
- });
});
diff --git a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
index 9084ecdb4cc..7fc240e520b 100644
--- a/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
+++ b/spec/frontend/ci/runner/admin_runners/admin_runners_app_spec.js
@@ -39,6 +39,7 @@ import {
I18N_GROUP_TYPE,
I18N_PROJECT_TYPE,
INSTANCE_TYPE,
+ JOBS_ROUTE_PATH,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
@@ -56,6 +57,7 @@ import {
allRunnersDataPaginated,
onlineContactTimeoutSecs,
staleTimeoutSecs,
+ newRunnerPath,
emptyPageInfo,
emptyStateSvgPath,
emptyStateFilteredSvgPath,
@@ -113,6 +115,7 @@ describe('AdminRunnersApp', () => {
apolloProvider: createMockApollo(handlers, {}, cacheConfig),
propsData: {
registrationToken: mockRegistrationToken,
+ newRunnerPath,
...props,
},
provide: {
@@ -280,11 +283,14 @@ describe('AdminRunnersApp', () => {
it('Shows job status and links to jobs', () => {
const badge = wrapper
- .find('tr [data-testid="td-summary"]')
+ .find('tr [data-testid="td-status"]')
.findComponent(RunnerJobStatusBadge);
expect(badge.props('jobStatus')).toBe(mockRunners[0].jobExecutionStatus);
- expect(badge.attributes('href')).toBe(`http://localhost/admin/runners/${id}#/jobs`);
+
+ const badgeHref = new URL(badge.attributes('href'));
+ expect(badgeHref.pathname).toBe(`/admin/runners/${id}`);
+ expect(badgeHref.hash).toBe(`#${JOBS_ROUTE_PATH}`);
});
it('When runner is paused or unpaused, some data is refetched', async () => {
@@ -443,7 +449,13 @@ describe('AdminRunnersApp', () => {
});
it('shows an empty state', () => {
- expect(findRunnerListEmptyState().props('isSearchFiltered')).toBe(false);
+ expect(findRunnerListEmptyState().props()).toEqual({
+ newRunnerPath,
+ isSearchFiltered: false,
+ filteredSvgPath: emptyStateFilteredSvgPath,
+ registrationToken: mockRegistrationToken,
+ svgPath: emptyStateSvgPath,
+ });
});
describe('when a filter is selected by the user', () => {
diff --git a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
index 2fb824a8fa5..1ff60ff1a9d 100644
--- a/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_status_cell_spec.js
@@ -10,6 +10,7 @@ import {
INSTANCE_TYPE,
STATUS_ONLINE,
STATUS_OFFLINE,
+ JOB_STATUS_IDLE,
} from '~/ci/runner/constants';
describe('RunnerStatusCell', () => {
@@ -18,16 +19,18 @@ describe('RunnerStatusCell', () => {
const findStatusBadge = () => wrapper.findComponent(RunnerStatusBadge);
const findPausedBadge = () => wrapper.findComponent(RunnerPausedBadge);
- const createComponent = ({ runner = {} } = {}) => {
+ const createComponent = ({ runner = {}, ...options } = {}) => {
wrapper = mount(RunnerStatusCell, {
propsData: {
runner: {
runnerType: INSTANCE_TYPE,
active: true,
status: STATUS_ONLINE,
+ jobExecutionStatus: JOB_STATUS_IDLE,
...runner,
},
},
+ ...options,
});
};
@@ -74,4 +77,14 @@ describe('RunnerStatusCell', () => {
expect(wrapper.text()).toBe('');
});
+
+ it('Displays "runner-job-status-badge" slot', () => {
+ createComponent({
+ scopedSlots: {
+ 'runner-job-status-badge': ({ runner }) => `Job status ${runner.jobExecutionStatus}`,
+ },
+ });
+
+ expect(wrapper.text()).toContain(`Job status ${JOB_STATUS_IDLE}`);
+ });
});
diff --git a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
index 10280c77303..1711df42491 100644
--- a/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
+++ b/spec/frontend/ci/runner/components/cells/runner_summary_cell_spec.js
@@ -3,7 +3,6 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import RunnerSummaryCell from '~/ci/runner/components/cells/runner_summary_cell.vue';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
import RunnerTags from '~/ci/runner/components/runner_tags.vue';
-import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue';
import RunnerSummaryField from '~/ci/runner/components/cells/runner_summary_field.vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
@@ -22,7 +21,6 @@ describe('RunnerTypeCell', () => {
let wrapper;
const findLockIcon = () => wrapper.findByTestId('lock-icon');
- const findRunnerJobStatusBadge = () => wrapper.findComponent(RunnerJobStatusBadge);
const findRunnerTags = () => wrapper.findComponent(RunnerTags);
const findRunnerSummaryField = (icon) =>
wrapper.findAllComponents(RunnerSummaryField).filter((w) => w.props('icon') === icon)
@@ -95,10 +93,6 @@ describe('RunnerTypeCell', () => {
expect(wrapper.text()).toContain(I18N_NO_DESCRIPTION);
});
- it('Displays job execution status', () => {
- expect(findRunnerJobStatusBadge().props('jobStatus')).toBe(mockRunner.jobExecutionStatus);
- });
-
it('Displays last contact', () => {
createComponent({
contactedAt: '2022-01-02',
@@ -166,14 +160,14 @@ describe('RunnerTypeCell', () => {
expect(findRunnerTags().props('tagList')).toEqual(['shell', 'linux']);
});
- it.each(['runner-name', 'runner-job-status-badge'])('Displays a custom "%s" slot', (slotName) => {
+ it('Displays a custom runner-name slot', () => {
const slotContent = 'My custom runner name';
createComponent(
{},
{
slots: {
- [slotName]: slotContent,
+ 'runner-name': slotContent,
},
},
);
diff --git a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
index 0ecafdd7d83..0daaca9c4ff 100644
--- a/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
+++ b/spec/frontend/ci/runner/components/registration/registration_dropdown_spec.js
@@ -1,8 +1,9 @@
import { GlModal, GlDropdown, GlDropdownItem, GlDropdownForm } from '@gitlab/ui';
import { mount, shallowMount, createWrapper } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
-
import VueApollo from 'vue-apollo';
+
+import { s__ } from '~/locale';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -84,9 +85,9 @@ describe('RegistrationDropdown', () => {
it.each`
type | text
- ${INSTANCE_TYPE} | ${'Register an instance runner'}
- ${GROUP_TYPE} | ${'Register a group runner'}
- ${PROJECT_TYPE} | ${'Register a project runner'}
+ ${INSTANCE_TYPE} | ${s__('Runners|Register an instance runner')}
+ ${GROUP_TYPE} | ${s__('Runners|Register a group runner')}
+ ${PROJECT_TYPE} | ${s__('Runners|Register a project runner')}
`('Dropdown text for type $type is "$text"', () => {
createComponent({ props: { type: INSTANCE_TYPE } }, mount);
diff --git a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
index 64f5a0e3b57..0dc5a90fb83 100644
--- a/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
+++ b/spec/frontend/ci/runner/components/runner_bulk_delete_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import { makeVar } from '@apollo/client/core';
import { GlModal, GlSprintf } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { createAlert } from '~/flash';
@@ -22,6 +23,7 @@ describe('RunnerBulkDelete', () => {
let mockState;
let mockCheckedRunnerIds;
+ const findBanner = () => wrapper.findByTestId('runner-bulk-delete-banner');
const findClearBtn = () => wrapper.findByText(s__('Runners|Clear selection'));
const findDeleteBtn = () => wrapper.findByText(s__('Runners|Delete selected'));
const findModal = () => wrapper.findComponent(GlModal);
@@ -64,10 +66,11 @@ describe('RunnerBulkDelete', () => {
beforeEach(() => {
mockState = createLocalState();
+ mockCheckedRunnerIds = makeVar([]);
jest
.spyOn(mockState.cacheConfig.typePolicies.Query.fields, 'checkedRunnerIds')
- .mockImplementation(() => mockCheckedRunnerIds);
+ .mockImplementation(() => mockCheckedRunnerIds());
});
afterEach(() => {
@@ -76,15 +79,13 @@ describe('RunnerBulkDelete', () => {
describe('When no runners are checked', () => {
beforeEach(async () => {
- mockCheckedRunnerIds = [];
-
createComponent();
await waitForPromises();
});
it('shows no contents', () => {
- expect(wrapper.html()).toBe('');
+ expect(findBanner().exists()).toBe(false);
});
});
@@ -94,7 +95,7 @@ describe('RunnerBulkDelete', () => {
${2} | ${[mockId1, mockId2]} | ${'2 runners'}
`('When $count runner(s) are checked', ({ ids, text }) => {
beforeEach(() => {
- mockCheckedRunnerIds = ids;
+ mockCheckedRunnerIds(ids);
createComponent();
@@ -102,7 +103,7 @@ describe('RunnerBulkDelete', () => {
});
it(`shows "${text}"`, () => {
- expect(wrapper.text()).toContain(text);
+ expect(findBanner().text()).toContain(text);
});
it('clears selection', () => {
@@ -133,7 +134,7 @@ describe('RunnerBulkDelete', () => {
};
beforeEach(() => {
- mockCheckedRunnerIds = [mockId1, mockId2];
+ mockCheckedRunnerIds([mockId1, mockId2]);
createComponent();
@@ -157,20 +158,23 @@ describe('RunnerBulkDelete', () => {
it('mutation is called', () => {
expect(bulkRunnerDeleteHandler).toHaveBeenCalledWith({
- input: { ids: mockCheckedRunnerIds },
+ input: { ids: mockCheckedRunnerIds() },
});
});
});
describe('when deletion is successful', () => {
+ let deletedIds;
+
beforeEach(async () => {
+ deletedIds = mockCheckedRunnerIds();
bulkRunnerDeleteHandler.mockResolvedValue({
data: {
- bulkRunnerDelete: { deletedIds: mockCheckedRunnerIds, errors: [] },
+ bulkRunnerDelete: { deletedIds, errors: [] },
},
});
-
confirmDeletion();
+ mockCheckedRunnerIds([]);
await waitForPromises();
});
@@ -182,12 +186,12 @@ describe('RunnerBulkDelete', () => {
it('user interface is updated', () => {
const { evict, gc } = apolloCache;
- expect(evict).toHaveBeenCalledTimes(mockCheckedRunnerIds.length);
+ expect(evict).toHaveBeenCalledTimes(deletedIds.length);
expect(evict).toHaveBeenCalledWith({
- id: expect.stringContaining(mockCheckedRunnerIds[0]),
+ id: expect.stringContaining(deletedIds[0]),
});
expect(evict).toHaveBeenCalledWith({
- id: expect.stringContaining(mockCheckedRunnerIds[1]),
+ id: expect.stringContaining(deletedIds[1]),
});
expect(gc).toHaveBeenCalledTimes(1);
@@ -195,7 +199,7 @@ describe('RunnerBulkDelete', () => {
it('emits deletion confirmation', () => {
expect(wrapper.emitted('deleted')).toEqual([
- [{ message: expect.stringContaining(`${mockCheckedRunnerIds.length}`) }],
+ [{ message: expect.stringContaining(`${deletedIds.length}`) }],
]);
});
diff --git a/spec/frontend/ci/runner/components/runner_details_tabs_spec.js b/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
new file mode 100644
index 00000000000..a59c5a21377
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_details_tabs_spec.js
@@ -0,0 +1,127 @@
+import Vue from 'vue';
+import { GlTab, GlTabs } from '@gitlab/ui';
+import VueRouter from 'vue-router';
+import VueApollo from 'vue-apollo';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { JOBS_ROUTE_PATH, I18N_DETAILS, I18N_JOBS } from '~/ci/runner/constants';
+
+import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue';
+import RunnerDetails from '~/ci/runner/components/runner_details.vue';
+import RunnerJobs from '~/ci/runner/components/runner_jobs.vue';
+
+import { runnerData } from '../mock_data';
+
+// Vue Test Utils `stubs` option does not stub components mounted
+// in <router-view>. Use mocking instead:
+jest.mock('~/ci/runner/components/runner_jobs.vue', () => {
+ const ActualRunnerJobs = jest.requireActual('~/ci/runner/components/runner_jobs.vue').default;
+ return {
+ props: ActualRunnerJobs.props,
+ render() {},
+ };
+});
+
+const mockRunner = runnerData.data.runner;
+
+Vue.use(VueApollo);
+Vue.use(VueRouter);
+
+describe('RunnerDetailsTabs', () => {
+ let wrapper;
+ let routerPush;
+
+ const findTabs = () => wrapper.findComponent(GlTabs);
+ const findRunnerDetails = () => wrapper.findComponent(RunnerDetails);
+ const findRunnerJobs = () => wrapper.findComponent(RunnerJobs);
+ const findJobCountBadge = () => wrapper.findByTestId('job-count-badge');
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(RunnerDetailsTabs, {
+ propsData: {
+ runner: mockRunner,
+ ...props,
+ },
+ ...options,
+ });
+
+ routerPush = jest.spyOn(wrapper.vm.$router, 'push').mockImplementation(() => {});
+
+ return waitForPromises();
+ };
+
+ it('shows basic runner details', async () => {
+ await createComponent({ mountFn: mountExtended });
+
+ expect(findRunnerDetails().props('runner')).toBe(mockRunner);
+ expect(findRunnerJobs().exists()).toBe(false);
+ });
+
+ it('shows runner jobs', async () => {
+ setWindowLocation(`#${JOBS_ROUTE_PATH}`);
+
+ await createComponent({ mountFn: mountExtended });
+
+ expect(findRunnerDetails().exists()).toBe(false);
+ expect(findRunnerJobs().props('runner')).toBe(mockRunner);
+ });
+
+ it.each`
+ jobCount | badgeText
+ ${null} | ${null}
+ ${1} | ${'1'}
+ ${1000} | ${'1,000'}
+ ${1001} | ${'1,000+'}
+ `('shows runner jobs count', async ({ jobCount, badgeText }) => {
+ await createComponent({
+ stubs: {
+ GlTab,
+ },
+ props: {
+ runner: {
+ ...mockRunner,
+ jobCount,
+ },
+ },
+ });
+
+ if (!badgeText) {
+ expect(findJobCountBadge().exists()).toBe(false);
+ } else {
+ expect(findJobCountBadge().text()).toBe(badgeText);
+ }
+ });
+
+ it.each(['#/', '#/unknown-tab'])('shows details when location hash is `%s`', async (hash) => {
+ setWindowLocation(hash);
+
+ await createComponent({ mountFn: mountExtended });
+
+ expect(findTabs().props('value')).toBe(0);
+ expect(findRunnerDetails().exists()).toBe(true);
+ expect(findRunnerJobs().exists()).toBe(false);
+ });
+
+ describe.each`
+ location | tab | navigatedTo
+ ${'#/details'} | ${I18N_DETAILS} | ${[]}
+ ${'#/details'} | ${I18N_JOBS} | ${[[{ name: 'jobs' }]]}
+ ${'#/jobs'} | ${I18N_JOBS} | ${[]}
+ ${'#/jobs'} | ${I18N_DETAILS} | ${[[{ name: 'details' }]]}
+ `('When at $location', ({ location, tab, navigatedTo }) => {
+ beforeEach(async () => {
+ setWindowLocation(location);
+
+ await createComponent({
+ mountFn: mountExtended,
+ });
+ });
+
+ it(`on click on ${tab}, navigates to ${JSON.stringify(navigatedTo)}`, () => {
+ wrapper.findByText(tab).trigger('click');
+
+ expect(routerPush.mock.calls).toEqual(navigatedTo);
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_form_fields_spec.js b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
new file mode 100644
index 00000000000..5b429645d17
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_form_fields_spec.js
@@ -0,0 +1,87 @@
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerFormFields from '~/ci/runner/components/runner_form_fields.vue';
+import { ACCESS_LEVEL_NOT_PROTECTED, ACCESS_LEVEL_REF_PROTECTED } from '~/ci/runner/constants';
+
+const mockDescription = 'My description';
+const mockMaxTimeout = 60;
+const mockTags = 'tag, tag2';
+
+describe('RunnerFormFields', () => {
+ let wrapper;
+
+ const findInput = (name) => wrapper.find(`input[name="${name}"]`);
+
+ const createComponent = ({ runner } = {}) => {
+ wrapper = mountExtended(RunnerFormFields, {
+ propsData: {
+ value: runner,
+ },
+ });
+ };
+
+ it('updates runner fields', async () => {
+ createComponent();
+
+ expect(wrapper.emitted('input')).toBe(undefined);
+
+ findInput('description').setValue(mockDescription);
+ findInput('max-timeout').setValue(mockMaxTimeout);
+ findInput('paused').setChecked(true);
+ findInput('protected').setChecked(true);
+ findInput('run-untagged').setChecked(true);
+ findInput('tags').setValue(mockTags);
+
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0][0]).toMatchObject({
+ description: mockDescription,
+ maximumTimeout: mockMaxTimeout,
+ tagList: mockTags,
+ });
+ });
+
+ it('checks checkbox fields', async () => {
+ createComponent({
+ runner: {
+ paused: false,
+ accessLevel: ACCESS_LEVEL_NOT_PROTECTED,
+ runUntagged: false,
+ },
+ });
+
+ findInput('paused').setChecked(true);
+ findInput('protected').setChecked(true);
+ findInput('run-untagged').setChecked(true);
+
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0][0]).toEqual({
+ paused: true,
+ accessLevel: ACCESS_LEVEL_REF_PROTECTED,
+ runUntagged: true,
+ });
+ });
+
+ it('unchecks checkbox fields', async () => {
+ createComponent({
+ runner: {
+ paused: true,
+ accessLevel: ACCESS_LEVEL_REF_PROTECTED,
+ runUntagged: true,
+ },
+ });
+
+ findInput('paused').setChecked(false);
+ findInput('protected').setChecked(false);
+ findInput('run-untagged').setChecked(false);
+
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0][0]).toEqual({
+ paused: false,
+ accessLevel: ACCESS_LEVEL_NOT_PROTECTED,
+ runUntagged: false,
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_header_spec.js b/spec/frontend/ci/runner/components/runner_header_spec.js
index a04011de1cd..abe3b47767e 100644
--- a/spec/frontend/ci/runner/components/runner_header_spec.js
+++ b/spec/frontend/ci/runner/components/runner_header_spec.js
@@ -6,7 +6,7 @@ import {
GROUP_TYPE,
STATUS_ONLINE,
} from '~/ci/runner/constants';
-import { TYPE_CI_RUNNER } from '~/graphql_shared/constants';
+import { TYPENAME_CI_RUNNER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import TimeAgo from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -71,7 +71,7 @@ describe('RunnerHeader', () => {
it('displays the runner id', () => {
createComponent({
runner: {
- id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
+ id: convertToGraphQLId(TYPENAME_CI_RUNNER, 99),
},
});
@@ -99,7 +99,7 @@ describe('RunnerHeader', () => {
it('does not display runner creation time if "createdAt" is missing', () => {
createComponent({
runner: {
- id: convertToGraphQLId(TYPE_CI_RUNNER, 99),
+ id: convertToGraphQLId(TYPENAME_CI_RUNNER, 99),
createdAt: null,
},
});
diff --git a/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
index 015bebf40e3..c4476d01386 100644
--- a/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
+++ b/spec/frontend/ci/runner/components/runner_job_status_badge_spec.js
@@ -23,16 +23,25 @@ describe('RunnerTypeBadge', () => {
};
it.each`
- jobStatus | classes | text
- ${JOB_STATUS_RUNNING} | ${['gl-mr-3', 'gl-bg-transparent!', 'gl-text-blue-600!', 'gl-border', 'gl-border-blue-600!']} | ${I18N_JOB_STATUS_RUNNING}
- ${JOB_STATUS_IDLE} | ${['gl-mr-3', 'gl-bg-transparent!', 'gl-text-gray-700!', 'gl-border', 'gl-border-gray-500!']} | ${I18N_JOB_STATUS_IDLE}
+ jobStatus | classes | text
+ ${JOB_STATUS_RUNNING} | ${['gl-text-blue-600!', 'gl-border-blue-600!']} | ${I18N_JOB_STATUS_RUNNING}
+ ${JOB_STATUS_IDLE} | ${['gl-text-gray-700!', 'gl-border-gray-500!']} | ${I18N_JOB_STATUS_IDLE}
`(
'renders $jobStatus job status with "$text" text and styles',
({ jobStatus, classes, text }) => {
createComponent({ props: { jobStatus } });
- expect(findBadge().props()).toMatchObject({ size: 'sm', variant: 'muted' });
- expect(findBadge().classes().sort()).toEqual(classes.sort());
+ expect(findBadge().props()).toMatchObject({ size: 'md', variant: 'muted' });
+ expect(findBadge().classes().sort()).toEqual(
+ [
+ ...classes,
+ 'gl-border',
+ 'gl-display-inline-block',
+ 'gl-max-w-full',
+ 'gl-text-truncate',
+ 'gl-bg-transparent!',
+ ].sort(),
+ );
expect(findBadge().text()).toBe(text);
},
);
diff --git a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
index 8defe568df8..281aa1aeb77 100644
--- a/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
+++ b/spec/frontend/ci/runner/components/runner_jobs_table_spec.js
@@ -72,7 +72,7 @@ describe('RunnerJobsTable', () => {
});
it('Displays details of a job', () => {
- const { id, detailedStatus, pipeline, shortSha, commitPath } = mockJobs[0];
+ const { id, detailedStatus, project, shortSha, commitPath } = mockJobs[0];
expect(findCell({ field: 'status' }).text()).toMatchInterpolatedText(detailedStatus.text);
@@ -81,10 +81,8 @@ describe('RunnerJobsTable', () => {
detailedStatus.detailsPath,
);
- expect(findCell({ field: 'project' }).text()).toBe(pipeline.project.name);
- expect(findCell({ field: 'project' }).find('a').attributes('href')).toBe(
- pipeline.project.webUrl,
- );
+ expect(findCell({ field: 'project' }).text()).toBe(project.name);
+ expect(findCell({ field: 'project' }).find('a').attributes('href')).toBe(project.webUrl);
expect(findCell({ field: 'commit' }).text()).toBe(shortSha);
expect(findCell({ field: 'commit' }).find('a').attributes('href')).toBe(commitPath);
diff --git a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
index d351f7b6908..6aea3ddf58c 100644
--- a/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_empty_state_spec.js
@@ -4,10 +4,14 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions/runner_instructions_modal.vue';
+import {
+ newRunnerPath,
+ emptyStateSvgPath,
+ emptyStateFilteredSvgPath,
+} from 'jest/ci/runner/mock_data';
+
import RunnerListEmptyState from '~/ci/runner/components/runner_list_empty_state.vue';
-const mockSvgPath = 'mock-svg-path.svg';
-const mockFilteredSvgPath = 'mock-filtered-svg-path.svg';
const mockRegistrationToken = 'REGISTRATION_TOKEN';
describe('RunnerListEmptyState', () => {
@@ -17,12 +21,13 @@ describe('RunnerListEmptyState', () => {
const findLink = () => wrapper.findComponent(GlLink);
const findRunnerInstructionsModal = () => wrapper.findComponent(RunnerInstructionsModal);
- const createComponent = ({ props, mountFn = shallowMountExtended } = {}) => {
+ const createComponent = ({ props, mountFn = shallowMountExtended, ...options } = {}) => {
wrapper = mountFn(RunnerListEmptyState, {
propsData: {
- svgPath: mockSvgPath,
- filteredSvgPath: mockFilteredSvgPath,
+ svgPath: emptyStateSvgPath,
+ filteredSvgPath: emptyStateFilteredSvgPath,
registrationToken: mockRegistrationToken,
+ newRunnerPath,
...props,
},
directives: {
@@ -33,6 +38,7 @@ describe('RunnerListEmptyState', () => {
GlSprintf,
GlLink,
},
+ ...options,
});
};
@@ -45,7 +51,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(mockSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
});
it('displays "no results" text with instructions', () => {
@@ -56,10 +62,53 @@ describe('RunnerListEmptyState', () => {
expect(findEmptyState().text()).toMatchInterpolatedText(`${title} ${desc}`);
});
- it('opens a runner registration instructions modal with a link', () => {
- const { value } = getBinding(findLink().element, 'gl-modal');
+ describe('when create_runner_workflow is enabled', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: { createRunnerWorkflow: true },
+ },
+ });
+ });
+
+ it('shows a link to the new runner page', () => {
+ expect(findLink().attributes('href')).toBe(newRunnerPath);
+ });
+ });
+
+ describe('when create_runner_workflow is enabled and newRunnerPath not defined', () => {
+ beforeEach(() => {
+ createComponent({
+ props: {
+ newRunnerPath: null,
+ },
+ provide: {
+ glFeatures: { createRunnerWorkflow: true },
+ },
+ });
+ });
+
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
+
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
+ });
+
+ describe('when create_runner_workflow is disabled', () => {
+ beforeEach(() => {
+ createComponent({
+ provide: {
+ glFeatures: { createRunnerWorkflow: false },
+ },
+ });
+ });
+
+ it('opens a runner registration instructions modal with a link', () => {
+ const { value } = getBinding(findLink().element, 'gl-modal');
- expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ expect(findRunnerInstructionsModal().props('modalId')).toEqual(value);
+ });
});
});
@@ -69,7 +118,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders an illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(mockSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(emptyStateSvgPath);
});
it('displays "no results" text', () => {
@@ -92,7 +141,7 @@ describe('RunnerListEmptyState', () => {
});
it('renders a "filtered search" illustration', () => {
- expect(findEmptyState().props('svgPath')).toBe(mockFilteredSvgPath);
+ expect(findEmptyState().props('svgPath')).toBe(emptyStateFilteredSvgPath);
});
it('displays "no filtered results" text', () => {
diff --git a/spec/frontend/ci/runner/components/runner_list_spec.js b/spec/frontend/ci/runner/components/runner_list_spec.js
index 1267d045623..2e5d1dbd063 100644
--- a/spec/frontend/ci/runner/components/runner_list_spec.js
+++ b/spec/frontend/ci/runner/components/runner_list_spec.js
@@ -177,30 +177,30 @@ describe('RunnerList', () => {
});
describe('Scoped cell slots', () => {
- it('Render #runner-name slot in "summary" cell', () => {
+ it('Render #runner-job-status-badge slot in "status" cell', () => {
createComponent(
{
- scopedSlots: { 'runner-name': ({ runner }) => `Summary: ${runner.id}` },
+ scopedSlots: {
+ 'runner-job-status-badge': ({ runner }) => `Job status ${runner.jobExecutionStatus}`,
+ },
},
mountExtended,
);
- expect(findCell({ fieldKey: 'summary' }).text()).toContain(`Summary: ${mockRunners[0].id}`);
+ expect(findCell({ fieldKey: 'status' }).text()).toContain(
+ `Job status ${mockRunners[0].jobExecutionStatus}`,
+ );
});
- it('Render #runner-job-status-badge slot in "summary" cell', () => {
+ it('Render #runner-name slot in "summary" cell', () => {
createComponent(
{
- scopedSlots: {
- 'runner-job-status-badge': ({ runner }) => `Job status ${runner.jobExecutionStatus}`,
- },
+ scopedSlots: { 'runner-name': ({ runner }) => `Summary: ${runner.id}` },
},
mountExtended,
);
- expect(findCell({ fieldKey: 'summary' }).text()).toContain(
- `Job status ${mockRunners[0].jobExecutionStatus}`,
- );
+ expect(findCell({ fieldKey: 'summary' }).text()).toContain(`Summary: ${mockRunners[0].id}`);
});
it('Render #runner-actions-cell slot in "actions" cell', () => {
diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
new file mode 100644
index 00000000000..db6fd2c369b
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_platforms_radio_group_spec.js
@@ -0,0 +1,96 @@
+import { nextTick } from 'vue';
+import { GlFormRadioGroup, GlIcon, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import RunnerPlatformsRadio from '~/ci/runner/components/runner_platforms_radio.vue';
+import {
+ LINUX_PLATFORM,
+ MACOS_PLATFORM,
+ WINDOWS_PLATFORM,
+ AWS_PLATFORM,
+ DOCKER_HELP_URL,
+ KUBERNETES_HELP_URL,
+} from '~/ci/runner/constants';
+
+import RunnerPlatformsRadioGroup from '~/ci/runner/components/runner_platforms_radio_group.vue';
+
+const mockProvide = {
+ awsImgPath: 'awsLogo.svg',
+ dockerImgPath: 'dockerLogo.svg',
+ kubernetesImgPath: 'kubernetesLogo.svg',
+};
+
+describe('RunnerPlatformsRadioGroup', () => {
+ let wrapper;
+
+ const findFormRadioGroup = () => wrapper.findComponent(GlFormRadioGroup);
+ const findFormRadios = () => wrapper.findAllComponents(RunnerPlatformsRadio).wrappers;
+ const findFormRadioByText = (text) =>
+ findFormRadios()
+ .filter((w) => w.text() === text)
+ .at(0);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(RunnerPlatformsRadioGroup, {
+ propsData: {
+ value: null,
+ ...props,
+ },
+ provide: mockProvide,
+ ...options,
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('contains expected options with images', () => {
+ const labels = findFormRadios().map((w) => [w.text(), w.props('image')]);
+
+ expect(labels).toEqual([
+ ['Linux', null],
+ ['macOS', null],
+ ['Windows', null],
+ ['AWS', expect.any(String)],
+ ['Docker', expect.any(String)],
+ ['Kubernetes', expect.any(String)],
+ ]);
+ });
+
+ it('allows users to use radio group', async () => {
+ findFormRadioGroup().vm.$emit('input', MACOS_PLATFORM);
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0]).toEqual([MACOS_PLATFORM]);
+ });
+
+ it.each`
+ text | value
+ ${'Linux'} | ${LINUX_PLATFORM}
+ ${'macOS'} | ${MACOS_PLATFORM}
+ ${'Windows'} | ${WINDOWS_PLATFORM}
+ ${'AWS'} | ${AWS_PLATFORM}
+ `('user can select "$text"', async ({ text, value }) => {
+ const radio = findFormRadioByText(text);
+ expect(radio.props('value')).toBe(value);
+
+ radio.vm.$emit('input', value);
+ await nextTick();
+
+ expect(wrapper.emitted('input')[0]).toEqual([value]);
+ });
+
+ it.each`
+ text | href
+ ${'Docker'} | ${DOCKER_HELP_URL}
+ ${'Kubernetes'} | ${KUBERNETES_HELP_URL}
+ `('provides link to "$text" docs', async ({ text, href }) => {
+ const radio = findFormRadioByText(text);
+
+ expect(radio.findComponent(GlLink).attributes()).toEqual({
+ href,
+ target: '_blank',
+ });
+ expect(radio.findComponent(GlIcon).props('name')).toBe('external-link');
+ });
+});
diff --git a/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js b/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
new file mode 100644
index 00000000000..fb81edd1ae2
--- /dev/null
+++ b/spec/frontend/ci/runner/components/runner_platforms_radio_spec.js
@@ -0,0 +1,154 @@
+import { GlFormRadio } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import RunnerPlatformsRadio from '~/ci/runner/components/runner_platforms_radio.vue';
+
+const mockImg = 'mock.svg';
+const mockValue = 'value';
+const mockValue2 = 'value2';
+const mockSlot = '<div>a</div>';
+
+describe('RunnerPlatformsRadio', () => {
+ let wrapper;
+
+ const findDiv = () => wrapper.find('div');
+ const findImg = () => wrapper.find('img');
+ const findFormRadio = () => wrapper.findComponent(GlFormRadio);
+
+ const createComponent = ({ props = {}, mountFn = shallowMountExtended, ...options } = {}) => {
+ wrapper = mountFn(RunnerPlatformsRadio, {
+ propsData: {
+ image: mockImg,
+ value: mockValue,
+ ...props,
+ },
+ ...options,
+ });
+ };
+
+ describe('when its selectable', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { value: mockValue },
+ });
+ });
+
+ it('shows the item is clickable', () => {
+ expect(wrapper.classes('gl-cursor-pointer')).toBe(true);
+ });
+
+ it('shows radio option', () => {
+ expect(findFormRadio().attributes('value')).toBe(mockValue);
+ });
+
+ it('emits when item is clicked', async () => {
+ findDiv().trigger('click');
+
+ expect(wrapper.emitted('input')).toEqual([[mockValue]]);
+ });
+
+ it.each(['input', 'change'])('emits radio "%s" event', (event) => {
+ findFormRadio().vm.$emit(event, mockValue2);
+
+ expect(wrapper.emitted(event)).toEqual([[mockValue2]]);
+ });
+
+ it('shows image', () => {
+ expect(findImg().attributes()).toMatchObject({
+ src: mockImg,
+ 'aria-hidden': 'true',
+ });
+ });
+
+ it('shows slot', () => {
+ createComponent({
+ slots: {
+ default: mockSlot,
+ },
+ });
+
+ expect(wrapper.html()).toContain(mockSlot);
+ });
+
+ describe('with no image', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { value: mockValue, image: null },
+ });
+ });
+
+ it('shows no image', () => {
+ expect(findImg().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when its not selectable', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { value: null },
+ });
+ });
+
+ it('shows the item is clickable', () => {
+ expect(wrapper.classes('gl-cursor-pointer')).toBe(false);
+ });
+
+ it('does not emit when item is clicked', async () => {
+ findDiv().trigger('click');
+
+ expect(wrapper.emitted('input')).toBe(undefined);
+ });
+
+ it('does not show a radio option', () => {
+ expect(findFormRadio().exists()).toBe(false);
+ });
+
+ it('shows image', () => {
+ expect(findImg().attributes()).toMatchObject({
+ src: mockImg,
+ 'aria-hidden': 'true',
+ });
+ });
+
+ it('shows slot', () => {
+ createComponent({
+ slots: {
+ default: mockSlot,
+ },
+ });
+
+ expect(wrapper.html()).toContain(mockSlot);
+ });
+
+ describe('with no image', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { value: null, image: null },
+ });
+ });
+
+ it('shows no image', () => {
+ expect(findImg().exists()).toBe(false);
+ });
+ });
+ });
+
+ describe('when selected', () => {
+ beforeEach(() => {
+ createComponent({
+ props: { checked: mockValue },
+ });
+ });
+
+ it('highlights the item', () => {
+ expect(wrapper.classes('gl-bg-blue-50')).toBe(true);
+ expect(wrapper.classes('gl-border-blue-500')).toBe(true);
+ });
+
+ it('shows radio option as selected', () => {
+ expect(findFormRadio().attributes('value')).toBe(mockValue);
+ expect(findFormRadio().props('checked')).toBe(mockValue);
+ });
+ });
+});
diff --git a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
index 3dce5a509ca..b7d9d3ad23e 100644
--- a/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
+++ b/spec/frontend/ci/runner/components/search_tokens/tag_token_spec.js
@@ -5,7 +5,7 @@ import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import TagToken, { TAG_SUGGESTIONS_PATH } from '~/ci/runner/components/search_tokens/tag_token.vue';
import { OPERATORS_IS } from '~/vue_shared/components/filtered_search_bar/constants';
import { getRecentlyUsedSuggestions } from '~/vue_shared/components/filtered_search_bar/filtered_search_utils';
@@ -80,10 +80,10 @@ describe('TagToken', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(200, mockTags);
+ mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(HTTP_STATUS_OK, mockTags);
mock
.onGet(TAG_SUGGESTIONS_PATH, { params: { search: mockSearchTerm } })
- .reply(200, mockTagsFiltered);
+ .reply(HTTP_STATUS_OK, mockTagsFiltered);
getRecentlyUsedSuggestions.mockReturnValue([]);
});
@@ -163,7 +163,9 @@ describe('TagToken', () => {
describe('when suggestions cannot be loaded', () => {
beforeEach(async () => {
- mock.onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } }).reply(500);
+ mock
+ .onGet(TAG_SUGGESTIONS_PATH, { params: { search: '' } })
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
await waitForPromises();
diff --git a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
index c6c3f3b7040..2ad31dea774 100644
--- a/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
+++ b/spec/frontend/ci/runner/group_runner_show/group_runner_show_app_spec.js
@@ -1,4 +1,5 @@
import Vue from 'vue';
+import VueRouter from 'vue-router';
import VueApollo from 'vue-apollo';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -12,6 +13,9 @@ import RunnerDetails from '~/ci/runner/components/runner_details.vue';
import RunnerPauseButton from '~/ci/runner/components/runner_pause_button.vue';
import RunnerDeleteButton from '~/ci/runner/components/runner_delete_button.vue';
import RunnerEditButton from '~/ci/runner/components/runner_edit_button.vue';
+import RunnerDetailsTabs from '~/ci/runner/components/runner_details_tabs.vue';
+import RunnersJobs from '~/ci/runner/components/runner_jobs.vue';
+
import runnerQuery from '~/ci/runner/graphql/show/runner.query.graphql';
import GroupRunnerShowApp from '~/ci/runner/group_runner_show/group_runner_show_app.vue';
import { captureException } from '~/ci/runner/sentry_utils';
@@ -31,6 +35,7 @@ const mockRunnersPath = '/groups/group1/-/runners';
const mockEditGroupRunnerPath = `/groups/group1/-/runners/${mockRunnerId}/edit`;
Vue.use(VueApollo);
+Vue.use(VueRouter);
describe('GroupRunnerShowApp', () => {
let wrapper;
@@ -41,6 +46,8 @@ describe('GroupRunnerShowApp', () => {
const findRunnerDeleteButton = () => wrapper.findComponent(RunnerDeleteButton);
const findRunnerEditButton = () => wrapper.findComponent(RunnerEditButton);
const findRunnerPauseButton = () => wrapper.findComponent(RunnerPauseButton);
+ const findRunnerDetailsTabs = () => wrapper.findComponent(RunnerDetailsTabs);
+ const findRunnersJobs = () => wrapper.findComponent(RunnersJobs);
const mockRunnerQueryResult = (runner = {}) => {
mockRunnerQuery = jest.fn().mockResolvedValue({
@@ -81,16 +88,23 @@ describe('GroupRunnerShowApp', () => {
expect(mockRunnerQuery).toHaveBeenCalledWith({ id: mockRunnerGraphqlId });
});
- it('displays the header', async () => {
+ it('displays the runner header', () => {
expect(findRunnerHeader().text()).toContain(`Runner #${mockRunnerId}`);
});
- it('displays edit, pause, delete buttons', async () => {
- expect(findRunnerEditButton().exists()).toBe(true);
+ it('displays the runner edit and pause buttons', async () => {
+ expect(findRunnerEditButton().attributes('href')).toBe(mockEditGroupRunnerPath);
expect(findRunnerPauseButton().exists()).toBe(true);
expect(findRunnerDeleteButton().exists()).toBe(true);
});
+ it('shows runner details', () => {
+ expect(findRunnerDetailsTabs().props()).toEqual({
+ runner: mockRunner,
+ showAccessHelp: true,
+ });
+ });
+
it('shows basic runner details', () => {
const expected = `Description My Runner
Last contact Never contacted
@@ -104,17 +118,12 @@ describe('GroupRunnerShowApp', () => {
Token expiry
Runner authentication token expiration
Runner authentication tokens will expire based on a set interval.
- They will automatically rotate once expired. Learn more
- Never expires
+ They will automatically rotate once expired. Learn more Never expires
Tags None`.replace(/\s+/g, ' ');
expect(wrapper.text().replace(/\s+/g, ' ')).toContain(expected);
});
- it('renders runner details component', () => {
- expect(findRunnerDetails().props('runner')).toEqual(mockRunner);
- });
-
describe('when runner cannot be updated', () => {
beforeEach(async () => {
mockRunnerQueryResult({
@@ -129,7 +138,7 @@ describe('GroupRunnerShowApp', () => {
});
});
- it('does not display edit and pause buttons', () => {
+ it('does not display the runner edit and pause buttons', () => {
expect(findRunnerEditButton().exists()).toBe(false);
expect(findRunnerPauseButton().exists()).toBe(false);
});
@@ -153,7 +162,7 @@ describe('GroupRunnerShowApp', () => {
});
});
- it('does not display delete button', () => {
+ it('does not display the delete button', () => {
expect(findRunnerDeleteButton().exists()).toBe(false);
});
@@ -187,8 +196,17 @@ describe('GroupRunnerShowApp', () => {
mockRunnerQueryResult();
createComponent();
+
expect(findRunnerDetails().exists()).toBe(false);
});
+
+ it('does not show runner jobs', () => {
+ mockRunnerQueryResult();
+
+ createComponent();
+
+ expect(findRunnersJobs().exists()).toBe(false);
+ });
});
describe('When there is an error', () => {
diff --git a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
index 1e5bb828dbf..39ea5cade28 100644
--- a/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
+++ b/spec/frontend/ci/runner/group_runners/group_runners_app_spec.js
@@ -25,6 +25,7 @@ import RunnerActionsCell from '~/ci/runner/components/cells/runner_actions_cell.
import RegistrationDropdown from '~/ci/runner/components/registration/registration_dropdown.vue';
import RunnerPagination from '~/ci/runner/components/runner_pagination.vue';
import RunnerMembershipToggle from '~/ci/runner/components/runner_membership_toggle.vue';
+import RunnerJobStatusBadge from '~/ci/runner/components/runner_job_status_badge.vue';
import {
CREATED_ASC,
@@ -35,6 +36,7 @@ import {
I18N_STATUS_STALE,
INSTANCE_TYPE,
GROUP_TYPE,
+ JOBS_ROUTE_PATH,
PARAM_KEY_PAUSED,
PARAM_KEY_STATUS,
PARAM_KEY_TAG,
@@ -112,7 +114,6 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersCount,
...props,
},
provide: {
@@ -254,7 +255,7 @@ describe('GroupRunnersApp', () => {
let showToast;
const { webUrl, editUrl, node } = mockGroupRunnersEdges[0];
- const { id: graphqlId, shortSha } = node;
+ const { id: graphqlId, shortSha, jobExecutionStatus } = node;
const id = getIdFromGraphQLId(graphqlId);
const COUNT_QUERIES = 6; // Smart queries that display a filtered count of runners
const FILTERED_COUNT_QUERIES = 6; // Smart queries that display a count of runners in tabs and single stats
@@ -264,6 +265,13 @@ describe('GroupRunnersApp', () => {
showToast = jest.spyOn(wrapper.vm.$root.$toast, 'show');
});
+ it('Shows job status and links to jobs', () => {
+ const badge = findRunnerRow(id).findByTestId('td-status').findComponent(RunnerJobStatusBadge);
+
+ expect(badge.props('jobStatus')).toBe(jobExecutionStatus);
+ expect(badge.attributes('href')).toBe(`${webUrl}#${JOBS_ROUTE_PATH}`);
+ });
+
it('view link is displayed correctly', () => {
const viewLink = findRunnerRow(id).findByTestId('td-summary').findComponent(GlLink);
@@ -466,7 +474,6 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: mockRegistrationToken,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersCount,
},
});
});
@@ -482,7 +489,6 @@ describe('GroupRunnersApp', () => {
propsData: {
registrationToken: null,
groupFullPath: mockGroupFullPath,
- groupRunnersLimitedCount: mockGroupRunnersCount,
},
});
});
diff --git a/spec/frontend/ci/runner/mock_data.js b/spec/frontend/ci/runner/mock_data.js
index 525756ed513..5cdf0ea4e3b 100644
--- a/spec/frontend/ci/runner/mock_data.js
+++ b/spec/frontend/ci/runner/mock_data.js
@@ -304,6 +304,7 @@ export const mockSearchExamples = [
export const onlineContactTimeoutSecs = 2 * 60 * 60;
export const staleTimeoutSecs = 7889238; // Ruby's `3.months`
+export const newRunnerPath = '/runners/new';
export const emptyStateSvgPath = 'emptyStateSvgPath.svg';
export const emptyStateFilteredSvgPath = 'emptyStateFilteredSvgPath.svg';
diff --git a/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap b/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap
new file mode 100644
index 00000000000..b2084e3a7de
--- /dev/null
+++ b/spec/frontend/ci_secure_files/components/metadata/__snapshots__/modal_spec.js.snap
@@ -0,0 +1,386 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Secure File Metadata Modal when a .cer file is supplied matches cer the snapshot 1`] = `
+<div
+ category="primary"
+ hide-footer=""
+>
+ <div
+ data-testid="slot-modal-title"
+ >
+ myfile.cer Metadata
+ </div>
+ <div
+ data-testid="slot-default"
+ >
+
+ <table
+ aria-busy="false"
+ aria-colcount="2"
+ class="table b-table gl-table"
+ role="table"
+ >
+ <!---->
+ <!---->
+ <thead
+ class=""
+ role="rowgroup"
+ >
+ <!---->
+ <tr
+ class=""
+ role="row"
+ >
+ <th
+ aria-colindex="1"
+ class="hidden"
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Item Name
+ </div>
+ </th>
+ <th
+ aria-colindex="2"
+ class="hidden"
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Item Data
+ </div>
+ </th>
+ </tr>
+ </thead>
+ <tbody
+ role="rowgroup"
+ >
+ <!---->
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Name
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ Apple Distribution: Team Name (ABC123XYZ)
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Serial
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ 33669367788748363528491290218354043267
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Team
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ Team Name (ABC123XYZ)
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Issuer
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ Apple Worldwide Developer Relations Certification Authority - G3
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Expires at
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ April 26, 2022 at 7:20:40 PM GMT
+
+ </td>
+ </tr>
+ <!---->
+ <!---->
+ </tbody>
+ <!---->
+ </table>
+ </div>
+</div>
+`;
+
+exports[`Secure File Metadata Modal when a .mobileprovision file is supplied matches the mobileprovision snapshot 1`] = `
+<div
+ category="primary"
+ hide-footer=""
+>
+ <div
+ data-testid="slot-modal-title"
+ >
+ sample.mobileprovision Metadata
+ </div>
+ <div
+ data-testid="slot-default"
+ >
+
+ <table
+ aria-busy="false"
+ aria-colcount="2"
+ class="table b-table gl-table"
+ role="table"
+ >
+ <!---->
+ <!---->
+ <thead
+ class=""
+ role="rowgroup"
+ >
+ <!---->
+ <tr
+ class=""
+ role="row"
+ >
+ <th
+ aria-colindex="1"
+ class="hidden"
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Item Name
+ </div>
+ </th>
+ <th
+ aria-colindex="2"
+ class="hidden"
+ role="columnheader"
+ scope="col"
+ >
+ <div>
+ Item Data
+ </div>
+ </th>
+ </tr>
+ </thead>
+ <tbody
+ role="rowgroup"
+ >
+ <!---->
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ UUID
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ 6b9fcce1-b9a9-4b37-b2ce-ec4da2044abf
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Platforms
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ iOS
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Team
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ Team Name (ABC123XYZ)
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ App
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ iOS Demo - match Development com.gitlab.ios-demo
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Certificates
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ 33669367788748363528491290218354043267
+
+ </td>
+ </tr>
+ <tr
+ class=""
+ role="row"
+ >
+ <td
+ aria-colindex="1"
+ class=""
+ role="cell"
+ >
+ <strong>
+ Expires at
+ </strong>
+ </td>
+ <td
+ aria-colindex="2"
+ class=""
+ role="cell"
+ >
+
+ August 1, 2023 at 11:15:13 PM GMT
+
+ </td>
+ </tr>
+ <!---->
+ <!---->
+ </tbody>
+ <!---->
+ </table>
+ </div>
+</div>
+`;
diff --git a/spec/frontend/ci_secure_files/components/metadata/button_spec.js b/spec/frontend/ci_secure_files/components/metadata/button_spec.js
new file mode 100644
index 00000000000..4ac5b3325d4
--- /dev/null
+++ b/spec/frontend/ci_secure_files/components/metadata/button_spec.js
@@ -0,0 +1,49 @@
+import { GlButton } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import Button from '~/ci_secure_files/components/metadata/button.vue';
+import { secureFiles } from '../../mock_data';
+
+const secureFileWithoutMetadata = secureFiles[0];
+const secureFileWithMetadata = secureFiles[2];
+const modalId = 'metadataModalId';
+
+describe('Secure File Metadata Button', () => {
+ let wrapper;
+
+ const findButton = () => wrapper.findComponent(GlButton);
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const createWrapper = (secureFile = {}, admin = false) => {
+ wrapper = mount(Button, {
+ propsData: {
+ admin,
+ modalId,
+ secureFile,
+ },
+ });
+ };
+
+ describe('metadata button visibility', () => {
+ it.each`
+ visibility | admin | fileName
+ ${true} | ${true} | ${secureFileWithMetadata}
+ ${false} | ${false} | ${secureFileWithMetadata}
+ ${false} | ${false} | ${secureFileWithoutMetadata}
+ ${false} | ${false} | ${secureFileWithoutMetadata}
+ `(
+ 'button visibility is $visibility when admin equals $admin and $fileName.name is suppled',
+ ({ visibility, admin, fileName }) => {
+ createWrapper(fileName, admin);
+ expect(findButton().exists()).toBe(visibility);
+
+ if (visibility) {
+ expect(findButton().isVisible()).toBe(true);
+ expect(findButton().attributes('aria-label')).toBe('View File Metadata');
+ }
+ },
+ );
+ });
+});
diff --git a/spec/frontend/ci_secure_files/components/metadata/modal_spec.js b/spec/frontend/ci_secure_files/components/metadata/modal_spec.js
new file mode 100644
index 00000000000..230507d32d7
--- /dev/null
+++ b/spec/frontend/ci_secure_files/components/metadata/modal_spec.js
@@ -0,0 +1,78 @@
+import { GlModal } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
+
+import Modal from '~/ci_secure_files/components/metadata/modal.vue';
+
+import { secureFiles } from '../../mock_data';
+
+const cerFile = secureFiles[2];
+const mobileprovisionFile = secureFiles[3];
+const modalId = 'metadataModalId';
+
+describe('Secure File Metadata Modal', () => {
+ let wrapper;
+ let trackingSpy;
+
+ const createWrapper = (secureFile = {}) => {
+ wrapper = mount(Modal, {
+ stubs: {
+ GlModal: stubComponent(GlModal, {
+ template: RENDER_ALL_SLOTS_TEMPLATE,
+ }),
+ },
+ propsData: {
+ modalId,
+ name: secureFile.name,
+ metadata: secureFile.metadata,
+ fileExtension: secureFile.file_extension,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, undefined, jest.spyOn);
+ });
+
+ afterEach(() => {
+ unmockTracking();
+ wrapper.destroy();
+ });
+
+ describe('when a .cer file is supplied', () => {
+ it('matches cer the snapshot', () => {
+ createWrapper(cerFile);
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('when a .mobileprovision file is supplied', () => {
+ it('matches the mobileprovision snapshot', () => {
+ createWrapper(mobileprovisionFile);
+ expect(wrapper.element).toMatchSnapshot();
+ });
+ });
+
+ describe('event tracking', () => {
+ it('sends cer tracking information when the modal is loaded', () => {
+ createWrapper(cerFile);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, 'load_secure_file_metadata_cer', {});
+ expect(trackingSpy).not.toHaveBeenCalledWith(
+ undefined,
+ 'load_secure_file_metadata_mobileprovision',
+ {},
+ );
+ });
+
+ it('sends mobileprovision tracking information when the modal is loaded', () => {
+ createWrapper(mobileprovisionFile);
+ expect(trackingSpy).toHaveBeenCalledWith(
+ undefined,
+ 'load_secure_file_metadata_mobileprovision',
+ {},
+ );
+ expect(trackingSpy).not.toHaveBeenCalledWith(undefined, 'load_secure_file_metadata_cer', {});
+ });
+ });
+});
diff --git a/spec/frontend/ci_secure_files/components/secure_files_list_spec.js b/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
index 5273aafbb04..ab6200ca6f4 100644
--- a/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
+++ b/spec/frontend/ci_secure_files/components/secure_files_list_spec.js
@@ -2,6 +2,7 @@ import { GlLoadingIcon, GlModal } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import SecureFilesList from '~/ci_secure_files/components/secure_files_list.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
@@ -64,7 +65,7 @@ describe('SecureFilesList', () => {
describe('when secure files exist in a project', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles);
createWrapper();
await waitForPromises();
@@ -116,7 +117,7 @@ describe('SecureFilesList', () => {
describe('when no secure files exist in a project', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, []);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, []);
createWrapper();
await waitForPromises();
@@ -137,7 +138,7 @@ describe('SecureFilesList', () => {
describe('pagination', () => {
it('displays the pagination component with there are more than 20 items', async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles, { 'x-total': 30 });
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles, { 'x-total': 30 });
createWrapper();
await waitForPromises();
@@ -147,7 +148,7 @@ describe('SecureFilesList', () => {
it('does not display the pagination component with there are 20 items', async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles, { 'x-total': 20 });
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles, { 'x-total': 20 });
createWrapper();
await waitForPromises();
@@ -159,7 +160,7 @@ describe('SecureFilesList', () => {
describe('loading state', () => {
it('displays the loading icon while waiting for the backend request', () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles);
createWrapper();
expect(findLoadingIcon().exists()).toBe(true);
@@ -167,7 +168,7 @@ describe('SecureFilesList', () => {
it('does not display the loading icon after the backend request has completed', async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles);
createWrapper();
await waitForPromises();
@@ -180,7 +181,7 @@ describe('SecureFilesList', () => {
describe('with admin permissions', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles);
createWrapper();
await waitForPromises();
@@ -198,7 +199,7 @@ describe('SecureFilesList', () => {
describe('without admin permissions', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
- mock.onGet(expectedUrl).reply(200, secureFiles);
+ mock.onGet(expectedUrl).reply(HTTP_STATUS_OK, secureFiles);
createWrapper(false);
await waitForPromises();
diff --git a/spec/frontend/ci_secure_files/mock_data.js b/spec/frontend/ci_secure_files/mock_data.js
index 5a9e16d1ad6..f532b468fb9 100644
--- a/spec/frontend/ci_secure_files/mock_data.js
+++ b/spec/frontend/ci_secure_files/mock_data.js
@@ -4,15 +4,72 @@ export const secureFiles = [
name: 'myfile.jks',
checksum: '16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aac',
checksum_algorithm: 'sha256',
- permissions: 'read_only',
created_at: '2022-02-22T22:22:22.222Z',
+ file_extension: 'jks',
+ metadata: null,
},
{
id: 2,
name: 'myotherfile.jks',
checksum: '16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aa2',
checksum_algorithm: 'sha256',
- permissions: 'execute',
created_at: '2022-02-22T22:22:22.222Z',
+ file_extension: 'jks',
+ metadata: null,
+ },
+ {
+ id: 3,
+ name: 'myfile.cer',
+ checksum: '16630b189ab34b2e3504f4758e1054d2e478deda510b2b08cc0ef38d12e80aa2',
+ checksum_algorithm: 'sha256',
+ created_at: '2022-02-22T22:22:22.222Z',
+ file_extension: 'cer',
+ expires_at: '2022-04-26T19:20:40.000Z',
+ metadata: {
+ id: '33669367788748363528491290218354043267',
+ issuer: {
+ C: 'US',
+ O: 'Apple Inc.',
+ CN: 'Apple Worldwide Developer Relations Certification Authority',
+ OU: 'G3',
+ },
+ subject: {
+ C: 'US',
+ O: 'Team Name',
+ CN: 'Apple Distribution: Team Name (ABC123XYZ)',
+ OU: 'ABC123XYZ',
+ UID: 'ABC123XYZ',
+ },
+ expires_at: '2022-04-26T19:20:40.000Z',
+ },
+ },
+ {
+ id: 4,
+ name: 'sample.mobileprovision',
+ checksum: '9e194bbde00d57c64b6640ed2c9e166d76b4c79d9dbd49770f95be56678f2a62',
+ checksum_algorithm: 'sha256',
+ created_at: '2022-11-15T19:29:57.577Z',
+ expires_at: '2023-08-01T23:15:13.000Z',
+ metadata: {
+ id: '6b9fcce1-b9a9-4b37-b2ce-ec4da2044abf',
+ app_id: 'match Development com.gitlab.ios-demo',
+ devices: ['00008101-001454860C10001E'],
+ team_id: ['ABC123XYZ'],
+ app_name: 'iOS Demo',
+ platforms: ['iOS'],
+ team_name: 'Team Name',
+ expires_at: '2023-08-01T18:15:13.000-05:00',
+ entitlements: {
+ 'get-task-allow': true,
+ 'application-identifier': 'N7SYAN8PX8.com.gitlab.ios-demo',
+ 'keychain-access-groups': ['ABC123XYZ.*', 'com.apple.token'],
+ 'com.apple.developer.game-center': true,
+ 'com.apple.developer.team-identifier': 'ABC123XYZ',
+ },
+ app_id_prefix: ['ABC123XYZ'],
+ xcode_managed: false,
+ certificate_ids: ['33669367788748363528491290218354043267'],
+ },
+ file_extension: 'mobileprovision',
},
];
diff --git a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
index 01eb08f4ece..f1df4208fa2 100644
--- a/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
+++ b/spec/frontend/ci_settings_pipeline_triggers/components/triggers_list_spec.js
@@ -1,6 +1,6 @@
import { GlTable, GlBadge } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import TriggersList from '~/ci_settings_pipeline_triggers/components/triggers_list.vue';
import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -11,7 +11,7 @@ describe('TriggersList', () => {
let wrapper;
const createComponent = (props = {}) => {
- wrapper = mount(TriggersList, {
+ wrapper = mountExtended(TriggersList, {
propsData: { triggers, ...props },
});
};
@@ -25,59 +25,75 @@ describe('TriggersList', () => {
const findInvalidBadge = (i) => findCell(i, 0).findComponent(GlBadge);
const findEditBtn = (i) => findRowAt(i).find('[data-testid="edit-btn"]');
const findRevokeBtn = (i) => findRowAt(i).find('[data-testid="trigger_revoke_button"]');
+ const findRevealHideButton = () => wrapper.findByTestId('reveal-hide-values-button');
- beforeEach(async () => {
- createComponent();
+ describe('With triggers set', () => {
+ beforeEach(async () => {
+ createComponent();
- await nextTick();
- });
+ await nextTick();
+ });
- it('displays a table with expected headers', () => {
- const headers = ['Token', 'Description', 'Owner', 'Last Used', ''];
- headers.forEach((header, i) => {
- expect(findHeaderAt(i).text()).toBe(header);
+ it('displays a table with expected headers', () => {
+ const headers = ['Token', 'Description', 'Owner', 'Last Used', ''];
+ headers.forEach((header, i) => {
+ expect(findHeaderAt(i).text()).toBe(header);
+ });
});
- });
- it('displays a table with rows', () => {
- expect(findRows()).toHaveLength(triggers.length);
+ it('displays a "Reveal/Hide values" button', async () => {
+ const revealHideButton = findRevealHideButton();
- const [trigger] = triggers;
+ expect(revealHideButton.exists()).toBe(true);
+ expect(revealHideButton.text()).toBe('Reveal values');
- expect(findCell(0, 0).text()).toBe(trigger.token);
- expect(findCell(0, 1).text()).toBe(trigger.description);
- expect(findCell(0, 2).text()).toContain(trigger.owner.name);
- });
+ await revealHideButton.vm.$emit('click');
- it('displays a "copy to cliboard" button for exposed tokens', () => {
- expect(findClipboardBtn(0).exists()).toBe(true);
- expect(findClipboardBtn(0).props('text')).toBe(triggers[0].token);
+ expect(revealHideButton.text()).toBe('Hide values');
+ });
- expect(findClipboardBtn(1).exists()).toBe(false);
- });
+ it('displays a table with rows', async () => {
+ await findRevealHideButton().vm.$emit('click');
- it('displays an "invalid" label for tokens without access', () => {
- expect(findInvalidBadge(0).exists()).toBe(false);
+ expect(findRows()).toHaveLength(triggers.length);
- expect(findInvalidBadge(1).exists()).toBe(true);
- });
+ const [trigger] = triggers;
- it('displays a time ago label when last used', () => {
- expect(findCell(0, 3).text()).toBe('Never');
+ expect(findCell(0, 0).text()).toBe(trigger.token);
+ expect(findCell(0, 1).text()).toBe(trigger.description);
+ expect(findCell(0, 2).text()).toContain(trigger.owner.name);
+ });
- expect(findCell(1, 3).findComponent(TimeAgoTooltip).props('time')).toBe(triggers[1].lastUsed);
- });
+ it('displays a "copy to cliboard" button for exposed tokens', () => {
+ expect(findClipboardBtn(0).exists()).toBe(true);
+ expect(findClipboardBtn(0).props('text')).toBe(triggers[0].token);
- it('displays actions in a rows', () => {
- const [data] = triggers;
- const confirmWarning =
- 'By revoking a trigger you will break any processes making use of it. Are you sure?';
+ expect(findClipboardBtn(1).exists()).toBe(false);
+ });
+
+ it('displays an "invalid" label for tokens without access', () => {
+ expect(findInvalidBadge(0).exists()).toBe(false);
- expect(findEditBtn(0).attributes('href')).toBe(data.editProjectTriggerPath);
+ expect(findInvalidBadge(1).exists()).toBe(true);
+ });
- expect(findRevokeBtn(0).attributes('href')).toBe(data.projectTriggerPath);
- expect(findRevokeBtn(0).attributes('data-method')).toBe('delete');
- expect(findRevokeBtn(0).attributes('data-confirm')).toBe(confirmWarning);
+ it('displays a time ago label when last used', () => {
+ expect(findCell(0, 3).text()).toBe('Never');
+
+ expect(findCell(1, 3).findComponent(TimeAgoTooltip).props('time')).toBe(triggers[1].lastUsed);
+ });
+
+ it('displays actions in a rows', () => {
+ const [data] = triggers;
+ const confirmWarning =
+ 'By revoking a trigger you will break any processes making use of it. Are you sure?';
+
+ expect(findEditBtn(0).attributes('href')).toBe(data.editProjectTriggerPath);
+
+ expect(findRevokeBtn(0).attributes('href')).toBe(data.projectTriggerPath);
+ expect(findRevokeBtn(0).attributes('data-method')).toBe('delete');
+ expect(findRevokeBtn(0).attributes('data-confirm')).toBe(confirmWarning);
+ });
});
describe('when there are no triggers set', () => {
diff --git a/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js b/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js
index 2af64191a88..db1219ccb41 100644
--- a/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js
+++ b/spec/frontend/clusters/agents/components/agent_integration_status_row_spec.js
@@ -31,7 +31,7 @@ describe('IntegrationStatus', () => {
describe('icon', () => {
const icon = 'status-success';
- const iconClass = 'text-success-500';
+ const iconClass = 'gl-text-green-500';
it.each`
props | iconName | iconClassName
${{ icon, iconClass }} | ${icon} | ${iconClass}
diff --git a/spec/frontend/clusters/clusters_bundle_spec.js b/spec/frontend/clusters/clusters_bundle_spec.js
index ad2aa4acbaf..a2ec19c5b4a 100644
--- a/spec/frontend/clusters/clusters_bundle_spec.js
+++ b/spec/frontend/clusters/clusters_bundle_spec.js
@@ -3,10 +3,11 @@ import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import Clusters from '~/clusters/clusters_bundle';
import axios from '~/lib/utils/axios_utils';
-import initProjectSelectDropdown from '~/project_select';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { initProjectSelects } from '~/vue_shared/components/entity_select/init_project_selects';
jest.mock('~/lib/utils/poll');
-jest.mock('~/project_select');
+jest.mock('~/vue_shared/components/entity_select/init_project_selects');
useMockLocationHelper();
@@ -19,7 +20,7 @@ describe('Clusters', () => {
mock = new MockAdapter(axios);
- mock.onGet(statusPath).reply(200);
+ mock.onGet(statusPath).reply(HTTP_STATUS_OK);
};
beforeEach(() => {
@@ -48,7 +49,7 @@ describe('Clusters', () => {
});
it('should call initProjectSelectDropdown on construct', () => {
- expect(initProjectSelectDropdown).toHaveBeenCalled();
+ expect(initProjectSelects).toHaveBeenCalled();
});
});
diff --git a/spec/frontend/clusters_list/components/clusters_spec.js b/spec/frontend/clusters_list/components/clusters_spec.js
index e8e705a6384..20dbff9df15 100644
--- a/spec/frontend/clusters_list/components/clusters_spec.js
+++ b/spec/frontend/clusters_list/components/clusters_spec.js
@@ -7,6 +7,7 @@ import Clusters from '~/clusters_list/components/clusters.vue';
import ClustersEmptyState from '~/clusters_list/components/clusters_empty_state.vue';
import ClusterStore from '~/clusters_list/store';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { apiData } from '../mock_data';
describe('Clusters', () => {
@@ -68,7 +69,7 @@ describe('Clusters', () => {
captureException = jest.spyOn(Sentry, 'captureException');
mock = new MockAdapter(axios);
- mockPollingApi(200, apiData, paginationHeader());
+ mockPollingApi(HTTP_STATUS_OK, apiData, paginationHeader());
return createWrapper({});
});
@@ -255,7 +256,7 @@ describe('Clusters', () => {
const totalSecondPage = 500;
beforeEach(() => {
- mockPollingApi(200, apiData, paginationHeader(totalFirstPage, perPage, 1));
+ mockPollingApi(HTTP_STATUS_OK, apiData, paginationHeader(totalFirstPage, perPage, 1));
return createWrapper({});
});
@@ -269,7 +270,7 @@ describe('Clusters', () => {
describe('when updating currentPage', () => {
beforeEach(() => {
- mockPollingApi(200, apiData, paginationHeader(totalSecondPage, perPage, 2));
+ mockPollingApi(HTTP_STATUS_OK, apiData, paginationHeader(totalSecondPage, perPage, 2));
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
// eslint-disable-next-line no-restricted-syntax
wrapper.setData({ currentPage: 2 });
diff --git a/spec/frontend/clusters_list/store/actions_spec.js b/spec/frontend/clusters_list/store/actions_spec.js
index 1deebf8b75a..360fd3b2842 100644
--- a/spec/frontend/clusters_list/store/actions_spec.js
+++ b/spec/frontend/clusters_list/store/actions_spec.js
@@ -7,6 +7,7 @@ import * as actions from '~/clusters_list/store/actions';
import * as types from '~/clusters_list/store/mutation_types';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { apiData } from '../mock_data';
@@ -65,7 +66,7 @@ describe('Clusters store actions', () => {
afterEach(() => mock.restore());
it('should commit SET_CLUSTERS_DATA with received response', () => {
- mock.onGet().reply(200, apiData, headers);
+ mock.onGet().reply(HTTP_STATUS_OK, apiData, headers);
return testAction(
actions.fetchClusters,
@@ -81,7 +82,7 @@ describe('Clusters store actions', () => {
});
it('should show flash on API error', async () => {
- mock.onGet().reply(400, 'Not Found');
+ mock.onGet().reply(HTTP_STATUS_BAD_REQUEST, 'Not Found');
await testAction(
actions.fetchClusters,
@@ -118,7 +119,7 @@ describe('Clusters store actions', () => {
pollRequest = jest.spyOn(Poll.prototype, 'makeRequest');
pollStop = jest.spyOn(Poll.prototype, 'stop');
- mock.onGet().reply(200, apiData, pollHeaders);
+ mock.onGet().reply(HTTP_STATUS_OK, apiData, pollHeaders);
});
afterEach(() => {
@@ -171,7 +172,7 @@ describe('Clusters store actions', () => {
it('should stop polling and report to Sentry when data is invalid', async () => {
const badApiResponse = { clusters: {} };
- mock.onGet().reply(200, badApiResponse, pollHeaders);
+ mock.onGet().reply(HTTP_STATUS_OK, badApiResponse, pollHeaders);
await testAction(
actions.fetchClusters,
diff --git a/spec/frontend/code_navigation/store/actions_spec.js b/spec/frontend/code_navigation/store/actions_spec.js
index 8eee61d1342..ab5d7fce905 100644
--- a/spec/frontend/code_navigation/store/actions_spec.js
+++ b/spec/frontend/code_navigation/store/actions_spec.js
@@ -4,6 +4,7 @@ import testAction from 'helpers/vuex_action_helper';
import actions from '~/code_navigation/store/actions';
import { setCurrentHoverElement, addInteractionClass } from '~/code_navigation/utils';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/code_navigation/utils');
@@ -45,7 +46,7 @@ describe('Code navigation actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(codeNavigationPath).replyOnce(200, [
+ mock.onGet(codeNavigationPath).replyOnce(HTTP_STATUS_OK, [
{
start_line: 0,
start_char: 0,
@@ -124,7 +125,7 @@ describe('Code navigation actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(codeNavigationPath).replyOnce(500);
+ mock.onGet(codeNavigationPath).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches requestDataError', () => {
diff --git a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
index 16737003fa0..debd10de118 100644
--- a/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
+++ b/spec/frontend/commit/commit_box_pipeline_mini_graph_spec.js
@@ -141,6 +141,16 @@ describe('Commit box pipeline mini graph', () => {
expect(upstreamPipeline).toEqual(null);
});
+ it('should render the latest downstream pipeline only', async () => {
+ createComponent(downstreamHandler);
+
+ await waitForPromises();
+
+ const downstreamPipelines = findPipelineMiniGraph().props('downstreamPipelines');
+
+ expect(downstreamPipelines).toHaveLength(1);
+ });
+
it('should pass the pipeline path prop for the counter badge', async () => {
createComponent(downstreamHandler);
@@ -173,7 +183,14 @@ describe('Commit box pipeline mini graph', () => {
const upstreamPipeline = findPipelineMiniGraph().props('upstreamPipeline');
expect(upstreamPipeline).toEqual(samplePipeline);
- expect(downstreamPipelines).toEqual(expect.arrayContaining([samplePipeline]));
+ expect(downstreamPipelines).toEqual(
+ expect.arrayContaining([
+ {
+ ...samplePipeline,
+ sourceJob: expect.any(Object),
+ },
+ ]),
+ );
});
});
diff --git a/spec/frontend/commit/mock_data.js b/spec/frontend/commit/mock_data.js
index aef137e6fa5..a13ef9c563e 100644
--- a/spec/frontend/commit/mock_data.js
+++ b/spec/frontend/commit/mock_data.js
@@ -1,3 +1,90 @@
+export const mockDownstreamPipelinesGraphql = ({ includeSourceJobRetried = true } = {}) => ({
+ nodes: [
+ {
+ id: 'gid://gitlab/Ci::Pipeline/612',
+ path: '/root/job-log-sections/-/pipelines/612',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-612-612',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/532',
+ retried: includeSourceJobRetried ? false : null,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/611',
+ path: '/root/job-log-sections/-/pipelines/611',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-611-611',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/531',
+ retried: includeSourceJobRetried ? true : null,
+ },
+ __typename: 'Pipeline',
+ },
+ {
+ id: 'gid://gitlab/Ci::Pipeline/609',
+ path: '/root/job-log-sections/-/pipelines/609',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'job-log-sections',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-609-609',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ sourceJob: {
+ id: 'gid://gitlab/Ci::Bridge/530',
+ retried: includeSourceJobRetried ? true : null,
+ },
+ __typename: 'Pipeline',
+ },
+ ],
+ __typename: 'PipelineConnection',
+});
+
+const upstream = {
+ id: 'gid://gitlab/Ci::Pipeline/610',
+ path: '/root/trigger-downstream/-/pipelines/610',
+ project: {
+ id: 'gid://gitlab/Project/21',
+ name: 'trigger-downstream',
+ __typename: 'Project',
+ },
+ detailedStatus: {
+ id: 'success-610-610',
+ group: 'success',
+ icon: 'status_success',
+ label: 'passed',
+ __typename: 'DetailedStatus',
+ },
+ __typename: 'Pipeline',
+};
+
export const mockStages = [
{
name: 'build',
@@ -74,24 +161,7 @@ export const mockDownstreamQueryResponse = {
pipeline: {
path: '/root/ci-project/-/pipelines/790',
id: 'pipeline-1',
- downstream: {
- nodes: [
- {
- id: 'gid://gitlab/Ci::Pipeline/612',
- path: '/root/job-log-sections/-/pipelines/612',
- project: { id: '1', name: 'job-log-sections', __typename: 'Project' },
- detailedStatus: {
- id: 'status-1',
- group: 'success',
- icon: 'status_success',
- label: 'passed',
- __typename: 'DetailedStatus',
- },
- __typename: 'Pipeline',
- },
- ],
- __typename: 'PipelineConnection',
- },
+ downstream: mockDownstreamPipelinesGraphql(),
upstream: null,
},
__typename: 'Project',
@@ -106,37 +176,8 @@ export const mockUpstreamDownstreamQueryResponse = {
pipeline: {
id: 'pipeline-1',
path: '/root/ci-project/-/pipelines/790',
- downstream: {
- nodes: [
- {
- id: 'gid://gitlab/Ci::Pipeline/612',
- path: '/root/job-log-sections/-/pipelines/612',
- project: { id: '1', name: 'job-log-sections', __typename: 'Project' },
- detailedStatus: {
- id: 'status-1',
- group: 'success',
- icon: 'status_success',
- label: 'passed',
- __typename: 'DetailedStatus',
- },
- __typename: 'Pipeline',
- },
- ],
- __typename: 'PipelineConnection',
- },
- upstream: {
- id: 'gid://gitlab/Ci::Pipeline/610',
- path: '/root/trigger-downstream/-/pipelines/610',
- project: { id: '1', name: 'trigger-downstream', __typename: 'Project' },
- detailedStatus: {
- id: 'status-1',
- group: 'success',
- icon: 'status_success',
- label: 'passed',
- __typename: 'DetailedStatus',
- },
- __typename: 'Pipeline',
- },
+ downstream: mockDownstreamPipelinesGraphql(),
+ upstream,
},
__typename: 'Project',
},
@@ -154,19 +195,7 @@ export const mockUpstreamQueryResponse = {
nodes: [],
__typename: 'PipelineConnection',
},
- upstream: {
- id: 'gid://gitlab/Ci::Pipeline/610',
- path: '/root/trigger-downstream/-/pipelines/610',
- project: { id: '1', name: 'trigger-downstream', __typename: 'Project' },
- detailedStatus: {
- id: 'status-1',
- group: 'success',
- icon: 'status_success',
- label: 'passed',
- __typename: 'DetailedStatus',
- },
- __typename: 'Pipeline',
- },
+ upstream,
},
__typename: 'Project',
},
diff --git a/spec/frontend/commit/pipelines/pipelines_table_spec.js b/spec/frontend/commit/pipelines/pipelines_table_spec.js
index 6865b721441..4bffb6a0fd3 100644
--- a/spec/frontend/commit/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/commit/pipelines/pipelines_table_spec.js
@@ -10,6 +10,7 @@ import PipelinesTable from '~/commit/pipelines/pipelines_table.vue';
import {
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_OK,
HTTP_STATUS_UNAUTHORIZED,
} from '~/lib/utils/http_status';
import { createAlert } from '~/flash';
@@ -69,7 +70,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
describe('successful request', () => {
describe('without pipelines', () => {
beforeEach(async () => {
- mock.onGet('endpoint.json').reply(200, []);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, []);
createComponent();
@@ -96,7 +97,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
describe('with pipelines', () => {
beforeEach(async () => {
- mock.onGet('endpoint.json').reply(200, [pipeline], { 'x-total': 10 });
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipeline], { 'x-total': 10 });
createComponent();
@@ -168,7 +169,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
pipelineCopy.flags.detached_merge_request_pipeline = true;
pipelineCopy.flags.merge_request_pipeline = true;
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]);
createComponent();
@@ -184,7 +185,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
pipelineCopy.flags.detached_merge_request_pipeline = false;
pipelineCopy.flags.merge_request_pipeline = false;
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]);
createComponent();
@@ -199,7 +200,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
beforeEach(async () => {
pipelineCopy.flags.detached_merge_request_pipeline = true;
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]);
createComponent({
canRunPipeline: true,
@@ -277,7 +278,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
beforeEach(async () => {
pipelineCopy.flags.detached_merge_request_pipeline = true;
- mock.onGet('endpoint.json').reply(200, [pipelineCopy]);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, [pipelineCopy]);
createComponent({
projectId: '5',
@@ -309,7 +310,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
describe('when no pipelines were created on a forked merge request', () => {
beforeEach(async () => {
- mock.onGet('endpoint.json').reply(200, []);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_OK, []);
createComponent({
projectId: '5',
@@ -337,7 +338,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
describe('unsuccessfull request', () => {
beforeEach(async () => {
- mock.onGet('endpoint.json').reply(500, []);
+ mock.onGet('endpoint.json').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, []);
createComponent();
diff --git a/spec/frontend/commits_spec.js b/spec/frontend/commits_spec.js
index db1516ed4ec..c79170aa37e 100644
--- a/spec/frontend/commits_spec.js
+++ b/spec/frontend/commits_spec.js
@@ -4,6 +4,7 @@ import 'vendor/jquery.endless-scroll';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import CommitsList from '~/commits';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Pager from '~/pager';
describe('Commits List', () => {
@@ -64,7 +65,7 @@ describe('Commits List', () => {
jest.spyOn(window.history, 'replaceState').mockImplementation(() => {});
mock = new MockAdapter(axios);
- mock.onGet('/h5bp/html5-boilerplate/commits/main').reply(200, {
+ mock.onGet('/h5bp/html5-boilerplate/commits/main').reply(HTTP_STATUS_OK, {
html: '<li>Result</li>',
});
diff --git a/spec/frontend/confidential_merge_request/components/dropdown_spec.js b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
index 770f2636648..4d577fe1132 100644
--- a/spec/frontend/confidential_merge_request/components/dropdown_spec.js
+++ b/spec/frontend/confidential_merge_request/components/dropdown_spec.js
@@ -1,47 +1,79 @@
-import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
import Dropdown from '~/confidential_merge_request/components/dropdown.vue';
-let vm;
+const TEST_PROJECTS = [
+ {
+ id: 7,
+ name: 'test',
+ },
+ {
+ id: 9,
+ name: 'lorem ipsum',
+ },
+ {
+ id: 11,
+ name: 'dolar sit',
+ },
+];
-function factory(projects = []) {
- vm = mount(Dropdown, {
- propsData: {
- projects,
- selectedProject: projects[0],
- },
- });
-}
-
-describe('Confidential merge request project dropdown component', () => {
- afterEach(() => {
- vm.destroy();
- });
+describe('~/confidential_merge_request/components/dropdown.vue', () => {
+ let wrapper;
- it('renders dropdown items', () => {
- factory([
- {
- id: 1,
- name: 'test',
- },
- {
- id: 2,
- name: 'test',
+ function factory(props = {}) {
+ wrapper = shallowMount(Dropdown, {
+ propsData: {
+ projects: TEST_PROJECTS,
+ ...props,
},
- ]);
+ });
+ }
- expect(vm.findAllComponents(GlDropdownItem).length).toBe(2);
- });
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+
+ describe('default', () => {
+ beforeEach(() => {
+ factory();
+ });
+
+ it('renders collapsible listbox', () => {
+ expect(findListbox().props()).toMatchObject({
+ icon: 'lock',
+ selected: [],
+ toggleText: 'Select private project',
+ block: true,
+ items: TEST_PROJECTS.map(({ id, name }) => ({
+ value: String(id),
+ text: name,
+ })),
+ });
+ });
+
+ it('does not emit anything', () => {
+ expect(wrapper.emitted()).toEqual({});
+ });
- it('shows lock icon', () => {
- factory();
+ describe('when listbox emits selected', () => {
+ beforeEach(() => {
+ findListbox().vm.$emit('select', String(TEST_PROJECTS[1].id));
+ });
- expect(vm.findComponent(GlDropdown).props('icon')).toBe('lock');
+ it('emits selected project', () => {
+ expect(wrapper.emitted('select')).toEqual([[TEST_PROJECTS[1]]]);
+ });
+ });
});
- it('has dropdown text', () => {
- factory();
+ describe('with selected', () => {
+ beforeEach(() => {
+ factory({ selectedProject: TEST_PROJECTS[1] });
+ });
- expect(vm.findComponent(GlDropdown).props('text')).toBe('Select private project');
+ it('shows selected project', () => {
+ expect(findListbox().props()).toMatchObject({
+ selected: String(TEST_PROJECTS[1].id),
+ toggleText: TEST_PROJECTS[1].name,
+ });
+ });
});
});
diff --git a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
index 0e73d50fdb5..d6f16f1a644 100644
--- a/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
+++ b/spec/frontend/confidential_merge_request/components/project_form_group_spec.js
@@ -3,6 +3,7 @@ import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import ProjectFormGroup from '~/confidential_merge_request/components/project_form_group.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
const mockData = [
{
@@ -27,7 +28,7 @@ let mock;
function factory(projects = mockData) {
mock = new MockAdapter(axios);
- mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(200, projects);
+ mock.onGet(/api\/(.*)\/projects\/gitlab-org%2Fgitlab-ce\/forks/).reply(HTTP_STATUS_OK, projects);
wrapper = shallowMount(ProjectFormGroup, {
propsData: {
diff --git a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
index a23f8370adf..d4fc47601cf 100644
--- a/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
+++ b/spec/frontend/content_editor/components/toolbar_more_dropdown_spec.js
@@ -1,10 +1,9 @@
-import { GlDropdown } from '@gitlab/ui';
+import { GlDisclosureDropdown } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import ToolbarMoreDropdown from '~/content_editor/components/toolbar_more_dropdown.vue';
import Diagram from '~/content_editor/extensions/diagram';
import HorizontalRule from '~/content_editor/extensions/horizontal_rule';
import eventHubFactory from '~/helpers/event_hub_factory';
-import { stubComponent } from 'helpers/stub_component';
import { createTestEditor, mockChainedCommands, emitEditorEvent } from '../test_utils';
describe('content_editor/components/toolbar_more_dropdown', () => {
@@ -25,14 +24,11 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
tiptapEditor,
eventHub,
},
- stubs: {
- GlDropdown: stubComponent(GlDropdown),
- },
propsData,
});
};
- const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
beforeEach(() => {
buildEditor();
@@ -60,7 +56,7 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
beforeEach(async () => {
commands = mockChainedCommands(tiptapEditor, [command, 'focus', 'run']);
- btn = wrapper.findByRole('menuitem', { name });
+ btn = wrapper.findByRole('button', { name });
});
it(`inserts a ${contentType}`, async () => {
@@ -76,12 +72,11 @@ describe('content_editor/components/toolbar_more_dropdown', () => {
});
describe('a11y tests', () => {
- it('sets text, title, and text-sr-only properties to the table button dropdown', () => {
+ it('sets toggleText and text-sr-only properties to the table button dropdown', () => {
expect(findDropdown().props()).toMatchObject({
- text: 'More',
textSrOnly: true,
+ toggleText: 'More options',
});
- expect(findDropdown().attributes('title')).toBe('More');
});
});
});
diff --git a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
index 3f812d3cf4e..2f441f0f747 100644
--- a/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
+++ b/spec/frontend/contributors/component/__snapshots__/contributors_spec.js.snap
@@ -1,9 +1,47 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
-exports[`Contributors charts should render charts when loading completed and there is chart data 1`] = `
+exports[`Contributors charts should render charts and a RefSelector when loading completed and there is chart data 1`] = `
<div>
<div
- class="contributors-charts"
+ class="gl-border-b gl-border-gray-100 gl-mb-6 gl-bg-gray-10 gl-p-5"
+ >
+ <div
+ class="gl-display-flex"
+ >
+ <div
+ class="gl-mr-3"
+ >
+ <refselector-stub
+ enabledreftypes="REF_TYPE_BRANCHES,REF_TYPE_TAGS"
+ name=""
+ projectid="23"
+ state="true"
+ translations="[object Object]"
+ value="main"
+ />
+ </div>
+
+ <a
+ class="btn btn-default btn-md gl-button"
+ data-testid="history-button"
+ href="some/path"
+ >
+ <!---->
+
+ <!---->
+
+ <span
+ class="gl-button-text"
+ >
+ History
+
+ </span>
+ </a>
+ </div>
+ </div>
+
+ <div
+ data-testid="contributors-charts"
>
<h4
class="gl-mb-2 gl-mt-5"
@@ -49,8 +87,8 @@ exports[`Contributors charts should render charts when loading completed and the
class="gl-mb-3"
>
- 2 commits (jawnnypoo@gmail.com)
-
+ 2 commits (jawnnypoo@gmail.com)
+
</p>
<div>
diff --git a/spec/frontend/contributors/component/contributors_spec.js b/spec/frontend/contributors/component/contributors_spec.js
index 2f0b5719326..03b1e977548 100644
--- a/spec/frontend/contributors/component/contributors_spec.js
+++ b/spec/frontend/contributors/component/contributors_spec.js
@@ -1,41 +1,58 @@
-import { mount } from '@vue/test-utils';
-import { GlLoadingIcon } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import ContributorsCharts from '~/contributors/components/contributors.vue';
import { createStore } from '~/contributors/stores';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { visitUrl } from '~/lib/utils/url_utility';
+import RefSelector from '~/ref/components/ref_selector.vue';
+import { REF_TYPE_BRANCHES, REF_TYPE_TAGS } from '~/ref/constants';
+
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+}));
let wrapper;
let mock;
let store;
const Component = Vue.extend(ContributorsCharts);
-const endpoint = 'contributors';
+const endpoint = 'contributors/-/graphs';
const branch = 'main';
const chartData = [
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-05-05' },
{ author_name: 'John', author_email: 'jawnnypoo@gmail.com', date: '2019-03-03' },
];
+const projectId = '23';
+const commitsPath = 'some/path';
function factory() {
mock = new MockAdapter(axios);
jest.spyOn(axios, 'get');
- mock.onGet().reply(200, chartData);
+ mock.onGet().reply(HTTP_STATUS_OK, chartData);
store = createStore();
- wrapper = mount(Component, {
+ wrapper = mountExtended(Component, {
propsData: {
endpoint,
branch,
+ projectId,
+ commitsPath,
},
stubs: {
GlLoadingIcon: true,
GlAreaChart: true,
+ RefSelector: true,
},
store,
});
}
+const findLoadingIcon = () => wrapper.findByTestId('loading-app-icon');
+const findRefSelector = () => wrapper.findComponent(RefSelector);
+const findHistoryButton = () => wrapper.findByTestId('history-button');
+const findContributorsCharts = () => wrapper.findByTestId('contributors-charts');
+
describe('Contributors charts', () => {
beforeEach(() => {
factory();
@@ -53,15 +70,46 @@ describe('Contributors charts', () => {
it('should display loader whiled loading data', async () => {
wrapper.vm.$store.state.loading = true;
await nextTick();
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
+ expect(findLoadingIcon().exists()).toBe(true);
});
- it('should render charts when loading completed and there is chart data', async () => {
+ it('should render charts and a RefSelector when loading completed and there is chart data', async () => {
wrapper.vm.$store.state.loading = false;
wrapper.vm.$store.state.chartData = chartData;
await nextTick();
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
- expect(wrapper.find('.contributors-charts').exists()).toBe(true);
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findRefSelector().exists()).toBe(true);
+ expect(findRefSelector().props()).toMatchObject({
+ enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_TAGS],
+ value: branch,
+ projectId,
+ translations: { dropdownHeader: 'Switch branch/tag' },
+ useSymbolicRefNames: false,
+ state: true,
+ name: '',
+ });
+ expect(findContributorsCharts().exists()).toBe(true);
expect(wrapper.element).toMatchSnapshot();
});
+
+ it('should have a history button with a set href attribute', async () => {
+ wrapper.vm.$store.state.loading = false;
+ wrapper.vm.$store.state.chartData = chartData;
+ await nextTick();
+
+ const historyButton = findHistoryButton();
+ expect(historyButton.exists()).toBe(true);
+ expect(historyButton.attributes('href')).toBe(commitsPath);
+ });
+
+ it('visits a URL when clicking on a branch/tag', async () => {
+ wrapper.vm.$store.state.loading = false;
+ wrapper.vm.$store.state.chartData = chartData;
+ await nextTick();
+
+ findRefSelector().vm.$emit('input', branch);
+
+ expect(visitUrl).toHaveBeenCalledWith(`${endpoint}/${branch}`);
+ });
});
diff --git a/spec/frontend/contributors/store/actions_spec.js b/spec/frontend/contributors/store/actions_spec.js
index 865f683a91a..b2ebdf2f53c 100644
--- a/spec/frontend/contributors/store/actions_spec.js
+++ b/spec/frontend/contributors/store/actions_spec.js
@@ -4,6 +4,7 @@ import * as actions from '~/contributors/stores/actions';
import * as types from '~/contributors/stores/mutation_types';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/flash.js');
@@ -22,7 +23,7 @@ describe('Contributors store actions', () => {
});
it('should commit SET_CHART_DATA with received response', () => {
- mock.onGet().reply(200, chartData);
+ mock.onGet().reply(HTTP_STATUS_OK, chartData);
return testAction(
actions.fetchChartData,
@@ -38,7 +39,7 @@ describe('Contributors store actions', () => {
});
it('should show flash on API error', async () => {
- mock.onGet().reply(400, 'Not Found');
+ mock.onGet().reply(HTTP_STATUS_BAD_REQUEST, 'Not Found');
await testAction(
actions.fetchChartData,
diff --git a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
index bd4ed950f9d..7d9ae548c9a 100644
--- a/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
+++ b/spec/frontend/custom_metrics/components/custom_metrics_form_fields_spec.js
@@ -4,6 +4,7 @@ import MockAdapter from 'axios-mock-adapter';
import { TEST_HOST } from 'helpers/test_constants';
import CustomMetricsFormFields from '~/custom_metrics/components/custom_metrics_form_fields.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('custom metrics form fields component', () => {
let wrapper;
@@ -46,7 +47,7 @@ describe('custom metrics form fields component', () => {
});
it('checks form validity', async () => {
- mockAxios.onPost(validateQueryPath).reply(200, validQueryResponse);
+ mockAxios.onPost(validateQueryPath).reply(HTTP_STATUS_OK, validQueryResponse);
mountComponent({
metricPersisted: true,
...makeFormData({
@@ -143,7 +144,7 @@ describe('custom metrics form fields component', () => {
describe('when query validation is in flight', () => {
beforeEach(() => {
mountComponent({ metricPersisted: true, ...makeFormData({ query: 'validQuery' }) });
- mockAxios.onPost(validateQueryPath).reply(200, validQueryResponse);
+ mockAxios.onPost(validateQueryPath).reply(HTTP_STATUS_OK, validQueryResponse);
});
it('expect loading message to display', async () => {
@@ -168,7 +169,7 @@ describe('custom metrics form fields component', () => {
const invalidQueryResponse = { success: true, query: { valid: false, error: errorMessage } };
beforeEach(() => {
- mockAxios.onPost(validateQueryPath).reply(200, invalidQueryResponse);
+ mockAxios.onPost(validateQueryPath).reply(HTTP_STATUS_OK, invalidQueryResponse);
mountComponent({ metricPersisted: true, ...makeFormData({ query: 'invalidQuery' }) });
return axios.waitForAll();
});
@@ -180,7 +181,7 @@ describe('custom metrics form fields component', () => {
describe('when query is valid', () => {
beforeEach(() => {
- mockAxios.onPost(validateQueryPath).reply(200, validQueryResponse);
+ mockAxios.onPost(validateQueryPath).reply(HTTP_STATUS_OK, validQueryResponse);
mountComponent({ metricPersisted: true, ...makeFormData({ query: 'validQuery' }) });
});
diff --git a/spec/frontend/deploy_keys/components/app_spec.js b/spec/frontend/deploy_keys/components/app_spec.js
index 79a9aaa9184..d11ecf95de6 100644
--- a/spec/frontend/deploy_keys/components/app_spec.js
+++ b/spec/frontend/deploy_keys/components/app_spec.js
@@ -8,6 +8,7 @@ import deployKeysApp from '~/deploy_keys/components/app.vue';
import ConfirmModal from '~/deploy_keys/components/confirm_modal.vue';
import eventHub from '~/deploy_keys/eventhub';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
const TEST_ENDPOINT = `${TEST_HOST}/dummy/`;
@@ -28,7 +29,7 @@ describe('Deploy keys app component', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(TEST_ENDPOINT).reply(200, data);
+ mock.onGet(TEST_ENDPOINT).reply(HTTP_STATUS_OK, data);
});
afterEach(() => {
@@ -67,7 +68,7 @@ describe('Deploy keys app component', () => {
});
it('does not render key panels when keys object is empty', () => {
- mock.onGet(TEST_ENDPOINT).reply(200, []);
+ mock.onGet(TEST_ENDPOINT).reply(HTTP_STATUS_OK, []);
return mountComponent().then(() => {
expect(findKeyPanels().length).toBe(0);
diff --git a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
index 0bf69acd251..46f7b2f3604 100644
--- a/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
+++ b/spec/frontend/deploy_tokens/components/new_deploy_token_spec.js
@@ -3,6 +3,7 @@ import { nextTick } from 'vue';
import { GlButton, GlFormCheckbox, GlFormInput, GlFormInputGroup, GlDatepicker } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { TEST_HOST } from 'helpers/test_constants';
import NewDeployToken from '~/deploy_tokens/components/new_deploy_token.vue';
import waitForPromises from 'helpers/wait_for_promises';
@@ -131,7 +132,7 @@ describe('New Deploy Token', () => {
write_package_registry: true,
},
})
- .replyOnce(500, { message: expectedErrorMessage });
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, { message: expectedErrorMessage });
wrapper.findAllComponents(GlButton).at(0).vm.$emit('click');
@@ -183,7 +184,7 @@ describe('New Deploy Token', () => {
write_package_registry: true,
},
})
- .replyOnce(200, { username: 'test token username', token: 'test token' });
+ .replyOnce(HTTP_STATUS_OK, { username: 'test token username', token: 'test token' });
return submitTokenThenCheck();
});
@@ -216,7 +217,7 @@ describe('New Deploy Token', () => {
write_package_registry: true,
},
})
- .replyOnce(200, { username: 'test token username', token: 'test token' });
+ .replyOnce(HTTP_STATUS_OK, { username: 'test token username', token: 'test token' });
return submitTokenThenCheck();
});
diff --git a/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap b/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap
index 7cffd3cf3e8..1f4e579f075 100644
--- a/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap
+++ b/spec/frontend/design_management/components/__snapshots__/image_spec.js.snap
@@ -1,5 +1,19 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
+exports[`Design management large image component renders SVG with proper height and width 1`] = `
+<div
+ class="gl-mx-auto gl-my-auto js-design-image"
+>
+ <!---->
+
+ <img
+ alt="test"
+ class="mh-100 img-fluid"
+ src="mockImage.svg"
+ />
+</div>
+`;
+
exports[`Design management large image component renders image 1`] = `
<div
class="gl-mx-auto gl-my-auto js-design-image"
diff --git a/spec/frontend/design_management/components/design_overlay_spec.js b/spec/frontend/design_management/components/design_overlay_spec.js
index 169f2dbdccb..2807fe7727f 100644
--- a/spec/frontend/design_management/components/design_overlay_spec.js
+++ b/spec/frontend/design_management/components/design_overlay_spec.js
@@ -1,32 +1,54 @@
-import { mount } from '@vue/test-utils';
-import { nextTick } from 'vue';
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
import DesignOverlay from '~/design_management/components/design_overlay.vue';
-import { ACTIVE_DISCUSSION_SOURCE_TYPES } from '~/design_management/constants';
-import updateActiveDiscussion from '~/design_management/graphql/mutations/update_active_discussion.mutation.graphql';
+import { resolvers } from '~/design_management/graphql';
+import activeDiscussionQuery from '~/design_management/graphql/queries/active_discussion.query.graphql';
import notes from '../mock_data/notes';
-const mutate = jest.fn(() => Promise.resolve());
+Vue.use(VueApollo);
describe('Design overlay component', () => {
let wrapper;
+ let apolloProvider;
const mockDimensions = { width: 100, height: 100 };
- const findAllNotes = () => wrapper.findAll('.js-image-badge');
- const findCommentBadge = () => wrapper.find('.comment-indicator');
+ const findOverlay = () => wrapper.find('[data-testid="design-overlay"]');
+ const findAllNotes = () => wrapper.findAll('[data-testid="note-pin"]');
+ const findCommentBadge = () => wrapper.find('[data-testid="comment-badge"]');
const findBadgeAtIndex = (noteIndex) => findAllNotes().at(noteIndex);
const findFirstBadge = () => findBadgeAtIndex(0);
const findSecondBadge = () => findBadgeAtIndex(1);
const clickAndDragBadge = async (elem, fromPoint, toPoint) => {
- elem.trigger('mousedown', { clientX: fromPoint.x, clientY: fromPoint.y });
+ elem.vm.$emit(
+ 'mousedown',
+ new MouseEvent('click', { clientX: fromPoint.x, clientY: fromPoint.y }),
+ );
+ findOverlay().trigger('mousemove', { clientX: toPoint.x, clientY: toPoint.y });
await nextTick();
- elem.trigger('mousemove', { clientX: toPoint.x, clientY: toPoint.y });
+ elem.vm.$emit('mouseup', new MouseEvent('click', { clientX: toPoint.x, clientY: toPoint.y }));
await nextTick();
};
function createComponent(props = {}, data = {}) {
- wrapper = mount(DesignOverlay, {
+ apolloProvider = createMockApollo([], resolvers);
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: activeDiscussionQuery,
+ data: {
+ activeDiscussion: {
+ __typename: 'ActiveDiscussion',
+ id: null,
+ source: null,
+ },
+ },
+ });
+
+ wrapper = shallowMount(DesignOverlay, {
+ apolloProvider,
propsData: {
dimensions: mockDimensions,
position: {
@@ -45,14 +67,13 @@ describe('Design overlay component', () => {
...data,
};
},
- mocks: {
- $apollo: {
- mutate,
- },
- },
});
}
+ afterEach(() => {
+ apolloProvider = null;
+ });
+
it('should have correct inline style', () => {
createComponent();
@@ -96,12 +117,15 @@ describe('Design overlay component', () => {
});
it('should have set the correct position for each note badge', () => {
- expect(findFirstBadge().attributes().style).toBe('left: 10px; top: 15px;');
- expect(findSecondBadge().attributes().style).toBe('left: 50px; top: 50px;');
+ expect(findFirstBadge().props('position')).toEqual({
+ left: '10px',
+ top: '15px',
+ });
+ expect(findSecondBadge().props('position')).toEqual({ left: '50px', top: '50px' });
});
it('should apply resolved class to the resolved note pin', () => {
- expect(findSecondBadge().classes()).toContain('resolved');
+ expect(findSecondBadge().props('isResolved')).toBe(true);
});
describe('when no discussion is active', () => {
@@ -116,33 +140,37 @@ describe('Design overlay component', () => {
it.each([notes[0].discussion.notes.nodes[1], notes[0].discussion.notes.nodes[0]])(
'should not apply inactive class to the pin for the active discussion',
async (note) => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- activeDiscussion: {
- id: note.id,
- source: 'discussion',
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: activeDiscussionQuery,
+ data: {
+ activeDiscussion: {
+ __typename: 'ActiveDiscussion',
+ id: note.id,
+ source: 'discussion',
+ },
},
});
await nextTick();
- expect(findBadgeAtIndex(0).classes()).not.toContain('inactive');
+ expect(findBadgeAtIndex(0).props('isInactive')).toBe(false);
},
);
it('should apply inactive class to all pins besides the active one', async () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- activeDiscussion: {
- id: notes[0].id,
- source: 'discussion',
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: activeDiscussionQuery,
+ data: {
+ activeDiscussion: {
+ __typename: 'ActiveDiscussion',
+ id: notes[0].id,
+ source: 'discussion',
+ },
},
});
await nextTick();
- expect(findSecondBadge().classes()).toContain('inactive');
- expect(findFirstBadge().classes()).not.toContain('inactive');
+ expect(findSecondBadge().props('isInactive')).toBe(true);
+ expect(findFirstBadge().props('isInactive')).toBe(false);
});
});
});
@@ -156,7 +184,7 @@ describe('Design overlay component', () => {
},
});
- expect(findFirstBadge().attributes().style).toBe('left: 40px; top: 60px;');
+ expect(findFirstBadge().props('position')).toEqual({ left: '40px', top: '60px' });
wrapper.setProps({
dimensions: {
@@ -166,10 +194,10 @@ describe('Design overlay component', () => {
});
await nextTick();
- expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 30px;');
+ expect(findFirstBadge().props('position')).toEqual({ left: '20px', top: '30px' });
});
- it('should call an update active discussion mutation when clicking a note without moving it', async () => {
+ it('should update active discussion when clicking a note without moving it', async () => {
createComponent({
notes,
dimensions: {
@@ -178,61 +206,36 @@ describe('Design overlay component', () => {
},
});
+ expect(findFirstBadge().props('isInactive')).toBe(null);
+
const note = notes[0];
const { position } = note;
- const mutationVariables = {
- mutation: updateActiveDiscussion,
- variables: {
- id: note.id,
- source: ACTIVE_DISCUSSION_SOURCE_TYPES.pin,
- },
- };
- findFirstBadge().trigger('mousedown', { clientX: position.x, clientY: position.y });
+ findFirstBadge().vm.$emit(
+ 'mousedown',
+ new MouseEvent('click', { clientX: position.x, clientY: position.y }),
+ );
await nextTick();
- findFirstBadge().trigger('mouseup', { clientX: position.x, clientY: position.y });
- expect(mutate).toHaveBeenCalledWith(mutationVariables);
+ findFirstBadge().vm.$emit(
+ 'mouseup',
+ new MouseEvent('click', { clientX: position.x, clientY: position.y }),
+ );
+ await waitForPromises();
+ expect(findFirstBadge().props('isInactive')).toBe(false);
});
});
describe('when moving notes', () => {
- it('should update badge style when note is being moved', async () => {
- createComponent({
- notes,
- });
-
- const { position } = notes[0];
-
- await clickAndDragBadge(findFirstBadge(), { x: position.x, y: position.y }, { x: 20, y: 20 });
- expect(findFirstBadge().attributes().style).toBe('left: 20px; top: 20px;');
- });
-
it('should emit `moveNote` event when note-moving action ends', async () => {
createComponent({ notes });
const note = notes[0];
const { position } = note;
const newCoordinates = { x: 20, y: 20 };
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- movingNoteNewPosition: {
- ...position,
- ...newCoordinates,
- },
- movingNoteStartPosition: {
- noteId: notes[0].id,
- discussionId: notes[0].discussion.id,
- ...position,
- },
- });
-
const badge = findFirstBadge();
await clickAndDragBadge(badge, { x: position.x, y: position.y }, newCoordinates);
- badge.trigger('mouseup');
- await nextTick();
expect(wrapper.emitted('moveNote')).toEqual([
[
{
@@ -266,9 +269,10 @@ describe('Design overlay component', () => {
const badge = findAllNotes().at(0);
await clickAndDragBadge(badge, { ...mockNoteCoordinates }, { x: 20, y: 20 });
// note position should not change after a click-and-drag attempt
- expect(findFirstBadge().attributes().style).toContain(
- `left: ${mockNoteCoordinates.x}px; top: ${mockNoteCoordinates.y}px;`,
- );
+ expect(findFirstBadge().props('position')).toEqual({
+ left: `${mockNoteCoordinates.x}px`,
+ top: `${mockNoteCoordinates.y}px`,
+ });
});
});
});
@@ -282,27 +286,10 @@ describe('Design overlay component', () => {
});
expect(findCommentBadge().exists()).toBe(true);
- expect(findCommentBadge().attributes().style).toBe('left: 10px; top: 15px;');
+ expect(findCommentBadge().props('position')).toEqual({ left: '10px', top: '15px' });
});
describe('when moving the comment badge', () => {
- it('should update badge style to reflect new position', async () => {
- const { position } = notes[0];
-
- createComponent({
- currentCommentForm: {
- ...position,
- },
- });
-
- await clickAndDragBadge(
- findCommentBadge(),
- { x: position.x, y: position.y },
- { x: 20, y: 20 },
- );
- expect(findCommentBadge().attributes().style).toBe('left: 20px; top: 20px;');
- });
-
it('should update badge style when note-moving action ends', async () => {
const { position } = notes[0];
createComponent({
@@ -315,7 +302,7 @@ describe('Design overlay component', () => {
const toPoint = { x: 20, y: 20 };
await clickAndDragBadge(commentBadge, { x: position.x, y: position.y }, toPoint);
- commentBadge.trigger('mouseup');
+ commentBadge.vm.$emit('mouseup', new MouseEvent('click'));
// simulates the currentCommentForm being updated in index.vue component, and
// propagated back down to this prop
wrapper.setProps({
@@ -323,110 +310,50 @@ describe('Design overlay component', () => {
});
await nextTick();
- expect(commentBadge.attributes().style).toBe('left: 20px; top: 20px;');
+ expect(commentBadge.props('position')).toEqual({ left: '20px', top: '20px' });
});
- it.each`
- element | getElementFunc | event
- ${'overlay'} | ${() => wrapper} | ${'mouseleave'}
- ${'comment badge'} | ${findCommentBadge} | ${'mouseup'}
- `(
- 'should emit `openCommentForm` event when $event fired on $element element',
- async ({ getElementFunc, event }) => {
- createComponent({
- notes,
- currentCommentForm: {
- ...notes[0].position,
- },
- });
-
- const newCoordinates = { x: 20, y: 20 };
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- movingNoteStartPosition: {
- ...notes[0].position,
- },
- movingNoteNewPosition: {
- ...notes[0].position,
- ...newCoordinates,
- },
- });
-
- getElementFunc().trigger(event);
- await nextTick();
- expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]);
- },
- );
- });
- });
-
- describe('getMovingNotePositionDelta', () => {
- it('should calculate delta correctly from state', () => {
- createComponent();
+ it('should emit `openCommentForm` event when mouseleave fired on overlay element', async () => {
+ const { position } = notes[0];
+ createComponent({
+ notes,
+ currentCommentForm: {
+ ...position,
+ },
+ });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- movingNoteStartPosition: {
- clientX: 10,
- clientY: 20,
- },
- });
+ const newCoordinates = { x: 20, y: 20 };
- const mockMouseEvent = {
- clientX: 30,
- clientY: 10,
- };
+ await clickAndDragBadge(
+ findCommentBadge(),
+ { x: position.x, y: position.y },
+ newCoordinates,
+ );
- expect(wrapper.vm.getMovingNotePositionDelta(mockMouseEvent)).toEqual({
- deltaX: 20,
- deltaY: -10,
+ wrapper.trigger('mouseleave');
+ await nextTick();
+ expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]);
});
- });
- });
- describe('isPositionInOverlay', () => {
- createComponent({ dimensions: mockDimensions });
-
- it.each`
- test | coordinates | expectedResult
- ${'within overlay bounds'} | ${{ x: 50, y: 50 }} | ${true}
- ${'outside overlay bounds'} | ${{ x: 101, y: 101 }} | ${false}
- `('returns [$expectedResult] when position is $test', ({ coordinates, expectedResult }) => {
- const position = { ...mockDimensions, ...coordinates };
+ it('should emit `openCommentForm` event when mouseup fired on comment badge element', async () => {
+ const { position } = notes[0];
+ createComponent({
+ notes,
+ currentCommentForm: {
+ ...position,
+ },
+ });
- expect(wrapper.vm.isPositionInOverlay(position)).toBe(expectedResult);
- });
- });
+ const newCoordinates = { x: 20, y: 20 };
- describe('getNoteRelativePosition', () => {
- it('calculates position correctly', () => {
- createComponent({ dimensions: mockDimensions });
- const position = { x: 50, y: 50, width: 200, height: 200 };
+ await clickAndDragBadge(
+ findCommentBadge(),
+ { x: position.x, y: position.y },
+ newCoordinates,
+ );
- expect(wrapper.vm.getNoteRelativePosition(position)).toEqual({ left: 25, top: 25 });
+ expect(wrapper.emitted('openCommentForm')).toEqual([[newCoordinates]]);
+ });
});
});
-
- describe('canMoveNote', () => {
- it.each`
- repositionNotePermission | canMoveNoteResult
- ${true} | ${true}
- ${false} | ${false}
- ${undefined} | ${false}
- `(
- 'returns [$canMoveNoteResult] when [repositionNote permission] is [$repositionNotePermission]',
- ({ repositionNotePermission, canMoveNoteResult }) => {
- createComponent();
-
- const note = {
- userPermissions: {
- repositionNote: repositionNotePermission,
- },
- };
- expect(wrapper.vm.canMoveNote(note)).toBe(canMoveNoteResult);
- },
- );
- });
});
diff --git a/spec/frontend/design_management/components/image_spec.js b/spec/frontend/design_management/components/image_spec.js
index 8163cb0d87a..95d2ad504de 100644
--- a/spec/frontend/design_management/components/image_spec.js
+++ b/spec/frontend/design_management/components/image_spec.js
@@ -42,6 +42,16 @@ describe('Design management large image component', () => {
expect(wrapper.element).toMatchSnapshot();
});
+ it('renders SVG with proper height and width', () => {
+ createComponent({
+ isLoading: false,
+ image: 'mockImage.svg',
+ name: 'test',
+ });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+
it('sets correct classes and styles if imageStyle is set', async () => {
createComponent(
{
diff --git a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
index 096d776a7d2..3517c0f7a44 100644
--- a/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
+++ b/spec/frontend/design_management/components/list/__snapshots__/item_spec.js.snap
@@ -21,12 +21,14 @@ exports[`Design management list item component with notes renders item with mult
>
<!---->
- <gl-intersection-observer-stub>
+ <gl-intersection-observer-stub
+ class="gl-flex-grow-1"
+ >
<!---->
<img
alt="test"
- class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full gl-w-auto design-img"
data-qa-filename="test"
data-qa-selector="design_image"
data-testid="design-img-1"
@@ -98,12 +100,14 @@ exports[`Design management list item component with notes renders item with sing
>
<!---->
- <gl-intersection-observer-stub>
+ <gl-intersection-observer-stub
+ class="gl-flex-grow-1"
+ >
<!---->
<img
alt="test"
- class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full design-img"
+ class="gl-display-block gl-mx-auto gl-max-w-full gl-max-h-full gl-w-auto design-img"
data-qa-filename="test"
data-qa-selector="design_image"
data-testid="design-img-1"
diff --git a/spec/frontend/design_management/components/list/item_spec.js b/spec/frontend/design_management/components/list/item_spec.js
index 66d3f883960..e907e2e4ac5 100644
--- a/spec/frontend/design_management/components/list/item_spec.js
+++ b/spec/frontend/design_management/components/list/item_spec.js
@@ -160,9 +160,9 @@ describe('Design management list item component', () => {
describe('with associated event', () => {
it.each`
event | icon | className
- ${DESIGN_VERSION_EVENT.MODIFICATION} | ${'file-modified-solid'} | ${'text-primary-500'}
- ${DESIGN_VERSION_EVENT.DELETION} | ${'file-deletion-solid'} | ${'text-danger-500'}
- ${DESIGN_VERSION_EVENT.CREATION} | ${'file-addition-solid'} | ${'text-success-500'}
+ ${DESIGN_VERSION_EVENT.MODIFICATION} | ${'file-modified-solid'} | ${'gl-text-blue-500'}
+ ${DESIGN_VERSION_EVENT.DELETION} | ${'file-deletion-solid'} | ${'gl-text-red-500'}
+ ${DESIGN_VERSION_EVENT.CREATION} | ${'file-addition-solid'} | ${'gl-text-green-500'}
`('renders item with correct status icon for $event event', ({ event, icon, className }) => {
createComponent({ event });
const eventIcon = findEventIcon();
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
deleted file mode 100644
index a4af73dd194..00000000000
--- a/spec/frontend/design_management/components/upload/__snapshots__/design_version_dropdown_spec.js.snap
+++ /dev/null
@@ -1,243 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Design management design version dropdown component renders design version dropdown button 1`] = `
-<gl-base-dropdown-stub
- ariahaspopup="listbox"
- category="primary"
- icon=""
- issueiid=""
- projectpath=""
- size="small"
- toggleid="dropdown-toggle-btn-2"
- toggletext="Showing latest version"
- variant="default"
->
-
- <!---->
-
- <!---->
-
- <ul
- aria-labelledby="dropdown-toggle-btn-2"
- class="gl-dropdown-contents gl-list-style-none gl-pl-0 gl-mb-0"
- id="listbox"
- role="listbox"
- tabindex="-1"
- >
- <gl-listbox-item-stub
- data-testid="listbox-item-gid://gitlab/DesignManagement::Version/1"
- ischeckcentered="true"
- >
- <span
- class="gl-display-flex gl-align-items-center"
- >
- <div
- class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s32 gl-avatar-identicon-bg1"
- >
-
-
-
- </div>
-
- <span
- class="gl-display-flex gl-flex-direction-column"
- >
- <span
- class="gl-font-weight-bold"
- >
- Version 2 (latest)
- </span>
-
- <span
- class="gl-text-gray-600 gl-mt-1"
- >
- <span
- class="gl-display-block"
- >
- Adminstrator
- </span>
-
- <time-ago-stub
- class="text-1"
- cssclass=""
- time="2021-08-09T06:05:00Z"
- tooltipplacement="bottom"
- />
- </span>
- </span>
- </span>
- </gl-listbox-item-stub>
- <gl-listbox-item-stub
- data-testid="listbox-item-gid://gitlab/DesignManagement::Version/2"
- ischeckcentered="true"
- >
- <span
- class="gl-display-flex gl-align-items-center"
- >
- <div
- class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s32 gl-avatar-identicon-bg1"
- >
-
-
-
- </div>
-
- <span
- class="gl-display-flex gl-flex-direction-column"
- >
- <span
- class="gl-font-weight-bold"
- >
- Version 1
- </span>
-
- <span
- class="gl-text-gray-600 gl-mt-1"
- >
- <span
- class="gl-display-block"
- >
- Adminstrator
- </span>
-
- <time-ago-stub
- class="text-1"
- cssclass=""
- time="2021-08-09T06:05:00Z"
- tooltipplacement="bottom"
- />
- </span>
- </span>
- </span>
- </gl-listbox-item-stub>
-
- <!---->
-
- <!---->
- </ul>
-
- <!---->
-
-</gl-base-dropdown-stub>
-`;
-
-exports[`Design management design version dropdown component renders design version list 1`] = `
-<gl-base-dropdown-stub
- ariahaspopup="listbox"
- category="primary"
- icon=""
- issueiid=""
- projectpath=""
- size="small"
- toggleid="dropdown-toggle-btn-4"
- toggletext="Showing latest version"
- variant="default"
->
-
- <!---->
-
- <!---->
-
- <ul
- aria-labelledby="dropdown-toggle-btn-4"
- class="gl-dropdown-contents gl-list-style-none gl-pl-0 gl-mb-0"
- id="listbox"
- role="listbox"
- tabindex="-1"
- >
- <gl-listbox-item-stub
- data-testid="listbox-item-gid://gitlab/DesignManagement::Version/1"
- ischeckcentered="true"
- >
- <span
- class="gl-display-flex gl-align-items-center"
- >
- <div
- class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s32 gl-avatar-identicon-bg1"
- >
-
-
-
- </div>
-
- <span
- class="gl-display-flex gl-flex-direction-column"
- >
- <span
- class="gl-font-weight-bold"
- >
- Version 2 (latest)
- </span>
-
- <span
- class="gl-text-gray-600 gl-mt-1"
- >
- <span
- class="gl-display-block"
- >
- Adminstrator
- </span>
-
- <time-ago-stub
- class="text-1"
- cssclass=""
- time="2021-08-09T06:05:00Z"
- tooltipplacement="bottom"
- />
- </span>
- </span>
- </span>
- </gl-listbox-item-stub>
- <gl-listbox-item-stub
- data-testid="listbox-item-gid://gitlab/DesignManagement::Version/2"
- ischeckcentered="true"
- >
- <span
- class="gl-display-flex gl-align-items-center"
- >
- <div
- class="gl-avatar gl-avatar-identicon gl-avatar-circle gl-avatar-s32 gl-avatar-identicon-bg1"
- >
-
-
-
- </div>
-
- <span
- class="gl-display-flex gl-flex-direction-column"
- >
- <span
- class="gl-font-weight-bold"
- >
- Version 1
- </span>
-
- <span
- class="gl-text-gray-600 gl-mt-1"
- >
- <span
- class="gl-display-block"
- >
- Adminstrator
- </span>
-
- <time-ago-stub
- class="text-1"
- cssclass=""
- time="2021-08-09T06:05:00Z"
- tooltipplacement="bottom"
- />
- </span>
- </span>
- </span>
- </gl-listbox-item-stub>
-
- <!---->
-
- <!---->
- </ul>
-
- <!---->
-
-</gl-base-dropdown-stub>
-`;
diff --git a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
index 1e9f286a0ec..6ad10e707ab 100644
--- a/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
+++ b/spec/frontend/design_management/components/upload/design_version_dropdown_spec.js
@@ -32,7 +32,7 @@ describe('Design management design version dropdown component', () => {
mocks: {
$route,
},
- stubs: { GlAvatar, GlCollapsibleListbox },
+ stubs: { GlAvatar: true, GlCollapsibleListbox },
});
// setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
@@ -50,18 +50,28 @@ describe('Design management design version dropdown component', () => {
const findAllListboxItems = () => wrapper.findAllComponents(GlListboxItem);
const findVersionLink = (index) => wrapper.findAllComponents(GlListboxItem).at(index);
- it('renders design version dropdown button', async () => {
- createComponent();
+ describe('renders the item with custom template in design version list', () => {
+ let listItem;
+ const latestVersion = mockAllVersions[0];
- await nextTick();
- expect(wrapper.element).toMatchSnapshot();
- });
+ beforeEach(async () => {
+ createComponent();
+ await nextTick();
+ listItem = findAllListboxItems().at(0);
+ });
- it('renders design version list', async () => {
- createComponent();
+ it('should render author name and their avatar', () => {
+ expect(listItem.findComponent(GlAvatar).props('alt')).toBe(latestVersion.author.name);
+ expect(listItem.text()).toContain(latestVersion.author.name);
+ });
+
+ it('should render correct version number', () => {
+ expect(listItem.text()).toContain('Version 2 (latest)');
+ });
- await nextTick();
- expect(wrapper.element).toMatchSnapshot();
+ it('should render time ago tooltip', () => {
+ expect(listItem.findComponent(TimeAgo).props('time')).toBe(latestVersion.createdAt);
+ });
});
describe('selected version name', () => {
diff --git a/spec/frontend/diffs/components/app_spec.js b/spec/frontend/diffs/components/app_spec.js
index c8be0bedb4c..513e67ea247 100644
--- a/spec/frontend/diffs/components/app_spec.js
+++ b/spec/frontend/diffs/components/app_spec.js
@@ -17,6 +17,7 @@ import CollapsedFilesWarning from '~/diffs/components/collapsed_files_warning.vu
import HiddenFilesWarning from '~/diffs/components/hidden_files_warning.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as urlUtils from '~/lib/utils/url_utility';
import { stubPerformanceWebAPI } from 'helpers/performance';
import createDiffsStore from '../create_diffs_store';
@@ -87,7 +88,7 @@ describe('diffs/components/app', () => {
};
window.mrTabs.expandViewContainer = jest.fn();
mock = new MockAdapter(axios);
- mock.onGet(TEST_ENDPOINT).reply(200, {});
+ mock.onGet(TEST_ENDPOINT).reply(HTTP_STATUS_OK, {});
});
afterEach(() => {
diff --git a/spec/frontend/diffs/components/commit_item_spec.js b/spec/frontend/diffs/components/commit_item_spec.js
index 75d55376d09..08be3fa2745 100644
--- a/spec/frontend/diffs/components/commit_item_spec.js
+++ b/spec/frontend/diffs/components/commit_item_spec.js
@@ -9,8 +9,8 @@ import CommitPipelineStatus from '~/projects/tree/components/commit_pipeline_sta
const TEST_AUTHOR_NAME = 'test';
const TEST_AUTHOR_EMAIL = 'test+test@gitlab.com';
const TEST_AUTHOR_GRAVATAR = `${TEST_HOST}/avatar/test?s=40`;
-const TEST_SIGNATURE_HTML = `<a class="btn gpg-status-box valid" data-content="signature-content" data-html="true" data-placement="top" data-title="signature-title" data-toggle="popover" role="button" tabindex="0">
- Verified
+const TEST_SIGNATURE_HTML = `<a class="btn signature-badge" data-content="signature-content" data-html="true" data-placement="top" data-title="signature-title" data-toggle="popover" role="button" tabindex="0">
+ <span class="gl-badge badge badge-pill badge-success md">Verified</span>
</a>`;
const TEST_PIPELINE_STATUS_PATH = `${TEST_HOST}/pipeline/status`;
@@ -156,7 +156,7 @@ describe('diffs/components/commit_item', () => {
it('renders signature html', () => {
const actionsElement = getCommitActionsElement();
- const signatureElement = actionsElement.find('.gpg-status-box');
+ const signatureElement = actionsElement.find('.signature-badge');
expect(signatureElement.html()).toBe(TEST_SIGNATURE_HTML);
});
diff --git a/spec/frontend/diffs/components/diff_row_utils_spec.js b/spec/frontend/diffs/components/diff_row_utils_spec.js
index a6f508c73eb..6e9eb433924 100644
--- a/spec/frontend/diffs/components/diff_row_utils_spec.js
+++ b/spec/frontend/diffs/components/diff_row_utils_spec.js
@@ -21,262 +21,287 @@ function problemsClone({
};
}
-describe('isHighlighted', () => {
- it('should return true if line is highlighted', () => {
- const line = { line_code: LINE_CODE };
- const isCommented = false;
- expect(utils.isHighlighted(LINE_CODE, line, isCommented)).toBe(true);
- });
-
- it('should return false if line is not highlighted', () => {
- const line = { line_code: LINE_CODE };
- const isCommented = false;
- expect(utils.isHighlighted('xxx', line, isCommented)).toBe(false);
- });
-
- it('should return true if isCommented is true', () => {
- const line = { line_code: LINE_CODE };
- const isCommented = true;
- expect(utils.isHighlighted('xxx', line, isCommented)).toBe(true);
- });
-});
-
-describe('isContextLine', () => {
- it('return true if line type is context', () => {
- expect(utils.isContextLine(CONTEXT_LINE_TYPE)).toBe(true);
- });
-
- it('return false if line type is not context', () => {
- expect(utils.isContextLine('xxx')).toBe(false);
- });
-});
-
-describe('isMatchLine', () => {
- it('return true if line type is match', () => {
- expect(utils.isMatchLine(MATCH_LINE_TYPE)).toBe(true);
- });
-
- it('return false if line type is not match', () => {
- expect(utils.isMatchLine('xxx')).toBe(false);
- });
-});
-
-describe('isMetaLine', () => {
- it.each`
- type | expectation
- ${OLD_NO_NEW_LINE_TYPE} | ${true}
- ${NEW_NO_NEW_LINE_TYPE} | ${true}
- ${EMPTY_CELL_TYPE} | ${true}
- ${'xxx'} | ${false}
- `('should return $expectation if type is $type', ({ type, expectation }) => {
- expect(utils.isMetaLine(type)).toBe(expectation);
- });
-});
-
-describe('shouldRenderCommentButton', () => {
- it('should return false if comment button is not rendered', () => {
- expect(utils.shouldRenderCommentButton(true, false)).toBe(false);
- });
-
- it('should return false if not logged in', () => {
- expect(utils.shouldRenderCommentButton(false, true)).toBe(false);
- });
-
- it('should return true logged in and rendered', () => {
- expect(utils.shouldRenderCommentButton(true, true)).toBe(true);
- });
-});
-
-describe('hasDiscussions', () => {
- it('should return false if line is undefined', () => {
- expect(utils.hasDiscussions()).toBe(false);
- });
-
- it('should return false if discussions is undefined', () => {
- expect(utils.hasDiscussions({})).toBe(false);
- });
-
- it('should return false if discussions has legnth of 0', () => {
- expect(utils.hasDiscussions({ discussions: [] })).toBe(false);
- });
+describe('diff_row_utils', () => {
+ describe('isHighlighted', () => {
+ it('should return true if line is highlighted', () => {
+ const line = { line_code: LINE_CODE };
+ const isCommented = false;
+ expect(utils.isHighlighted(LINE_CODE, line, isCommented)).toBe(true);
+ });
+
+ it('should return false if line is not highlighted', () => {
+ const line = { line_code: LINE_CODE };
+ const isCommented = false;
+ expect(utils.isHighlighted('xxx', line, isCommented)).toBe(false);
+ });
- it('should return true if discussions has legnth > 0', () => {
- expect(utils.hasDiscussions({ discussions: [1] })).toBe(true);
- });
-});
-
-describe('lineHref', () => {
- it(`should return #${LINE_CODE}`, () => {
- expect(utils.lineHref({ line_code: LINE_CODE })).toEqual(`#${LINE_CODE}`);
- });
-
- it(`should return '#' if line is undefined`, () => {
- expect(utils.lineHref()).toEqual('#');
- });
-
- it(`should return '#' if line_code is undefined`, () => {
- expect(utils.lineHref({})).toEqual('#');
- });
-});
-
-describe('lineCode', () => {
- it(`should return undefined if line_code is undefined`, () => {
- expect(utils.lineCode()).toEqual(undefined);
- expect(utils.lineCode({ left: {} })).toEqual(undefined);
- expect(utils.lineCode({ right: {} })).toEqual(undefined);
- });
-
- it(`should return ${LINE_CODE}`, () => {
- expect(utils.lineCode({ line_code: LINE_CODE })).toEqual(LINE_CODE);
- expect(utils.lineCode({ left: { line_code: LINE_CODE } })).toEqual(LINE_CODE);
- expect(utils.lineCode({ right: { line_code: LINE_CODE } })).toEqual(LINE_CODE);
- });
-});
-
-describe('classNameMapCell', () => {
- it.each`
- line | hll | isLoggedIn | isHover | expectation
- ${undefined} | ${true} | ${true} | ${true} | ${[]}
- ${{ type: 'new' }} | ${false} | ${false} | ${false} | ${['new', { hll: false, 'is-over': false, new_line: true, old_line: false }]}
- ${{ type: 'new' }} | ${true} | ${true} | ${false} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
- ${{ type: 'new' }} | ${true} | ${false} | ${true} | ${['new', { hll: true, 'is-over': false, new_line: true, old_line: false }]}
- ${{ type: 'new' }} | ${true} | ${true} | ${true} | ${['new', { hll: true, 'is-over': true, new_line: true, old_line: false }]}
- `('should return $expectation', ({ line, hll, isLoggedIn, isHover, expectation }) => {
- const classes = utils.classNameMapCell({ line, hll, isLoggedIn, isHover });
- expect(classes).toEqual(expectation);
- });
-});
-
-describe('addCommentTooltip', () => {
- const brokenSymLinkTooltip =
- 'Commenting on symbolic links that replace or are replaced by files is not supported';
- const brokenRealTooltip =
- 'Commenting on files that replace or are replaced by symbolic links is not supported';
- const lineMovedOrRenamedFileTooltip =
- 'Commenting on files that are only moved or renamed is not supported';
- const lineWithNoLineCodeTooltip = 'Commenting on this line is not supported';
- const dragTooltip = 'Add a comment to this line or drag for multiple lines';
-
- it('should return default tooltip', () => {
- expect(utils.addCommentTooltip()).toBeUndefined();
- });
-
- it('should return drag comment tooltip when dragging is enabled', () => {
- expect(utils.addCommentTooltip({ problems: problemsClone() })).toEqual(dragTooltip);
- });
-
- it('should return broken symlink tooltip', () => {
- expect(
- utils.addCommentTooltip({
- problems: problemsClone({ brokenSymlink: { wasSymbolic: true } }),
- }),
- ).toEqual(brokenSymLinkTooltip);
- expect(
- utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { isSymbolic: true } }) }),
- ).toEqual(brokenSymLinkTooltip);
- });
-
- it('should return broken real tooltip', () => {
- expect(
- utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { wasReal: true } }) }),
- ).toEqual(brokenRealTooltip);
- expect(
- utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { isReal: true } }) }),
- ).toEqual(brokenRealTooltip);
- });
-
- it('reports a tooltip when the line is in a file that has only been moved or renamed', () => {
- expect(utils.addCommentTooltip({ problems: problemsClone({ fileOnlyMoved: true }) })).toEqual(
- lineMovedOrRenamedFileTooltip,
+ it('should return true if isCommented is true', () => {
+ const line = { line_code: LINE_CODE };
+ const isCommented = true;
+ expect(utils.isHighlighted('xxx', line, isCommented)).toBe(true);
+ });
+ });
+
+ describe('isContextLine', () => {
+ it('return true if line type is context', () => {
+ expect(utils.isContextLine(CONTEXT_LINE_TYPE)).toBe(true);
+ });
+
+ it('return false if line type is not context', () => {
+ expect(utils.isContextLine('xxx')).toBe(false);
+ });
+ });
+
+ describe('isMatchLine', () => {
+ it('return true if line type is match', () => {
+ expect(utils.isMatchLine(MATCH_LINE_TYPE)).toBe(true);
+ });
+
+ it('return false if line type is not match', () => {
+ expect(utils.isMatchLine('xxx')).toBe(false);
+ });
+ });
+
+ describe('isMetaLine', () => {
+ it.each`
+ type | expectation
+ ${OLD_NO_NEW_LINE_TYPE} | ${true}
+ ${NEW_NO_NEW_LINE_TYPE} | ${true}
+ ${EMPTY_CELL_TYPE} | ${true}
+ ${'xxx'} | ${false}
+ `('should return $expectation if type is $type', ({ type, expectation }) => {
+ expect(utils.isMetaLine(type)).toBe(expectation);
+ });
+ });
+
+ describe('shouldRenderCommentButton', () => {
+ it('should return false if comment button is not rendered', () => {
+ expect(utils.shouldRenderCommentButton(true, false)).toBe(false);
+ });
+
+ it('should return false if not logged in', () => {
+ expect(utils.shouldRenderCommentButton(false, true)).toBe(false);
+ });
+
+ it('should return true logged in and rendered', () => {
+ expect(utils.shouldRenderCommentButton(true, true)).toBe(true);
+ });
+ });
+
+ describe('hasDiscussions', () => {
+ it('should return false if line is undefined', () => {
+ expect(utils.hasDiscussions()).toBe(false);
+ });
+
+ it('should return false if discussions is undefined', () => {
+ expect(utils.hasDiscussions({})).toBe(false);
+ });
+
+ it('should return false if discussions has legnth of 0', () => {
+ expect(utils.hasDiscussions({ discussions: [] })).toBe(false);
+ });
+
+ it('should return true if discussions has legnth > 0', () => {
+ expect(utils.hasDiscussions({ discussions: [1] })).toBe(true);
+ });
+ });
+
+ describe('lineHref', () => {
+ it(`should return #${LINE_CODE}`, () => {
+ expect(utils.lineHref({ line_code: LINE_CODE })).toEqual(`#${LINE_CODE}`);
+ });
+
+ it(`should return '#' if line is undefined`, () => {
+ expect(utils.lineHref()).toEqual('#');
+ });
+
+ it(`should return '#' if line_code is undefined`, () => {
+ expect(utils.lineHref({})).toEqual('#');
+ });
+ });
+
+ describe('lineCode', () => {
+ it(`should return undefined if line_code is undefined`, () => {
+ expect(utils.lineCode()).toEqual(undefined);
+ expect(utils.lineCode({ left: {} })).toEqual(undefined);
+ expect(utils.lineCode({ right: {} })).toEqual(undefined);
+ });
+
+ it(`should return ${LINE_CODE}`, () => {
+ expect(utils.lineCode({ line_code: LINE_CODE })).toEqual(LINE_CODE);
+ expect(utils.lineCode({ left: { line_code: LINE_CODE } })).toEqual(LINE_CODE);
+ expect(utils.lineCode({ right: { line_code: LINE_CODE } })).toEqual(LINE_CODE);
+ });
+ });
+
+ describe('classNameMapCell', () => {
+ it.each`
+ line | highlighted | commented | selectionStart | selectionEnd | isLoggedIn | isHover | expectation
+ ${undefined} | ${true} | ${false} | ${false} | ${false} | ${true} | ${true} | ${[{ 'highlight-top': true, 'highlight-bottom': true, hll: true, commented: false }]}
+ ${undefined} | ${false} | ${true} | ${false} | ${false} | ${true} | ${true} | ${[{ 'highlight-top': false, 'highlight-bottom': false, hll: false, commented: true }]}
+ ${{ type: 'new' }} | ${false} | ${false} | ${false} | ${false} | ${false} | ${false} | ${[{ new: true, 'highlight-top': false, 'highlight-bottom': false, hll: false, commented: false, 'is-over': false, new_line: true, old_line: false }]}
+ ${{ type: 'new' }} | ${true} | ${false} | ${false} | ${false} | ${true} | ${false} | ${[{ new: true, 'highlight-top': true, 'highlight-bottom': true, hll: true, commented: false, 'is-over': false, new_line: true, old_line: false }]}
+ ${{ type: 'new' }} | ${true} | ${false} | ${false} | ${false} | ${false} | ${true} | ${[{ new: true, 'highlight-top': true, 'highlight-bottom': true, hll: true, commented: false, 'is-over': false, new_line: true, old_line: false }]}
+ ${{ type: 'new' }} | ${true} | ${false} | ${false} | ${false} | ${true} | ${true} | ${[{ new: true, 'highlight-top': true, 'highlight-bottom': true, hll: true, commented: false, 'is-over': true, new_line: true, old_line: false }]}
+ `(
+ 'should return $expectation',
+ ({
+ line,
+ highlighted,
+ commented,
+ selectionStart,
+ selectionEnd,
+ isLoggedIn,
+ isHover,
+ expectation,
+ }) => {
+ const classes = utils.classNameMapCell({
+ line,
+ highlighted,
+ commented,
+ selectionStart,
+ selectionEnd,
+ isLoggedIn,
+ isHover,
+ });
+ expect(classes).toEqual(expectation);
+ },
);
});
- it("reports a tooltip when the line doesn't have a line code to leave a comment on", () => {
- expect(utils.addCommentTooltip({ problems: problemsClone({ brokenLineCode: true }) })).toEqual(
- lineWithNoLineCodeTooltip,
+ describe('addCommentTooltip', () => {
+ const brokenSymLinkTooltip =
+ 'Commenting on symbolic links that replace or are replaced by files is not supported';
+ const brokenRealTooltip =
+ 'Commenting on files that replace or are replaced by symbolic links is not supported';
+ const lineMovedOrRenamedFileTooltip =
+ 'Commenting on files that are only moved or renamed is not supported';
+ const lineWithNoLineCodeTooltip = 'Commenting on this line is not supported';
+ const dragTooltip = 'Add a comment to this line or drag for multiple lines';
+
+ it('should return default tooltip', () => {
+ expect(utils.addCommentTooltip()).toBeUndefined();
+ });
+
+ it('should return drag comment tooltip when dragging is enabled', () => {
+ expect(utils.addCommentTooltip({ problems: problemsClone() })).toEqual(dragTooltip);
+ });
+
+ it('should return broken symlink tooltip', () => {
+ expect(
+ utils.addCommentTooltip({
+ problems: problemsClone({ brokenSymlink: { wasSymbolic: true } }),
+ }),
+ ).toEqual(brokenSymLinkTooltip);
+ expect(
+ utils.addCommentTooltip({
+ problems: problemsClone({ brokenSymlink: { isSymbolic: true } }),
+ }),
+ ).toEqual(brokenSymLinkTooltip);
+ });
+
+ it('should return broken real tooltip', () => {
+ expect(
+ utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { wasReal: true } }) }),
+ ).toEqual(brokenRealTooltip);
+ expect(
+ utils.addCommentTooltip({ problems: problemsClone({ brokenSymlink: { isReal: true } }) }),
+ ).toEqual(brokenRealTooltip);
+ });
+
+ it('reports a tooltip when the line is in a file that has only been moved or renamed', () => {
+ expect(utils.addCommentTooltip({ problems: problemsClone({ fileOnlyMoved: true }) })).toEqual(
+ lineMovedOrRenamedFileTooltip,
+ );
+ });
+
+ it("reports a tooltip when the line doesn't have a line code to leave a comment on", () => {
+ expect(
+ utils.addCommentTooltip({ problems: problemsClone({ brokenLineCode: true }) }),
+ ).toEqual(lineWithNoLineCodeTooltip);
+ });
+ });
+
+ describe('parallelViewLeftLineType', () => {
+ it(`should return ${OLD_NO_NEW_LINE_TYPE}`, () => {
+ expect(
+ utils.parallelViewLeftLineType({ line: { right: { type: NEW_NO_NEW_LINE_TYPE } } }),
+ ).toEqual(OLD_NO_NEW_LINE_TYPE);
+ });
+
+ it(`should return 'new'`, () => {
+ expect(utils.parallelViewLeftLineType({ line: { left: { type: 'new' } } })[0]).toBe('new');
+ });
+
+ it(`should return ${EMPTY_CELL_TYPE}`, () => {
+ expect(utils.parallelViewLeftLineType({})).toContain(EMPTY_CELL_TYPE);
+ });
+
+ it(`should return hll:true`, () => {
+ expect(utils.parallelViewLeftLineType({ highlighted: true })[1].hll).toBe(true);
+ });
+ });
+
+ describe('shouldShowCommentButton', () => {
+ it.each`
+ hover | context | meta | discussions | expectation
+ ${true} | ${false} | ${false} | ${false} | ${true}
+ ${false} | ${false} | ${false} | ${false} | ${false}
+ ${true} | ${true} | ${false} | ${false} | ${false}
+ ${true} | ${true} | ${true} | ${false} | ${false}
+ ${true} | ${true} | ${true} | ${true} | ${false}
+ `(
+ 'should return $expectation when hover is $hover',
+ ({ hover, context, meta, discussions, expectation }) => {
+ expect(utils.shouldShowCommentButton(hover, context, meta, discussions)).toBe(expectation);
+ },
);
});
-});
-
-describe('parallelViewLeftLineType', () => {
- it(`should return ${OLD_NO_NEW_LINE_TYPE}`, () => {
- expect(utils.parallelViewLeftLineType({ right: { type: NEW_NO_NEW_LINE_TYPE } })).toEqual(
- OLD_NO_NEW_LINE_TYPE,
- );
- });
-
- it(`should return 'new'`, () => {
- expect(utils.parallelViewLeftLineType({ left: { type: 'new' } })).toContain('new');
- });
-
- it(`should return ${EMPTY_CELL_TYPE}`, () => {
- expect(utils.parallelViewLeftLineType({})).toContain(EMPTY_CELL_TYPE);
- });
-
- it(`should return hll:true`, () => {
- expect(utils.parallelViewLeftLineType({}, true)[1]).toEqual({ hll: true });
- });
-});
-
-describe('shouldShowCommentButton', () => {
- it.each`
- hover | context | meta | discussions | expectation
- ${true} | ${false} | ${false} | ${false} | ${true}
- ${false} | ${false} | ${false} | ${false} | ${false}
- ${true} | ${true} | ${false} | ${false} | ${false}
- ${true} | ${true} | ${true} | ${false} | ${false}
- ${true} | ${true} | ${true} | ${true} | ${false}
- `(
- 'should return $expectation when hover is $hover',
- ({ hover, context, meta, discussions, expectation }) => {
- expect(utils.shouldShowCommentButton(hover, context, meta, discussions)).toBe(expectation);
- },
- );
-});
-describe('mapParallel', () => {
- it('should assign computed properties to the line object', () => {
- const side = {
- discussions: [{}],
- discussionsExpanded: true,
- hasForm: true,
- problems: problemsClone(),
- };
- const content = {
- diffFile: {},
- hasParallelDraftLeft: () => false,
- hasParallelDraftRight: () => false,
- draftsForLine: () => [],
- };
- const line = { left: side, right: side };
- const expectation = {
- commentRowClasses: '',
- draftRowClasses: 'js-temp-notes-holder',
- hasDiscussionsLeft: true,
- hasDiscussionsRight: true,
- isContextLineLeft: false,
- isContextLineRight: false,
- isMatchLineLeft: false,
- isMatchLineRight: false,
- isMetaLineLeft: false,
- isMetaLineRight: false,
- };
- const leftExpectation = {
- renderDiscussion: true,
- hasDraft: false,
- lineDrafts: [],
- hasCommentForm: true,
- };
- const rightExpectation = {
- renderDiscussion: false,
- hasDraft: false,
- lineDrafts: [],
- hasCommentForm: false,
- };
- const mapped = utils.mapParallel(content)(line);
-
- expect(mapped).toMatchObject(expectation);
- expect(mapped.left).toMatchObject(leftExpectation);
- expect(mapped.right).toMatchObject(rightExpectation);
+ describe('mapParallel', () => {
+ it('should assign computed properties to the line object', () => {
+ const side = {
+ discussions: [{}],
+ discussionsExpanded: true,
+ hasForm: true,
+ problems: problemsClone(),
+ };
+ const content = {
+ diffFile: {},
+ hasParallelDraftLeft: () => false,
+ hasParallelDraftRight: () => false,
+ draftsForLine: () => [],
+ };
+ const line = { left: side, right: side };
+ const expectation = {
+ commentRowClasses: '',
+ draftRowClasses: 'js-temp-notes-holder',
+ hasDiscussionsLeft: true,
+ hasDiscussionsRight: true,
+ isContextLineLeft: false,
+ isContextLineRight: false,
+ isMatchLineLeft: false,
+ isMatchLineRight: false,
+ isMetaLineLeft: false,
+ isMetaLineRight: false,
+ };
+ const leftExpectation = {
+ renderDiscussion: true,
+ hasDraft: false,
+ lineDrafts: [],
+ hasCommentForm: true,
+ };
+ const rightExpectation = {
+ renderDiscussion: false,
+ hasDraft: false,
+ lineDrafts: [],
+ hasCommentForm: false,
+ };
+ const mapped = utils.mapParallel(content)(line);
+
+ expect(mapped).toMatchObject(expectation);
+ expect(mapped.left).toMatchObject(leftExpectation);
+ expect(mapped.right).toMatchObject(rightExpectation);
+ });
});
});
diff --git a/spec/frontend/diffs/components/tree_list_spec.js b/spec/frontend/diffs/components/tree_list_spec.js
index ca7de8fd751..1656eaf8ba0 100644
--- a/spec/frontend/diffs/components/tree_list_spec.js
+++ b/spec/frontend/diffs/components/tree_list_spec.js
@@ -49,6 +49,7 @@ describe('Diffs tree list component', () => {
tempFile: true,
type: 'blob',
parentPath: 'app',
+ tree: [],
},
'test.rb': {
addedLines: 0,
@@ -62,6 +63,7 @@ describe('Diffs tree list component', () => {
tempFile: true,
type: 'blob',
parentPath: 'app',
+ tree: [],
},
app: {
key: 'app',
diff --git a/spec/frontend/diffs/store/actions_spec.js b/spec/frontend/diffs/store/actions_spec.js
index 9e0ffbf757f..78765204322 100644
--- a/spec/frontend/diffs/store/actions_spec.js
+++ b/spec/frontend/diffs/store/actions_spec.js
@@ -16,6 +16,12 @@ import * as treeWorkerUtils from '~/diffs/utils/tree_worker_utils';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
+import {
+ HTTP_STATUS_BAD_REQUEST,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import { mergeUrlParams } from '~/lib/utils/url_utility';
import eventHub from '~/notes/event_hub';
import { diffMetadata } from '../mock_data/diff_metadata';
@@ -142,7 +148,7 @@ describe('DiffsStoreActions', () => {
endpointBatch,
),
)
- .reply(200, res1)
+ .reply(HTTP_STATUS_OK, res1)
.onGet(
mergeUrlParams(
{
@@ -154,7 +160,7 @@ describe('DiffsStoreActions', () => {
endpointBatch,
),
)
- .reply(200, res2);
+ .reply(HTTP_STATUS_OK, res2);
return testAction(
diffActions.fetchDiffFilesBatch,
@@ -186,7 +192,7 @@ describe('DiffsStoreActions', () => {
});
it('should fetch diff meta information', () => {
- mock.onGet(endpointMetadata).reply(200, diffMetadata);
+ mock.onGet(endpointMetadata).reply(HTTP_STATUS_OK, diffMetadata);
return testAction(
diffActions.fetchDiffFilesMeta,
@@ -208,7 +214,7 @@ describe('DiffsStoreActions', () => {
});
it('should show a warning on 404 reponse', async () => {
- mock.onGet(endpointMetadata).reply(404);
+ mock.onGet(endpointMetadata).reply(HTTP_STATUS_NOT_FOUND);
await testAction(
diffActions.fetchDiffFilesMeta,
@@ -228,7 +234,7 @@ describe('DiffsStoreActions', () => {
});
it('should show no warning on any other status code', async () => {
- mock.onGet(endpointMetadata).reply(500);
+ mock.onGet(endpointMetadata).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await testAction(
diffActions.fetchDiffFilesMeta,
@@ -248,7 +254,7 @@ describe('DiffsStoreActions', () => {
it('should commit SET_COVERAGE_DATA with received response', () => {
const data = { files: { 'app.js': { 1: 0, 2: 1 } } };
- mock.onGet(endpointCoverage).reply(200, { data });
+ mock.onGet(endpointCoverage).reply(HTTP_STATUS_OK, { data });
return testAction(
diffActions.fetchCoverageFiles,
@@ -260,7 +266,7 @@ describe('DiffsStoreActions', () => {
});
it('should show flash on API error', async () => {
- mock.onGet(endpointCoverage).reply(400);
+ mock.onGet(endpointCoverage).reply(HTTP_STATUS_BAD_REQUEST);
await testAction(diffActions.fetchCoverageFiles, {}, { endpointCoverage }, [], []);
expect(createAlert).toHaveBeenCalledTimes(1);
@@ -545,7 +551,7 @@ describe('DiffsStoreActions', () => {
const nextLineNumbers = {};
const options = { endpoint, params, lineNumbers, fileHash, isExpandDown, nextLineNumbers };
const contextLines = { contextLines: [{ lineCode: 6 }] };
- mock.onGet(endpoint).reply(200, contextLines);
+ mock.onGet(endpoint).reply(HTTP_STATUS_OK, contextLines);
return testAction(
diffActions.loadMoreLines,
@@ -568,7 +574,7 @@ describe('DiffsStoreActions', () => {
const file = { hash: 123, load_collapsed_diff_url: '/load/collapsed/diff/url' };
const data = { hash: 123, parallelDiffLines: [{ lineCode: 1 }] };
const commit = jest.fn();
- mock.onGet(file.loadCollapsedDiffUrl).reply(200, data);
+ mock.onGet(file.loadCollapsedDiffUrl).reply(HTTP_STATUS_OK, data);
return diffActions
.loadCollapsedDiff({ commit, getters: { commitId: null }, state }, file)
@@ -1007,7 +1013,7 @@ describe('DiffsStoreActions', () => {
putSpy = jest.spyOn(axios, 'put');
gon = window.gon;
- mock.onPut(endpointUpdateUser).reply(200, {});
+ mock.onPut(endpointUpdateUser).reply(HTTP_STATUS_OK, {});
jest.spyOn(eventHub, '$emit').mockImplementation();
});
@@ -1084,7 +1090,7 @@ describe('DiffsStoreActions', () => {
describe('fetchFullDiff', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/context`).replyOnce(200, ['test']);
+ mock.onGet(`${TEST_HOST}/context`).replyOnce(HTTP_STATUS_OK, ['test']);
});
it('commits the success and dispatches an action to expand the new lines', () => {
@@ -1105,7 +1111,7 @@ describe('DiffsStoreActions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/context`).replyOnce(500);
+ mock.onGet(`${TEST_HOST}/context`).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches receiveFullDiffError', () => {
@@ -1169,7 +1175,7 @@ describe('DiffsStoreActions', () => {
describe('success', () => {
beforeEach(() => {
renamedFile = { ...testFile, context_lines_path: SUCCESS_URL };
- mock.onGet(SUCCESS_URL).replyOnce(200, testData);
+ mock.onGet(SUCCESS_URL).replyOnce(HTTP_STATUS_OK, testData);
});
it.each`
@@ -1269,7 +1275,7 @@ describe('DiffsStoreActions', () => {
describe('setSuggestPopoverDismissed', () => {
it('commits SET_SHOW_SUGGEST_POPOVER', async () => {
const state = { dismissEndpoint: `${TEST_HOST}/-/user_callouts` };
- mock.onPost(state.dismissEndpoint).reply(200, {});
+ mock.onPost(state.dismissEndpoint).reply(HTTP_STATUS_OK, {});
jest.spyOn(axios, 'post');
@@ -1444,7 +1450,7 @@ describe('DiffsStoreActions', () => {
beforeEach(() => {
putSpy = jest.spyOn(axios, 'put');
- mock.onPut(updateUserEndpoint).reply(200, {});
+ mock.onPut(updateUserEndpoint).reply(HTTP_STATUS_OK, {});
});
it.each`
diff --git a/spec/frontend/dropzone_input_spec.js b/spec/frontend/dropzone_input_spec.js
index 0f7926ccbf9..fdd157dd09f 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 { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
const TEST_FILE = new File([], 'somefile.jpg');
TEST_FILE.upload = {};
@@ -161,7 +161,7 @@ describe('dropzone_input', () => {
${'text/plain'} | ${TEST_ERROR_MESSAGE}
`('when AJAX fails with json', ({ responseType, responseBody }) => {
mock.post(TEST_UPLOAD_PATH, {
- status: 400,
+ status: HTTP_STATUS_BAD_REQUEST,
body: responseBody,
headers: { 'Content-Type': responseType },
});
diff --git a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
index 19ebe0e3cb7..c42ac28c498 100644
--- a/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
+++ b/spec/frontend/editor/source_editor_markdown_livepreview_ext_spec.js
@@ -14,6 +14,7 @@ import { EditorMarkdownPreviewExtension } from '~/editor/extensions/source_edito
import SourceEditor from '~/editor/source_editor';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import syntaxHighlight from '~/syntax_highlight';
import { spyOnApi } from './helpers';
@@ -154,7 +155,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
describe('onBeforeUnuse', () => {
beforeEach(async () => {
- mockAxios.onPost().reply(200, { body: responseData });
+ mockAxios.onPost().reply(HTTP_STATUS_OK, { body: responseData });
await togglePreview();
});
afterEach(() => {
@@ -260,7 +261,9 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
let previewMarkdownSpy;
beforeEach(() => {
- previewMarkdownSpy = jest.fn().mockImplementation(() => [200, { body: responseData }]);
+ previewMarkdownSpy = jest
+ .fn()
+ .mockImplementation(() => [HTTP_STATUS_OK, { body: responseData }]);
mockAxios.onPost(previewMarkdownPath).replyOnce((req) => previewMarkdownSpy(req));
});
@@ -285,7 +288,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
});
it('catches the errors when fetching the preview', async () => {
- mockAxios.onPost().reply(500);
+ mockAxios.onPost().reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await fetchPreview();
expect(createAlert).toHaveBeenCalled();
@@ -321,7 +324,7 @@ describe('Markdown Live Preview Extension for Source Editor', () => {
describe('togglePreview', () => {
beforeEach(() => {
- mockAxios.onPost().reply(200, { body: responseData });
+ mockAxios.onPost().reply(HTTP_STATUS_OK, { body: responseData });
});
it('toggles the condition to toggle preview/hide actions in the context menu', () => {
diff --git a/spec/frontend/emoji/awards_app/store/actions_spec.js b/spec/frontend/emoji/awards_app/store/actions_spec.js
index cd3dfab30d4..3e9b49707ed 100644
--- a/spec/frontend/emoji/awards_app/store/actions_spec.js
+++ b/spec/frontend/emoji/awards_app/store/actions_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import * as actions from '~/emoji/awards_app/store/actions';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('@sentry/browser');
jest.mock('~/vue_shared/plugins/global_toast');
@@ -41,10 +42,10 @@ describe('Awards app actions', () => {
window.gon = { relative_url_root: relativeRootUrl };
mock
.onGet(`${relativeRootUrl || ''}/awards`, { params: { per_page: 100, page: '1' } })
- .reply(200, ['thumbsup'], { 'x-next-page': '2' });
+ .reply(HTTP_STATUS_OK, ['thumbsup'], { 'x-next-page': '2' });
mock
.onGet(`${relativeRootUrl || ''}/awards`, { params: { per_page: 100, page: '2' } })
- .reply(200, ['thumbsdown']);
+ .reply(HTTP_STATUS_OK, ['thumbsdown']);
});
it('commits FETCH_AWARDS_SUCCESS', async () => {
@@ -61,7 +62,7 @@ describe('Awards app actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet('/awards').reply(500);
+ mock.onGet('/awards').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('calls Sentry.captureException', async () => {
@@ -115,7 +116,7 @@ describe('Awards app actions', () => {
describe('adding new award', () => {
describe('success', () => {
beforeEach(() => {
- mock.onPost(`${relativeRootUrl || ''}/awards`).reply(200, { id: 1 });
+ mock.onPost(`${relativeRootUrl || ''}/awards`).reply(HTTP_STATUS_OK, { id: 1 });
});
it('adds an optimistic award, removes it, and then commits ADD_NEW_AWARD', async () => {
@@ -129,7 +130,7 @@ describe('Awards app actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onPost(`${relativeRootUrl || ''}/awards`).reply(500);
+ mock.onPost(`${relativeRootUrl || ''}/awards`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('calls Sentry.captureException', async () => {
@@ -152,7 +153,7 @@ describe('Awards app actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(200);
+ mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(HTTP_STATUS_OK);
});
it('commits REMOVE_AWARD', async () => {
@@ -174,7 +175,9 @@ describe('Awards app actions', () => {
const name = 'thumbsup';
beforeEach(() => {
- mock.onDelete(`${relativeRootUrl || ''}/awards/1`).reply(500);
+ mock
+ .onDelete(`${relativeRootUrl || ''}/awards/1`)
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('calls Sentry.captureException', async () => {
diff --git a/spec/frontend/environments/deploy_board_component_spec.js b/spec/frontend/environments/deploy_board_component_spec.js
index c005ca22070..73a366457fb 100644
--- a/spec/frontend/environments/deploy_board_component_spec.js
+++ b/spec/frontend/environments/deploy_board_component_spec.js
@@ -1,6 +1,6 @@
import { GlTooltip, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
+import { nextTick } from 'vue';
import CanaryIngress from '~/environments/components/canary_ingress.vue';
import DeployBoard from '~/environments/components/deploy_board.vue';
import { deployBoardMockData } from './mock_data';
@@ -10,7 +10,7 @@ describe('Deploy Board', () => {
let wrapper;
const createComponent = (props = {}) =>
- mount(Vue.extend(DeployBoard), {
+ mount(DeployBoard, {
propsData: {
deployBoardData: deployBoardMockData,
isLoading: false,
diff --git a/spec/frontend/environments/edit_environment_spec.js b/spec/frontend/environments/edit_environment_spec.js
index 5ea23af4c16..fb1a8b8c00a 100644
--- a/spec/frontend/environments/edit_environment_spec.js
+++ b/spec/frontend/environments/edit_environment_spec.js
@@ -5,6 +5,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import EditEnvironment from '~/environments/components/edit_environment.vue';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
@@ -14,6 +15,7 @@ const DEFAULT_OPTS = {
provide: {
projectEnvironmentsPath: '/projects/environments',
updateEnvironmentPath: '/proejcts/environments/1',
+ protectedEnvironmentSettingsPath: '/projects/1/settings/ci_cd',
},
propsData: { environment: { id: '0', name: 'foo', external_url: 'https://foo.example.com' } },
};
@@ -67,7 +69,7 @@ describe('~/environments/components/edit.vue', () => {
expect(showsLoading()).toBe(false);
- await submitForm(expected, [200, { path: '/test' }]);
+ await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
expect(showsLoading()).toBe(true);
});
@@ -75,7 +77,7 @@ describe('~/environments/components/edit.vue', () => {
it('submits the updated environment on submit', async () => {
const expected = { url: 'https://google.ca' };
- await submitForm(expected, [200, { path: '/test' }]);
+ await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
expect(visitUrl).toHaveBeenCalledWith('/test');
});
@@ -83,7 +85,7 @@ describe('~/environments/components/edit.vue', () => {
it('shows errors on error', async () => {
const expected = { url: 'https://google.ca' };
- await submitForm(expected, [400, { message: ['uh oh!'] }]);
+ await submitForm(expected, [HTTP_STATUS_BAD_REQUEST, { message: ['uh oh!'] }]);
expect(createAlert).toHaveBeenCalledWith({ message: 'uh oh!' });
expect(showsLoading()).toBe(false);
diff --git a/spec/frontend/environments/environment_details/components/deployment_actions_spec.js b/spec/frontend/environments/environment_details/components/deployment_actions_spec.js
new file mode 100644
index 00000000000..725c8c6479e
--- /dev/null
+++ b/spec/frontend/environments/environment_details/components/deployment_actions_spec.js
@@ -0,0 +1,47 @@
+import DeploymentActions from '~/environments/environment_details/components/deployment_actions.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ActionsComponent from '~/environments/components/environment_actions.vue';
+
+describe('~/environments/environment_details/components/deployment_actions.vue', () => {
+ let wrapper;
+
+ const actionsData = [
+ {
+ playable: true,
+ playPath: 'http://www.example.com/play',
+ name: 'deploy-staging',
+ scheduledAt: '2023-01-18T08:50:08.390Z',
+ },
+ ];
+
+ const createWrapper = ({ actions }) => {
+ return mountExtended(DeploymentActions, {
+ propsData: {
+ actions,
+ },
+ });
+ };
+
+ describe('when there is no actions provided', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ actions: [] });
+ });
+
+ it('should not render actions component', () => {
+ const actionsComponent = wrapper.findComponent(ActionsComponent);
+ expect(actionsComponent.exists()).toBe(false);
+ });
+ });
+
+ describe('when there are actions provided', () => {
+ beforeEach(() => {
+ wrapper = createWrapper({ actions: actionsData });
+ });
+
+ it('should render actions component', () => {
+ const actionsComponent = wrapper.findComponent(ActionsComponent);
+ expect(actionsComponent.exists()).toBe(true);
+ expect(actionsComponent.props().actions).toBe(actionsData);
+ });
+ });
+});
diff --git a/spec/frontend/environments/environment_details/deployment_job_spec.js b/spec/frontend/environments/environment_details/components/deployment_job_spec.js
index 9bb61abb293..9bb61abb293 100644
--- a/spec/frontend/environments/environment_details/deployment_job_spec.js
+++ b/spec/frontend/environments/environment_details/components/deployment_job_spec.js
diff --git a/spec/frontend/environments/environment_details/deployment_status_link_spec.js b/spec/frontend/environments/environment_details/components/deployment_status_link_spec.js
index 5db7740423a..5db7740423a 100644
--- a/spec/frontend/environments/environment_details/deployment_status_link_spec.js
+++ b/spec/frontend/environments/environment_details/components/deployment_status_link_spec.js
diff --git a/spec/frontend/environments/environment_details/deployment_triggerer_spec.js b/spec/frontend/environments/environment_details/components/deployment_triggerer_spec.js
index 48af82661bf..48af82661bf 100644
--- a/spec/frontend/environments/environment_details/deployment_triggerer_spec.js
+++ b/spec/frontend/environments/environment_details/components/deployment_triggerer_spec.js
diff --git a/spec/frontend/environments/environment_form_spec.js b/spec/frontend/environments/environment_form_spec.js
index f1af08bcf32..b9b34bee80f 100644
--- a/spec/frontend/environments/environment_form_spec.js
+++ b/spec/frontend/environments/environment_form_spec.js
@@ -10,11 +10,14 @@ const DEFAULT_PROPS = {
cancelPath: '/cancel',
};
+const PROVIDE = { protectedEnvironmentSettingsPath: '/projects/not_real/settings/ci_cd' };
+
describe('~/environments/components/form.vue', () => {
let wrapper;
const createWrapper = (propsData = {}) =>
mountExtended(EnvironmentForm, {
+ provide: PROVIDE,
propsData: {
...DEFAULT_PROPS,
...propsData,
@@ -31,7 +34,7 @@ describe('~/environments/components/form.vue', () => {
});
it('links to documentation regarding environments', () => {
- const link = wrapper.findByRole('link', { name: 'More information' });
+ const link = wrapper.findByRole('link', { name: 'More information.' });
expect(link.attributes('href')).toBe('/help/ci/environments/index.md');
});
@@ -124,6 +127,10 @@ describe('~/environments/components/form.vue', () => {
expect(urlInput.element.value).toBe('');
});
+
+ it('does not show protected environment documentation', () => {
+ expect(wrapper.findByRole('link', { name: 'Protected environments' }).exists()).toBe(false);
+ });
});
describe('when an existing environment is being edited', () => {
diff --git a/spec/frontend/environments/environments_app_spec.js b/spec/frontend/environments/environments_app_spec.js
index 65a9f2907d2..986ecca4e84 100644
--- a/spec/frontend/environments/environments_app_spec.js
+++ b/spec/frontend/environments/environments_app_spec.js
@@ -195,6 +195,36 @@ describe('~/environments/components/environments_app.vue', () => {
expect(button.exists()).toBe(false);
});
+ it('should not show a button to clean up environments if the user has no permissions', async () => {
+ await createWrapperWithMocked({
+ environmentsApp: {
+ ...resolvedEnvironmentsApp,
+ canStopStaleEnvironments: false,
+ },
+ folder: resolvedFolder,
+ });
+
+ const button = wrapper.findByRole('button', {
+ name: s__('Environments|Clean up environments'),
+ });
+ expect(button.exists()).toBe(false);
+ });
+
+ it('should show a button to clean up environments if the user has permissions', async () => {
+ await createWrapperWithMocked({
+ environmentsApp: {
+ ...resolvedEnvironmentsApp,
+ canStopStaleEnvironments: true,
+ },
+ folder: resolvedFolder,
+ });
+
+ const button = wrapper.findByRole('button', {
+ name: s__('Environments|Clean up environments'),
+ });
+ expect(button.exists()).toBe(true);
+ });
+
describe('tabs', () => {
it('should show tabs for available and stopped environmets', async () => {
await createWrapperWithMocked({
diff --git a/spec/frontend/environments/environments_folder_view_spec.js b/spec/frontend/environments/environments_folder_view_spec.js
index 72a7449f24e..a87060f83d8 100644
--- a/spec/frontend/environments/environments_folder_view_spec.js
+++ b/spec/frontend/environments/environments_folder_view_spec.js
@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { environmentsList } from './mock_data';
describe('Environments Folder View', () => {
@@ -29,7 +30,7 @@ describe('Environments Folder View', () => {
describe('successful request', () => {
beforeEach(() => {
mock.onGet(mockData.endpoint).reply(
- 200,
+ HTTP_STATUS_OK,
{
environments: environmentsList,
stopped_count: 1,
diff --git a/spec/frontend/environments/folder/environments_folder_view_spec.js b/spec/frontend/environments/folder/environments_folder_view_spec.js
index f8b8465cf6f..23506eb018d 100644
--- a/spec/frontend/environments/folder/environments_folder_view_spec.js
+++ b/spec/frontend/environments/folder/environments_folder_view_spec.js
@@ -5,6 +5,7 @@ import { removeBreakLine, removeWhitespace } from 'helpers/text_helper';
import EnvironmentTable from '~/environments/components/environments_table.vue';
import EnvironmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { environmentsList } from '../mock_data';
describe('Environments Folder View', () => {
@@ -22,7 +23,7 @@ describe('Environments Folder View', () => {
const mockEnvironments = (environmentList) => {
mock.onGet(mockData.endpoint).reply(
- 200,
+ HTTP_STATUS_OK,
{
environments: environmentList,
stopped_count: 1,
@@ -54,7 +55,6 @@ describe('Environments Folder View', () => {
afterEach(() => {
mock.restore();
- wrapper.destroy();
});
describe('successful request', () => {
@@ -95,32 +95,12 @@ describe('Environments Folder View', () => {
it('should render pagination', () => {
expect(wrapper.findComponent(GlPagination).exists()).toBe(true);
});
-
- it('should make an API request when changing page', () => {
- jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
- wrapper.find('.gl-pagination .page-item:nth-last-of-type(2) .page-link').trigger('click');
- expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
- scope: wrapper.vm.scope,
- page: '10',
- nested: true,
- });
- });
-
- it('should make an API request when using tabs', () => {
- jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
- findEnvironmentsTabStopped().trigger('click');
- expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
- scope: 'stopped',
- page: '1',
- nested: true,
- });
- });
});
});
describe('unsuccessfull request', () => {
beforeEach(() => {
- mock.onGet(mockData.endpoint).reply(500, { environments: [] });
+ mock.onGet(mockData.endpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, { environments: [] });
createWrapper();
return axios.waitForAll();
});
@@ -160,29 +140,5 @@ describe('Environments Folder View', () => {
expect(wrapper.vm.requestData.page).toEqual('4');
}));
});
-
- describe('onChangeTab', () => {
- it('should set page to 1', () => {
- jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
- wrapper.vm.onChangeTab('stopped');
- expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
- scope: 'stopped',
- page: '1',
- nested: true,
- });
- });
- });
-
- describe('onChangePage', () => {
- it('should update page and keep scope', () => {
- jest.spyOn(wrapper.vm, 'updateContent').mockImplementation(() => {});
- wrapper.vm.onChangePage(4);
- expect(wrapper.vm.updateContent).toHaveBeenCalledWith({
- scope: wrapper.vm.scope,
- page: '4',
- nested: true,
- });
- });
- });
});
});
diff --git a/spec/frontend/environments/graphql/mock_data.js b/spec/frontend/environments/graphql/mock_data.js
index 355b77b55c3..5ea0be41614 100644
--- a/spec/frontend/environments/graphql/mock_data.js
+++ b/spec/frontend/environments/graphql/mock_data.js
@@ -166,7 +166,7 @@ export const environmentsApp = {
title: 'Play',
path: '/h5bp/html5-boilerplate/-/jobs/911/play',
method: 'post',
- button_title: 'Trigger this manual action',
+ button_title: 'Run job',
},
},
},
@@ -265,6 +265,7 @@ export const environmentsApp = {
review_snippet:
'{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
},
+ can_stop_stale_environments: true,
available_count: 4,
stopped_count: 0,
};
@@ -373,7 +374,7 @@ export const resolvedEnvironmentsApp = {
title: 'Play',
path: '/h5bp/html5-boilerplate/-/jobs/911/play',
method: 'post',
- buttonTitle: 'Trigger this manual action',
+ buttonTitle: 'Run job',
},
},
},
@@ -474,6 +475,7 @@ export const resolvedEnvironmentsApp = {
'{"deploy_review"=>{"stage"=>"deploy", "script"=>["echo \\"Deploy a review app\\""], "environment"=>{"name"=>"review/$CI_COMMIT_REF_NAME", "url"=>"https://$CI_ENVIRONMENT_SLUG.example.com"}, "only"=>["branches"]}}',
__typename: 'ReviewApp',
},
+ canStopStaleEnvironments: true,
stoppedCount: 0,
__typename: 'LocalEnvironmentApp',
};
@@ -673,7 +675,7 @@ export const resolvedEnvironment = {
title: 'Play',
path: '/h5bp/html5-boilerplate/-/jobs/1015/play',
method: 'post',
- buttonTitle: 'Trigger this manual action',
+ buttonTitle: 'Run job',
},
},
},
diff --git a/spec/frontend/environments/graphql/resolvers_spec.js b/spec/frontend/environments/graphql/resolvers_spec.js
index 7684cca2303..2c223d3a1a7 100644
--- a/spec/frontend/environments/graphql/resolvers_spec.js
+++ b/spec/frontend/environments/graphql/resolvers_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { s__ } from '~/locale';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { resolvers } from '~/environments/graphql/resolvers';
import environmentToRollback from '~/environments/graphql/queries/environment_to_rollback.query.graphql';
import environmentToDelete from '~/environments/graphql/queries/environment_to_delete.query.graphql';
@@ -44,7 +45,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const search = '';
mock
.onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search } })
- .reply(200, environmentsApp, {});
+ .reply(HTTP_STATUS_OK, environmentsApp, {});
const app = await mockResolvers.Query.environmentApp(
null,
@@ -63,7 +64,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const interval = 3000;
mock
.onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search: '' } })
- .reply(200, environmentsApp, {
+ .reply(HTTP_STATUS_OK, environmentsApp, {
'poll-interval': interval,
});
@@ -78,7 +79,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const scope = 'stopped';
mock
.onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search: '' } })
- .reply(200, environmentsApp, {
+ .reply(HTTP_STATUS_OK, environmentsApp, {
'x-next-page': '2',
'x-page': '1',
'X-Per-Page': '2',
@@ -108,7 +109,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
const scope = 'stopped';
mock
.onGet(ENDPOINT, { params: { nested: true, scope, page: 1, search: '' } })
- .reply(200, environmentsApp, {});
+ .reply(HTTP_STATUS_OK, environmentsApp, {});
await mockResolvers.Query.environmentApp(null, { scope, page: 1, search: '' }, { cache });
expect(cache.writeQuery).toHaveBeenCalledWith({
@@ -131,7 +132,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
it('should fetch the folder url passed to it', async () => {
mock
.onGet(ENDPOINT, { params: { per_page: 3, scope: 'available', search: '' } })
- .reply(200, folder);
+ .reply(HTTP_STATUS_OK, folder);
const environmentFolder = await mockResolvers.Query.folder(null, {
environment: { folderPath: ENDPOINT },
@@ -144,7 +145,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('stopEnvironment', () => {
it('should post to the stop environment path', async () => {
- mock.onPost(ENDPOINT).reply(200);
+ mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
const client = { writeQuery: jest.fn() };
const environment = { stopPath: ENDPOINT };
@@ -161,7 +162,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
});
it('should set is stopping to false if stop fails', async () => {
- mock.onPost(ENDPOINT).reply(500);
+ mock.onPost(ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
const client = { writeQuery: jest.fn() };
const environment = { stopPath: ENDPOINT };
@@ -180,7 +181,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('rollbackEnvironment', () => {
it('should post to the retry environment path', async () => {
- mock.onPost(ENDPOINT).reply(200);
+ mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
await mockResolvers.Mutation.rollbackEnvironment(null, {
environment: { retryUrl: ENDPOINT },
@@ -193,7 +194,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('deleteEnvironment', () => {
it('should DELETE to the delete environment path', async () => {
- mock.onDelete(ENDPOINT).reply(200);
+ mock.onDelete(ENDPOINT).reply(HTTP_STATUS_OK);
await mockResolvers.Mutation.deleteEnvironment(null, {
environment: { deletePath: ENDPOINT },
@@ -206,7 +207,7 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('cancelAutoStop', () => {
it('should post to the auto stop path', async () => {
- mock.onPost(ENDPOINT).reply(200);
+ mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
await mockResolvers.Mutation.cancelAutoStop(null, { autoStopUrl: ENDPOINT });
@@ -262,13 +263,13 @@ describe('~/frontend/environments/graphql/resolvers', () => {
});
describe('action', () => {
it('should POST to the given path', async () => {
- mock.onPost(ENDPOINT).reply(200);
+ mock.onPost(ENDPOINT).reply(HTTP_STATUS_OK);
const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
expect(errors).toEqual({ __typename: 'LocalEnvironmentErrors', errors: [] });
});
it('should return a nice error message on fail', async () => {
- mock.onPost(ENDPOINT).reply(500);
+ mock.onPost(ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
const errors = await mockResolvers.Mutation.action(null, { action: { playPath: ENDPOINT } });
expect(errors).toEqual({
diff --git a/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap b/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap
index 401c10338c1..326a28bd769 100644
--- a/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap
+++ b/spec/frontend/environments/helpers/__snapshots__/deployment_data_transformation_helper_spec.js.snap
@@ -2,6 +2,14 @@
exports[`deployment_data_transformation_helper convertToDeploymentTableRow should be converted to proper table row data 1`] = `
Object {
+ "actions": Array [
+ Object {
+ "name": "deploy-staging",
+ "playPath": "https://gdk.test:3000/redeploy/play",
+ "playable": true,
+ "scheduledAt": "2023-01-17T11:02:41.369Z",
+ },
+ ],
"commit": Object {
"author": Object {
"avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
@@ -35,6 +43,7 @@ Object {
exports[`deployment_data_transformation_helper convertToDeploymentTableRow should be converted to proper table row data 2`] = `
Object {
+ "actions": Array [],
"commit": Object {
"author": Object {
"avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
@@ -65,6 +74,7 @@ Object {
exports[`deployment_data_transformation_helper convertToDeploymentTableRow should be converted to proper table row data 3`] = `
Object {
+ "actions": Array [],
"commit": Object {
"author": Object {
"avatar_url": "/uploads/-/system/user/avatar/1/avatar.png",
diff --git a/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js b/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js
index 8bb87c0a208..65bb804a58e 100644
--- a/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js
+++ b/spec/frontend/environments/helpers/deployment_data_transformation_helper_spec.js
@@ -23,7 +23,7 @@ describe('deployment_data_transformation_helper', () => {
},
};
- const commitWithourAuthor = {
+ const commitWithoutAuthor = {
id: 'gid://gitlab/CommitPresenter/02274a949a88c9aef68a29685d99bd9a661a7f9b',
shortId: '02274a94',
message: 'Commit message',
@@ -48,6 +48,24 @@ describe('deployment_data_transformation_helper', () => {
refName: 'main',
id: 'gid://gitlab/Ci::Build/860',
webPath: '/gitlab-org/pipelinestest/-/jobs/860',
+ deploymentPipeline: {
+ jobs: {
+ nodes: [
+ {
+ name: 'deploy-staging',
+ playable: true,
+ scheduledAt: '2023-01-17T11:02:41.369Z',
+ webPath: 'https://gdk.test:3000/redeploy',
+ },
+ {
+ name: 'deploy-production',
+ playable: true,
+ scheduledAt: '2023-01-17T11:02:41.369Z',
+ webPath: 'https://gdk.test:3000/redeploy',
+ },
+ ],
+ },
+ },
},
commit: commitWithAuthor,
triggerer: {
@@ -65,8 +83,15 @@ describe('deployment_data_transformation_helper', () => {
finishedAt: null,
};
+ const environment = {
+ lastDeployment: {
+ job: {
+ name: 'deploy-production',
+ },
+ },
+ };
describe('getAuthorFromCommit', () => {
- it.each([commitWithAuthor, commitWithourAuthor])('should be properly converted', (commit) => {
+ it.each([commitWithAuthor, commitWithoutAuthor])('should be properly converted', (commit) => {
expect(getAuthorFromCommit(commit)).toMatchSnapshot();
});
});
@@ -89,7 +114,7 @@ describe('deployment_data_transformation_helper', () => {
it.each([deploymentNode, deploymentNodeWithEmptyJob, deploymentNodeWithNoJob])(
'should be converted to proper table row data',
(node) => {
- expect(convertToDeploymentTableRow(node)).toMatchSnapshot();
+ expect(convertToDeploymentTableRow(node, environment)).toMatchSnapshot();
},
);
});
diff --git a/spec/frontend/environments/mixins/environments_pagination_api_mixin_spec.js b/spec/frontend/environments/mixins/environments_pagination_api_mixin_spec.js
new file mode 100644
index 00000000000..b624178e3db
--- /dev/null
+++ b/spec/frontend/environments/mixins/environments_pagination_api_mixin_spec.js
@@ -0,0 +1,69 @@
+import { shallowMount } from '@vue/test-utils';
+import environmentsPaginationApiMixin from '~/environments/mixins/environments_pagination_api_mixin';
+
+describe('environments_pagination_api_mixin', () => {
+ const updateContentMock = jest.fn();
+ const mockComponent = {
+ template: `
+ <div>
+ <button id='change-page' @click="changePageClick" />
+ <button id='change-tab' @click="changeTabClick" />
+ </div>
+ `,
+ methods: {
+ updateContent: updateContentMock,
+ changePageClick() {
+ this.onChangePage(this.nextPage);
+ },
+ changeTabClick() {
+ this.onChangeTab(this.nextScope);
+ },
+ },
+ data() {
+ return {
+ scope: 'test',
+ };
+ },
+ };
+
+ let wrapper;
+
+ const createWrapper = ({ scope, nextPage, nextScope }) =>
+ shallowMount(mockComponent, {
+ mixins: [environmentsPaginationApiMixin],
+ data() {
+ return {
+ nextPage,
+ nextScope,
+ scope,
+ };
+ },
+ });
+
+ it.each([
+ ['test-scope', 2],
+ ['test-scope', 10],
+ ['test-scope-2', 3],
+ ])('should call updateContent when calling onChangePage', async (scopeName, pageNumber) => {
+ wrapper = createWrapper({ scope: scopeName, nextPage: pageNumber });
+
+ await wrapper.find('#change-page').trigger('click');
+
+ expect(updateContentMock).toHaveBeenCalledWith({
+ scope: scopeName,
+ page: pageNumber.toString(),
+ nested: true,
+ });
+ });
+
+ it('should call updateContent when calling onChageTab', async () => {
+ wrapper = createWrapper({ nextScope: 'stopped' });
+ await wrapper.find('#change-tab').trigger('click');
+
+ expect(updateContentMock).toHaveBeenCalledWith({
+ scope: 'stopped',
+ page: '1',
+ nested: true,
+ });
+ });
+});
diff --git a/spec/frontend/environments/new_environment_spec.js b/spec/frontend/environments/new_environment_spec.js
index 6dd4eea7437..a8cc05b297b 100644
--- a/spec/frontend/environments/new_environment_spec.js
+++ b/spec/frontend/environments/new_environment_spec.js
@@ -5,13 +5,17 @@ import waitForPromises from 'helpers/wait_for_promises';
import NewEnvironment from '~/environments/components/new_environment.vue';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility');
jest.mock('~/flash');
const DEFAULT_OPTS = {
- provide: { projectEnvironmentsPath: '/projects/environments' },
+ provide: {
+ projectEnvironmentsPath: '/projects/environments',
+ protectedEnvironmentSettingsPath: '/projects/not_real/settings/ci_cd',
+ },
};
describe('~/environments/components/new.vue', () => {
@@ -76,7 +80,7 @@ describe('~/environments/components/new.vue', () => {
expect(showsLoading()).toBe(false);
- await submitForm(expected, [200, { path: '/test' }]);
+ await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
expect(showsLoading()).toBe(true);
});
@@ -84,7 +88,7 @@ describe('~/environments/components/new.vue', () => {
it('submits the new environment on submit', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
- await submitForm(expected, [200, { path: '/test' }]);
+ await submitForm(expected, [HTTP_STATUS_OK, { path: '/test' }]);
expect(visitUrl).toHaveBeenCalledWith('/test');
});
@@ -92,7 +96,7 @@ describe('~/environments/components/new.vue', () => {
it('shows errors on error', async () => {
const expected = { name: 'test', url: 'https://google.ca' };
- await submitForm(expected, [400, { message: ['name taken'] }]);
+ await submitForm(expected, [HTTP_STATUS_BAD_REQUEST, { message: ['name taken'] }]);
expect(createAlert).toHaveBeenCalledWith({ message: 'name taken' });
expect(showsLoading()).toBe(false);
diff --git a/spec/frontend/environments/stop_stale_environments_modal_spec.js b/spec/frontend/environments/stop_stale_environments_modal_spec.js
new file mode 100644
index 00000000000..a2ab4f707b5
--- /dev/null
+++ b/spec/frontend/environments/stop_stale_environments_modal_spec.js
@@ -0,0 +1,60 @@
+import MockAdapter from 'axios-mock-adapter';
+import { GlModal } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import StopStaleEnvironmentsModal from '~/environments/components/stop_stale_environments_modal.vue';
+import axios from '~/lib/utils/axios_utils';
+import { getDateInPast } from '~/lib/utils/datetime_utility';
+import { STOP_STALE_ENVIRONMENTS_PATH } from '~/api/environments_api';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+
+const DEFAULT_OPTS = {
+ provide: { projectId: 1 },
+};
+
+const ONE_WEEK_AGO = getDateInPast(new Date(), 7);
+const TEN_YEARS_AGO = getDateInPast(new Date(), 3650);
+
+describe('~/environments/components/stop_stale_environments_modal.vue', () => {
+ let wrapper;
+ let mock;
+ let before;
+ let originalGon;
+
+ const createWrapper = (opts = {}) =>
+ shallowMount(StopStaleEnvironmentsModal, {
+ ...DEFAULT_OPTS,
+ ...opts,
+ propsData: { modalId: 'stop-stale-environments-modal', visible: true },
+ });
+
+ beforeEach(() => {
+ originalGon = window.gon;
+ window.gon = { api_version: 'v4' };
+
+ mock = new MockAdapter(axios);
+ jest.spyOn(axios, 'post');
+ wrapper = createWrapper();
+ before = wrapper.find("[data-testid='stop-environments-before']");
+ });
+
+ afterEach(() => {
+ mock.restore();
+ wrapper.destroy();
+ jest.resetAllMocks();
+ window.gon = originalGon;
+ });
+
+ it('sets the correct min and max dates', async () => {
+ expect(before.props().minDate.toISOString()).toBe(TEN_YEARS_AGO.toISOString());
+ expect(before.props().maxDate.toISOString()).toBe(ONE_WEEK_AGO.toISOString());
+ });
+
+ it('requests cleanup when submit is clicked', async () => {
+ mock.onPost().replyOnce(HTTP_STATUS_OK);
+ wrapper.findComponent(GlModal).vm.$emit('primary');
+ const url = STOP_STALE_ENVIRONMENTS_PATH.replace(':id', 1).replace(':version', 'v4');
+ expect(axios.post).toHaveBeenCalledWith(url, null, {
+ params: { before: ONE_WEEK_AGO.toISOString() },
+ });
+ });
+});
diff --git a/spec/frontend/error_tracking/store/actions_spec.js b/spec/frontend/error_tracking/store/actions_spec.js
index 8f085282f80..3ec43010d80 100644
--- a/spec/frontend/error_tracking/store/actions_spec.js
+++ b/spec/frontend/error_tracking/store/actions_spec.js
@@ -4,6 +4,7 @@ import * as actions from '~/error_tracking/store/actions';
import * as types from '~/error_tracking/store/mutation_types';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/flash.js');
@@ -29,7 +30,7 @@ describe('Sentry common store actions', () => {
describe('updateStatus', () => {
it('should handle successful status update', async () => {
- mock.onPut().reply(200, {});
+ mock.onPut().reply(HTTP_STATUS_OK, {});
await testAction(
actions.updateStatus,
params,
@@ -46,7 +47,7 @@ describe('Sentry common store actions', () => {
});
it('should handle unsuccessful status update', async () => {
- mock.onPut().reply(400, {});
+ mock.onPut().reply(HTTP_STATUS_BAD_REQUEST, {});
await testAction(actions.updateStatus, params, {}, [], []);
expect(visitUrl).not.toHaveBeenCalled();
expect(createAlert).toHaveBeenCalledTimes(1);
diff --git a/spec/frontend/error_tracking/store/details/actions_spec.js b/spec/frontend/error_tracking/store/details/actions_spec.js
index 1893d226270..383d8aaeb20 100644
--- a/spec/frontend/error_tracking/store/details/actions_spec.js
+++ b/spec/frontend/error_tracking/store/details/actions_spec.js
@@ -4,6 +4,11 @@ import * as actions from '~/error_tracking/store/details/actions';
import * as types from '~/error_tracking/store/details/mutation_types';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_BAD_REQUEST,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
let mockedAdapter;
@@ -30,7 +35,7 @@ describe('Sentry error details store actions', () => {
const endpoint = '123/stacktrace';
it('should commit SET_ERROR with received response', () => {
const payload = { error: [1, 2, 3] };
- mockedAdapter.onGet().reply(200, payload);
+ mockedAdapter.onGet().reply(HTTP_STATUS_OK, payload);
return testAction(
actions.startPollingStacktrace,
{ endpoint },
@@ -44,7 +49,7 @@ describe('Sentry error details store actions', () => {
});
it('should show flash on API error', async () => {
- mockedAdapter.onGet().reply(400);
+ mockedAdapter.onGet().reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.startPollingStacktrace,
@@ -58,7 +63,7 @@ describe('Sentry error details store actions', () => {
it('should not restart polling when receiving an empty 204 response', async () => {
mockedRestart = jest.spyOn(Poll.prototype, 'restart');
- mockedAdapter.onGet().reply(204);
+ mockedAdapter.onGet().reply(HTTP_STATUS_NO_CONTENT);
await testAction(actions.startPollingStacktrace, { endpoint }, {}, [], []);
mockedRestart = jest.spyOn(Poll.prototype, 'restart');
diff --git a/spec/frontend/error_tracking_settings/store/actions_spec.js b/spec/frontend/error_tracking_settings/store/actions_spec.js
index bcd816c2ae0..d8f61be6df7 100644
--- a/spec/frontend/error_tracking_settings/store/actions_spec.js
+++ b/spec/frontend/error_tracking_settings/store/actions_spec.js
@@ -6,6 +6,7 @@ import * as types from '~/error_tracking_settings/store/mutation_types';
import defaultState from '~/error_tracking_settings/store/state';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { refreshCurrentPage } from '~/lib/utils/url_utility';
import { projectList } from '../mock';
@@ -28,7 +29,7 @@ describe('error tracking settings actions', () => {
});
it('should request and transform the project list', async () => {
- mock.onGet(TEST_HOST).reply(() => [200, { projects: projectList }]);
+ mock.onGet(TEST_HOST).reply(() => [HTTP_STATUS_OK, { projects: projectList }]);
await testAction(
actions.fetchProjects,
null,
@@ -46,7 +47,7 @@ describe('error tracking settings actions', () => {
});
it('should handle a server error', async () => {
- mock.onGet(`${TEST_HOST}.json`).reply(() => [400]);
+ mock.onGet(`${TEST_HOST}.json`).reply(() => [HTTP_STATUS_BAD_REQUEST]);
await testAction(
actions.fetchProjects,
null,
@@ -118,14 +119,14 @@ describe('error tracking settings actions', () => {
});
it('should save the page', async () => {
- mock.onPatch(TEST_HOST).reply(200);
+ mock.onPatch(TEST_HOST).reply(HTTP_STATUS_OK);
await testAction(actions.updateSettings, null, state, [], [{ type: 'requestSettings' }]);
expect(mock.history.patch.length).toBe(1);
expect(refreshCurrentPage).toHaveBeenCalled();
});
it('should handle a server error', async () => {
- mock.onPatch(TEST_HOST).reply(400);
+ mock.onPatch(TEST_HOST).reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.updateSettings,
null,
diff --git a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
index 05709cd05e6..cf4605e21ea 100644
--- a/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
+++ b/spec/frontend/feature_flags/components/edit_feature_flag_spec.js
@@ -10,6 +10,7 @@ import EditFeatureFlag from '~/feature_flags/components/edit_feature_flag.vue';
import Form from '~/feature_flags/components/form.vue';
import createStore from '~/feature_flags/store/edit';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
Vue.use(Vuex);
@@ -35,7 +36,7 @@ describe('Edit feature flag form', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(`${TEST_HOST}/feature_flags.json`).replyOnce(200, {
+ mock.onGet(`${TEST_HOST}/feature_flags.json`).replyOnce(HTTP_STATUS_OK, {
id: 21,
iid: 5,
active: true,
diff --git a/spec/frontend/feature_flags/components/feature_flags_spec.js b/spec/frontend/feature_flags/components/feature_flags_spec.js
index d27b23c5cd1..e80f9c559c4 100644
--- a/spec/frontend/feature_flags/components/feature_flags_spec.js
+++ b/spec/frontend/feature_flags/components/feature_flags_spec.js
@@ -11,6 +11,7 @@ import FeatureFlagsComponent from '~/feature_flags/components/feature_flags.vue'
import FeatureFlagsTable from '~/feature_flags/components/feature_flags_table.vue';
import createStore from '~/feature_flags/store/index';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import TablePagination from '~/vue_shared/components/pagination/table_pagination.vue';
import { getRequestData } from '../mock_data';
@@ -74,7 +75,7 @@ describe('Feature flags', () => {
beforeEach(() => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
- .reply(200, getRequestData, {});
+ .reply(HTTP_STATUS_OK, getRequestData, {});
factory(provideData);
return waitForPromises();
});
@@ -119,7 +120,7 @@ describe('Feature flags', () => {
beforeEach(() => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
- .reply(200, getRequestData, {});
+ .reply(HTTP_STATUS_OK, getRequestData, {});
factory(provideData);
return waitForPromises();
});
@@ -141,7 +142,7 @@ describe('Feature flags', () => {
it('renders a loading icon', () => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
- .replyOnce(200, getRequestData, {});
+ .replyOnce(HTTP_STATUS_OK, getRequestData, {});
factory();
@@ -158,7 +159,7 @@ describe('Feature flags', () => {
beforeEach(async () => {
mock.onGet(mockState.endpoint, { params: { page: '1' } }).reply(
- 200,
+ HTTP_STATUS_OK,
{
feature_flags: [],
count: {
@@ -203,14 +204,16 @@ describe('Feature flags', () => {
describe('with paginated feature flags', () => {
beforeEach(() => {
- mock.onGet(mockState.endpoint, { params: { page: '1' } }).replyOnce(200, getRequestData, {
- 'x-next-page': '2',
- 'x-page': '1',
- 'X-Per-Page': '2',
- 'X-Prev-Page': '',
- 'X-TOTAL': '37',
- 'X-Total-Pages': '5',
- });
+ mock
+ .onGet(mockState.endpoint, { params: { page: '1' } })
+ .replyOnce(HTTP_STATUS_OK, getRequestData, {
+ 'x-next-page': '2',
+ 'x-page': '1',
+ 'X-Per-Page': '2',
+ 'X-Prev-Page': '',
+ 'X-TOTAL': '37',
+ 'X-Total-Pages': '5',
+ });
factory();
jest.spyOn(store, 'dispatch');
@@ -271,7 +274,9 @@ describe('Feature flags', () => {
describe('unsuccessful request', () => {
beforeEach(() => {
- mock.onGet(mockState.endpoint, { params: { page: '1' } }).replyOnce(500, {});
+ mock
+ .onGet(mockState.endpoint, { params: { page: '1' } })
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
factory();
return waitForPromises();
@@ -303,7 +308,7 @@ describe('Feature flags', () => {
beforeEach(() => {
mock
.onGet(`${TEST_HOST}/endpoint.json`, { params: { page: '1' } })
- .reply(200, getRequestData, {});
+ .reply(HTTP_STATUS_OK, getRequestData, {});
factory();
return waitForPromises();
});
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 b71cdf78207..14e1f34bc59 100644
--- a/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
+++ b/spec/frontend/feature_flags/components/new_environments_dropdown_spec.js
@@ -49,7 +49,7 @@ describe('New Environments Dropdown', () => {
describe('with empty results', () => {
let item;
beforeEach(async () => {
- axiosMock.onGet(TEST_HOST).reply(200, []);
+ axiosMock.onGet(TEST_HOST).reply(HTTP_STATUS_OK, []);
wrapper.findComponent(GlSearchBoxByType).vm.$emit('focus');
wrapper.findComponent(GlSearchBoxByType).vm.$emit('input', TEST_SEARCH);
await axios.waitForAll();
diff --git a/spec/frontend/feature_flags/store/edit/actions_spec.js b/spec/frontend/feature_flags/store/edit/actions_spec.js
index 7132e83a940..8b9b42f4eb1 100644
--- a/spec/frontend/feature_flags/store/edit/actions_spec.js
+++ b/spec/frontend/feature_flags/store/edit/actions_spec.js
@@ -17,6 +17,7 @@ import * as types from '~/feature_flags/store/edit/mutation_types';
import state from '~/feature_flags/store/edit/state';
import { mapStrategiesToRails } from '~/feature_flags/store/helpers';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/lib/utils/url_utility');
@@ -55,7 +56,9 @@ describe('Feature flags Edit Module actions', () => {
},
],
};
- mock.onPut(mockedState.endpoint, mapStrategiesToRails(featureFlag)).replyOnce(200);
+ mock
+ .onPut(mockedState.endpoint, mapStrategiesToRails(featureFlag))
+ .replyOnce(HTTP_STATUS_OK);
return testAction(
updateFeatureFlag,
@@ -76,7 +79,9 @@ describe('Feature flags Edit Module actions', () => {
describe('error', () => {
it('dispatches requestUpdateFeatureFlag and receiveUpdateFeatureFlagError', () => {
- mock.onPut(`${TEST_HOST}/endpoint.json`).replyOnce(500, { message: [] });
+ mock
+ .onPut(`${TEST_HOST}/endpoint.json`)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, { message: [] });
return testAction(
updateFeatureFlag,
@@ -155,7 +160,7 @@ describe('Feature flags Edit Module actions', () => {
describe('success', () => {
it('dispatches requestFeatureFlag and receiveFeatureFlagSuccess', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 1 });
+ mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(HTTP_STATUS_OK, { id: 1 });
return testAction(
fetchFeatureFlag,
@@ -177,7 +182,9 @@ describe('Feature flags Edit Module actions', () => {
describe('error', () => {
it('dispatches requestFeatureFlag and receiveUpdateFeatureFlagError', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {});
+ mock
+ .onGet(`${TEST_HOST}/endpoint.json`, {})
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
return testAction(
fetchFeatureFlag,
diff --git a/spec/frontend/feature_flags/store/index/actions_spec.js b/spec/frontend/feature_flags/store/index/actions_spec.js
index 96a7d868316..46a7843b937 100644
--- a/spec/frontend/feature_flags/store/index/actions_spec.js
+++ b/spec/frontend/feature_flags/store/index/actions_spec.js
@@ -20,6 +20,7 @@ import {
import * as types from '~/feature_flags/store/index/mutation_types';
import state from '~/feature_flags/store/index/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { getRequestData, rotateData, featureFlag } from '../../mock_data';
jest.mock('~/api.js');
@@ -57,7 +58,7 @@ describe('Feature flags actions', () => {
describe('success', () => {
it('dispatches requestFeatureFlags and receiveFeatureFlagsSuccess', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, getRequestData, {});
+ mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(HTTP_STATUS_OK, getRequestData, {});
return testAction(
fetchFeatureFlags,
@@ -79,7 +80,9 @@ describe('Feature flags actions', () => {
describe('error', () => {
it('dispatches requestFeatureFlags and receiveFeatureFlagsError', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {});
+ mock
+ .onGet(`${TEST_HOST}/endpoint.json`, {})
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
return testAction(
fetchFeatureFlags,
@@ -154,7 +157,7 @@ describe('Feature flags actions', () => {
describe('success', () => {
it('dispatches requestRotateInstanceId and receiveRotateInstanceIdSuccess', () => {
- mock.onPost(`${TEST_HOST}/endpoint.json`).replyOnce(200, rotateData, {});
+ mock.onPost(`${TEST_HOST}/endpoint.json`).replyOnce(HTTP_STATUS_OK, rotateData, {});
return testAction(
rotateInstanceId,
@@ -176,7 +179,9 @@ describe('Feature flags actions', () => {
describe('error', () => {
it('dispatches requestRotateInstanceId and receiveRotateInstanceIdError', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`, {}).replyOnce(500, {});
+ mock
+ .onGet(`${TEST_HOST}/endpoint.json`, {})
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
return testAction(
rotateInstanceId,
@@ -252,7 +257,7 @@ describe('Feature flags actions', () => {
});
describe('success', () => {
it('dispatches updateFeatureFlag and receiveUpdateFeatureFlagSuccess', () => {
- mock.onPut(featureFlag.update_path).replyOnce(200, featureFlag, {});
+ mock.onPut(featureFlag.update_path).replyOnce(HTTP_STATUS_OK, featureFlag, {});
return testAction(
toggleFeatureFlag,
@@ -275,7 +280,7 @@ describe('Feature flags actions', () => {
describe('error', () => {
it('dispatches updateFeatureFlag and receiveUpdateFeatureFlagSuccess', () => {
- mock.onPut(featureFlag.update_path).replyOnce(500);
+ mock.onPut(featureFlag.update_path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
toggleFeatureFlag,
diff --git a/spec/frontend/feature_flags/store/new/actions_spec.js b/spec/frontend/feature_flags/store/new/actions_spec.js
index dbe6669c868..01b6ab4d5ed 100644
--- a/spec/frontend/feature_flags/store/new/actions_spec.js
+++ b/spec/frontend/feature_flags/store/new/actions_spec.js
@@ -11,6 +11,7 @@ import {
import * as types from '~/feature_flags/store/new/mutation_types';
import state from '~/feature_flags/store/new/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/lib/utils/url_utility');
@@ -48,7 +49,9 @@ describe('Feature flags New Module Actions', () => {
},
],
};
- mock.onPost(mockedState.endpoint, mapStrategiesToRails(actionParams)).replyOnce(200);
+ mock
+ .onPost(mockedState.endpoint, mapStrategiesToRails(actionParams))
+ .replyOnce(HTTP_STATUS_OK);
return testAction(
createFeatureFlag,
@@ -85,7 +88,7 @@ describe('Feature flags New Module Actions', () => {
};
mock
.onPost(mockedState.endpoint, mapStrategiesToRails(actionParams))
- .replyOnce(500, { message: [] });
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, { message: [] });
return testAction(
createFeatureFlag,
diff --git a/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js b/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js
index dff6d11a320..30e1bfe94b5 100644
--- a/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_dropdown_manager_spec.js
@@ -2,13 +2,14 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilteredSearchDropdownManager from '~/filtered_search/filtered_search_dropdown_manager';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Filtered Search Dropdown Manager', () => {
let mock;
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet().reply(200);
+ mock.onGet().reply(HTTP_STATUS_OK);
});
describe('addWordToInput', () => {
diff --git a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
index 28fcf0b7ec7..ec0c712f959 100644
--- a/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/frontend/filtered_search/filtered_search_visual_tokens_spec.js
@@ -4,6 +4,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import FilteredSearchSpecHelper from 'helpers/filtered_search_spec_helper';
import waitForPromises from 'helpers/wait_for_promises';
import FilteredSearchVisualTokens from '~/filtered_search/filtered_search_visual_tokens';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { FILTERED_SEARCH_TERM } from '~/vue_shared/components/filtered_search_bar/constants';
describe('Filtered Search Visual Tokens', () => {
@@ -24,7 +25,7 @@ describe('Filtered Search Visual Tokens', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet().reply(200);
+ mock.onGet().reply(HTTP_STATUS_OK);
setHTMLFixture(`
<ul class="tokens-container">
diff --git a/spec/frontend/filtered_search/visual_token_value_spec.js b/spec/frontend/filtered_search/visual_token_value_spec.js
index 43c10090739..d3fa8fae9ab 100644
--- a/spec/frontend/filtered_search/visual_token_value_spec.js
+++ b/spec/frontend/filtered_search/visual_token_value_spec.js
@@ -79,7 +79,7 @@ describe('Filtered Search Visual Tokens', () => {
it('replaces author token with avatar and display name', async () => {
const dummyUser = {
name: 'Important Person',
- avatar_url: 'https://host.invalid/mypics/avatar.png',
+ avatar_url: `${TEST_HOST}/mypics/avatar.png`,
};
const { subject, tokenValueContainer, tokenValueElement } = findElements(authorToken);
const tokenValue = tokenValueElement.innerText;
diff --git a/spec/frontend/fixtures/jobs.rb b/spec/frontend/fixtures/jobs.rb
index ac58b99875b..6d452bf1bff 100644
--- a/spec/frontend/fixtures/jobs.rb
+++ b/spec/frontend/fixtures/jobs.rb
@@ -39,6 +39,7 @@ RSpec.describe 'Jobs (JavaScript fixtures)' do
let!(:build) { create(:ci_build, :success, name: 'build', pipeline: pipeline) }
let!(:cancelable) { create(:ci_build, :cancelable, name: 'cancelable', pipeline: pipeline) }
+ let!(:failed) { create(:ci_build, :failed, name: 'failed', pipeline: pipeline) }
let!(:created_by_tag) { create(:ci_build, :success, name: 'created_by_tag', tag: true, pipeline: pipeline) }
let!(:pending) { create(:ci_build, :pending, name: 'pending', pipeline: pipeline) }
let!(:playable) { create(:ci_build, :playable, name: 'playable', pipeline: pipeline) }
diff --git a/spec/frontend/fixtures/listbox.rb b/spec/frontend/fixtures/listbox.rb
index 8f8489a2827..8f746f1707a 100644
--- a/spec/frontend/fixtures/listbox.rb
+++ b/spec/frontend/fixtures/listbox.rb
@@ -26,6 +26,9 @@ RSpec.describe 'initRedirectListboxBehavior', '(JavaScript fixtures)', type: :he
arbitrary_key: 'qux xyz'
}]
- @tag = helper.gl_redirect_listbox_tag(items, 'bar', class: %w[test-class-1 test-class-2], data: { right: true })
+ @tag = helper.gl_redirect_listbox_tag(items, 'bar',
+ class: %w[test-class-1 test-class-2],
+ data: { placement: 'right' }
+ )
end
end
diff --git a/spec/frontend/fixtures/merge_requests.rb b/spec/frontend/fixtures/merge_requests.rb
index 18f89fbc5e5..7ee89ca3694 100644
--- a/spec/frontend/fixtures/merge_requests.rb
+++ b/spec/frontend/fixtures/merge_requests.rb
@@ -148,6 +148,53 @@ RSpec.describe Projects::MergeRequestsController, '(JavaScript fixtures)', type:
end
end
+ context 'merge request with no approvals' do
+ base_input_path = 'vue_merge_request_widget/components/approvals/queries/'
+ base_output_path = 'graphql/merge_requests/approvals/'
+ query_name = 'approved_by.query.graphql'
+
+ it "#{base_output_path}#{query_name}_no_approvals.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: Gitlab.ee?)
+
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: merge_request.iid.to_s })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'merge request approved by current user' do
+ base_input_path = 'vue_merge_request_widget/components/approvals/queries/'
+ base_output_path = 'graphql/merge_requests/approvals/'
+ query_name = 'approved_by.query.graphql'
+
+ it "#{base_output_path}#{query_name}.json" do
+ merge_request.approved_by_users << user
+
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: Gitlab.ee?)
+
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: merge_request.iid.to_s })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'merge request approved by multiple users' do
+ base_input_path = 'vue_merge_request_widget/components/approvals/queries/'
+ base_output_path = 'graphql/merge_requests/approvals/'
+ query_name = 'approved_by.query.graphql'
+
+ it "#{base_output_path}#{query_name}_multiple_users.json" do
+ merge_request.approved_by_users << user
+ merge_request.approved_by_users << create(:user)
+
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}", ee: Gitlab.ee?)
+
+ post_graphql(query, current_user: user, variables: { projectPath: project.full_path, iid: merge_request.iid.to_s })
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
context 'merge request in state getState query' do
base_input_path = 'vue_merge_request_widget/queries/'
base_output_path = 'graphql/merge_requests/'
diff --git a/spec/frontend/fixtures/pipelines.rb b/spec/frontend/fixtures/pipelines.rb
index 44b471a70d8..768934d6278 100644
--- a/spec/frontend/fixtures/pipelines.rb
+++ b/spec/frontend/fixtures/pipelines.rb
@@ -23,8 +23,19 @@ RSpec.describe Projects::PipelinesController, '(JavaScript fixtures)', type: :co
let!(:build_test) { create(:ci_build, pipeline: pipeline, stage: 'test') }
let!(:build_deploy_failed) { create(:ci_build, status: :failed, pipeline: pipeline, stage: 'deploy') }
+ let(:bridge) { create(:ci_bridge, pipeline: pipeline) }
+ let(:retried_bridge) { create(:ci_bridge, :retried, pipeline: pipeline) }
+
+ let(:downstream_pipeline) { create(:ci_pipeline, :with_job) }
+ let(:retried_downstream_pipeline) { create(:ci_pipeline, :with_job) }
+ let!(:ci_sources_pipeline) { create(:ci_sources_pipeline, pipeline: downstream_pipeline, source_job: bridge) }
+ let!(:retried_ci_sources_pipeline) do
+ create(:ci_sources_pipeline, pipeline: retried_downstream_pipeline, source_job: retried_bridge)
+ end
+
before do
sign_in(user)
+ project.add_developer(user)
end
it 'pipelines/pipelines.json' do
diff --git a/spec/frontend/fixtures/runner.rb b/spec/frontend/fixtures/runner.rb
index de87114766e..f60e4991292 100644
--- a/spec/frontend/fixtures/runner.rb
+++ b/spec/frontend/fixtures/runner.rb
@@ -30,7 +30,7 @@ RSpec.describe 'Runner (JavaScript fixtures)' do
before do
allow_next_instance_of(::Gitlab::Ci::RunnerUpgradeCheck) do |instance|
allow(instance).to receive(:check_runner_upgrade_suggestion)
- .and_return([nil, :not_available])
+ .and_return([nil, :unavailable])
end
end
diff --git a/spec/frontend/fixtures/saved_replies.rb b/spec/frontend/fixtures/saved_replies.rb
new file mode 100644
index 00000000000..c80ba06bca1
--- /dev/null
+++ b/spec/frontend/fixtures/saved_replies.rb
@@ -0,0 +1,46 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+RSpec.describe GraphQL::Query, type: :request, feature_category: :user_profile do
+ include JavaScriptFixturesHelpers
+ include ApiHelpers
+ include GraphqlHelpers
+
+ let_it_be(:current_user) { create(:user) }
+
+ before do
+ sign_in(current_user)
+ end
+
+ context 'when user has no saved replies' do
+ base_input_path = 'saved_replies/queries/'
+ base_output_path = 'graphql/saved_replies/'
+ query_name = 'saved_replies.query.graphql'
+
+ it "#{base_output_path}saved_replies_empty.query.graphql.json" do
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: current_user)
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+
+ context 'when user has saved replies' do
+ base_input_path = 'saved_replies/queries/'
+ base_output_path = 'graphql/saved_replies/'
+ query_name = 'saved_replies.query.graphql'
+
+ it "#{base_output_path}saved_replies.query.graphql.json" do
+ create(:saved_reply, user: current_user)
+ create(:saved_reply, user: current_user)
+
+ query = get_graphql_query_as_string("#{base_input_path}#{query_name}")
+
+ post_graphql(query, current_user: current_user)
+
+ expect_graphql_errors_to_be_empty
+ end
+ end
+end
diff --git a/spec/frontend/fixtures/static/project_select_combo_button.html b/spec/frontend/fixtures/static/project_select_combo_button.html
deleted file mode 100644
index 3776610ed4c..00000000000
--- a/spec/frontend/fixtures/static/project_select_combo_button.html
+++ /dev/null
@@ -1,13 +0,0 @@
-<div class="project-item-select-holder">
- <input class="project-item-select" data-group-id="12345" data-relative-path="issues/new" />
- <a class="js-new-project-item-link" data-label="issue" data-type="issues" href="">
- <span class="gl-spinner"></span>
- </a>
- <a class="new-project-item-select-button">
- <svg data-testid="chevron-down-icon" class="gl-icon s16">
- <use
- href="/assets/icons-795a2ef2fd636a0538bbef3b8d2787dd90927b42d7617fdda8620930016b333d.svg#chevron-down"
- ></use>
- </svg>
- </a>
-</div>
diff --git a/spec/frontend/flash_spec.js b/spec/frontend/flash_spec.js
index 2f0a52a9884..17d6cea23df 100644
--- a/spec/frontend/flash_spec.js
+++ b/spec/frontend/flash_spec.js
@@ -1,12 +1,6 @@
import * as Sentry from '@sentry/browser';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import {
- hideFlash,
- addDismissFlashClickListener,
- FLASH_CLOSED_EVENT,
- createAlert,
- VARIANT_WARNING,
-} from '~/flash';
+import { createAlert, VARIANT_WARNING } from '~/flash';
jest.mock('@sentry/browser');
@@ -14,65 +8,6 @@ describe('Flash', () => {
const findTextContent = (containerSelector = '.flash-container') =>
document.querySelector(containerSelector).textContent.replace(/\s+/g, ' ').trim();
- describe('hideFlash', () => {
- let el;
-
- beforeEach(() => {
- el = document.createElement('div');
- el.className = 'js-testing';
- });
-
- it('sets transition style', () => {
- hideFlash(el);
-
- expect(el.style.transition).toBe('opacity 0.15s');
- });
-
- it('sets opacity style', () => {
- hideFlash(el);
-
- expect(el.style.opacity).toBe('0');
- });
-
- it('does not set styles when fadeTransition is false', () => {
- hideFlash(el, false);
-
- expect(el.style.opacity).toBe('');
- expect(el.style.transition).toHaveLength(0);
- });
-
- it('removes element after transitionend', () => {
- document.body.appendChild(el);
-
- hideFlash(el);
- el.dispatchEvent(new Event('transitionend'));
-
- expect(document.querySelector('.js-testing')).toBeNull();
- });
-
- it('calls event listener callback once', () => {
- jest.spyOn(el, 'remove');
- document.body.appendChild(el);
-
- hideFlash(el);
-
- el.dispatchEvent(new Event('transitionend'));
- el.dispatchEvent(new Event('transitionend'));
-
- expect(el.remove.mock.calls.length).toBe(1);
- });
-
- it(`dispatches ${FLASH_CLOSED_EVENT} event after transitionend event`, () => {
- jest.spyOn(el, 'dispatchEvent');
-
- hideFlash(el);
-
- el.dispatchEvent(new Event('transitionend'));
-
- expect(el.dispatchEvent).toHaveBeenCalledWith(new Event(FLASH_CLOSED_EVENT));
- });
- });
-
describe('createAlert', () => {
const mockMessage = 'a message';
let alert;
@@ -338,45 +273,4 @@ describe('Flash', () => {
});
});
});
-
- describe('addDismissFlashClickListener', () => {
- let el;
-
- describe('with close icon', () => {
- beforeEach(() => {
- el = document.createElement('div');
- el.innerHTML = `
- <div class="flash-container">
- <div class="flash">
- <div class="close-icon js-close-icon"></div>
- </div>
- </div>
- `;
- });
-
- it('removes global flash on click', () => {
- addDismissFlashClickListener(el, false);
-
- el.querySelector('.js-close-icon').click();
-
- expect(document.querySelector('.flash')).toBeNull();
- });
- });
-
- describe('without close icon', () => {
- beforeEach(() => {
- el = document.createElement('div');
- el.innerHTML = `
- <div class="flash-container">
- <div class="flash">
- </div>
- </div>
- `;
- });
-
- it('does not throw', () => {
- expect(() => addDismissFlashClickListener(el, false)).not.toThrow();
- });
- });
- });
});
diff --git a/spec/frontend/frequent_items/components/app_spec.js b/spec/frontend/frequent_items/components/app_spec.js
index b1e87aca63d..e1890555de0 100644
--- a/spec/frontend/frequent_items/components/app_spec.js
+++ b/spec/frontend/frequent_items/components/app_spec.js
@@ -12,6 +12,7 @@ import eventHub from '~/frequent_items/event_hub';
import { createStore } from '~/frequent_items/store';
import { getTopFrequentItems } from '~/frequent_items/utils';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { currentSession, mockFrequentProjects, mockSearchedProjects } from '../mock_data';
Vue.use(Vuex);
@@ -32,6 +33,7 @@ describe('Frequent Items App Component', () => {
const createComponent = (props = {}) => {
const session = currentSession[TEST_NAMESPACE];
gon.api_version = session.apiVersion;
+ gon.features = { fullPathProjectSearch: true };
wrapper = mountExtended(App, {
store,
@@ -115,7 +117,9 @@ describe('Frequent Items App Component', () => {
});
it('should render searched projects list', async () => {
- mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects.data);
+ mock
+ .onGet(/\/api\/v4\/projects.json(.*)$/)
+ .replyOnce(HTTP_STATUS_OK, mockSearchedProjects.data);
setSearch('gitlab');
await nextTick();
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 4f2badf869d..c54a2a1d039 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
@@ -154,7 +154,8 @@ describe('FrequentItemsListItemComponent', () => {
link.vm.$emit('click');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_link', {
- label: 'projects_dropdown_frequent_items_list_item_git_lab_community_edition',
+ label: 'projects_dropdown_frequent_items_list_item',
+ property: 'navigation_top',
});
});
});
diff --git a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
index 94fc97b82c2..dfce88ca0a8 100644
--- a/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
+++ b/spec/frontend/frequent_items/components/frequent_items_search_input_spec.js
@@ -65,6 +65,7 @@ describe('FrequentItemsSearchInputComponent', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'type_search_query', {
label: 'projects_dropdown_frequent_items_search_input',
+ property: 'navigation_top',
});
expect(store.dispatch).toHaveBeenCalledWith('frequentProjects/setSearchQuery', value);
});
diff --git a/spec/frontend/frequent_items/store/actions_spec.js b/spec/frontend/frequent_items/store/actions_spec.js
index 4f998cc26da..c228bca4973 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 { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import {
mockNamespace,
@@ -24,6 +25,7 @@ describe('Frequent Items Dropdown Store Actions', () => {
mockedState.namespace = mockNamespace;
mockedState.storageKey = mockStorageKey;
+ gon.features = { fullPathProjectSearch: true };
});
afterEach(() => {
@@ -173,7 +175,9 @@ describe('Frequent Items Dropdown Store Actions', () => {
});
it('should dispatch `receiveSearchedItemsSuccess`', () => {
- mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(200, mockSearchedProjects, {});
+ mock
+ .onGet(/\/api\/v4\/projects.json(.*)$/)
+ .replyOnce(HTTP_STATUS_OK, mockSearchedProjects, {});
return testAction(
actions.fetchSearchedItems,
@@ -192,7 +196,7 @@ describe('Frequent Items Dropdown Store Actions', () => {
it('should dispatch `receiveSearchedItemsError`', () => {
gon.api_version = 'v4';
- mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(500);
+ mock.onGet(/\/api\/v4\/projects.json(.*)$/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
actions.fetchSearchedItems,
diff --git a/spec/frontend/gfm_auto_complete_spec.js b/spec/frontend/gfm_auto_complete_spec.js
index cc2dc084e47..e4fd8649263 100644
--- a/spec/frontend/gfm_auto_complete_spec.js
+++ b/spec/frontend/gfm_auto_complete_spec.js
@@ -17,6 +17,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import AjaxCache from '~/lib/utils/ajax_cache';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
eventlistenersMockDefaultMap,
crmContactsMock,
@@ -184,17 +185,20 @@ describe('GfmAutoComplete', () => {
});
});
- it.each([200, 500])('should set the loading state', async (responseStatus) => {
- mock.onGet('vulnerabilities_autocomplete_url').replyOnce(responseStatus);
+ it.each([HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR])(
+ 'should set the loading state',
+ async (responseStatus) => {
+ mock.onGet('vulnerabilities_autocomplete_url').replyOnce(responseStatus);
- fetchData.call(context, {}, '[vulnerability:', 'query');
+ fetchData.call(context, {}, '[vulnerability:', 'query');
- expect(context.isLoadingData['[vulnerability:']).toBe(true);
+ expect(context.isLoadingData['[vulnerability:']).toBe(true);
- await waitForPromises();
+ await waitForPromises();
- expect(context.isLoadingData['[vulnerability:']).toBe(false);
- });
+ expect(context.isLoadingData['[vulnerability:']).toBe(false);
+ },
+ );
});
describe('data is in cache', () => {
diff --git a/spec/frontend/gl_form_spec.js b/spec/frontend/gl_form_spec.js
index ab5627ce216..6ad9d9f4338 100644
--- a/spec/frontend/gl_form_spec.js
+++ b/spec/frontend/gl_form_spec.js
@@ -6,6 +6,47 @@ import '~/lib/utils/common_utils';
describe('GLForm', () => {
const testContext = {};
+ const mockGl = {
+ GfmAutoComplete: {
+ dataSources: {
+ commands: '/group/projects/-/autocomplete_sources/commands',
+ },
+ },
+ };
+
+ describe('Setting up GfmAutoComplete', () => {
+ describe('setupForm', () => {
+ let setupFormSpy;
+
+ beforeEach(() => {
+ setupFormSpy = jest.spyOn(GLForm.prototype, 'setupForm');
+
+ testContext.form = $('<form class="gfm-form"><textarea class="js-gfm-input"></form>');
+ testContext.textarea = testContext.form.find('textarea');
+ });
+
+ it('should be called with the global data source `windows.gl`', () => {
+ window.gl = { ...mockGl };
+ testContext.glForm = new GLForm(testContext.form, {}, false);
+
+ expect(setupFormSpy).toHaveBeenCalledTimes(1);
+ expect(setupFormSpy).toHaveBeenCalledWith(window.gl.GfmAutoComplete.dataSources, false);
+ });
+
+ it('should be called with the provided custom data source', () => {
+ window.gl = { ...mockGl };
+
+ const customDataSources = {
+ foobar: '/group/projects/-/autocomplete_sources/foobar',
+ };
+
+ testContext.glForm = new GLForm(testContext.form, {}, false, customDataSources);
+
+ expect(setupFormSpy).toHaveBeenCalledTimes(1);
+ expect(setupFormSpy).toHaveBeenCalledWith(customDataSources, false);
+ });
+ });
+ });
describe('when instantiated', () => {
beforeEach(() => {
diff --git a/spec/frontend/gpg_badges_spec.js b/spec/frontend/gpg_badges_spec.js
index 0a1596b492d..2d961e6872e 100644
--- a/spec/frontend/gpg_badges_spec.js
+++ b/spec/frontend/gpg_badges_spec.js
@@ -3,6 +3,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'spec/test_constants';
import GpgBadges from '~/gpg_badges';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('GpgBadges', () => {
let mock;
@@ -27,7 +28,7 @@ describe('GpgBadges', () => {
<input type="search" name="search" value="${search}" id="commits-search"class="form-control search-text-input input-short">
</form>
<div class="parent-container">
- <div class="js-loading-gpg-badge" data-commit-sha="${dummyCommitSha}"></div>
+ <div class="js-loading-signature-badge" data-commit-sha="${dummyCommitSha}"></div>
</div>
`);
};
@@ -63,7 +64,7 @@ describe('GpgBadges', () => {
});
it('fetches commit signatures', async () => {
- mock.onGet(dummyUrl).replyOnce(200);
+ mock.onGet(dummyUrl).replyOnce(HTTP_STATUS_OK);
await GpgBadges.fetch();
@@ -75,7 +76,7 @@ describe('GpgBadges', () => {
});
it('fetches commit signatures with search parameters with spaces', async () => {
- mock.onGet(dummyUrl).replyOnce(200);
+ mock.onGet(dummyUrl).replyOnce(HTTP_STATUS_OK);
setForm({ search: 'my search' });
await GpgBadges.fetch();
@@ -88,7 +89,7 @@ describe('GpgBadges', () => {
});
it('fetches commit signatures with search parameters with plus symbols', async () => {
- mock.onGet(dummyUrl).replyOnce(200);
+ mock.onGet(dummyUrl).replyOnce(HTTP_STATUS_OK);
setForm({ search: 'my+search' });
await GpgBadges.fetch();
@@ -101,20 +102,20 @@ describe('GpgBadges', () => {
});
it('displays a loading spinner', async () => {
- mock.onGet(dummyUrl).replyOnce(200);
+ mock.onGet(dummyUrl).replyOnce(HTTP_STATUS_OK);
await GpgBadges.fetch();
- expect(document.querySelector('.js-loading-gpg-badge:empty')).toBe(null);
- const spinners = document.querySelectorAll('.js-loading-gpg-badge span.gl-spinner');
+ expect(document.querySelector('.js-loading-signature-badge:empty')).toBe(null);
+ const spinners = document.querySelectorAll('.js-loading-signature-badge span.gl-spinner');
expect(spinners.length).toBe(1);
});
it('replaces the loading spinner', async () => {
- mock.onGet(dummyUrl).replyOnce(200, dummyResponse);
+ mock.onGet(dummyUrl).replyOnce(HTTP_STATUS_OK, dummyResponse);
await GpgBadges.fetch();
- expect(document.querySelector('.js-loading-gpg-badge')).toBe(null);
+ expect(document.querySelector('.js-loading-signature-badge')).toBe(null);
const parentContainer = document.querySelector('.parent-container');
expect(parentContainer.innerHTML.trim()).toEqual(dummyBadgeHtml);
diff --git a/spec/frontend/graphql_shared/utils_spec.js b/spec/frontend/graphql_shared/utils_spec.js
index bf899e47d1c..cd334ef0d97 100644
--- a/spec/frontend/graphql_shared/utils_spec.js
+++ b/spec/frontend/graphql_shared/utils_spec.js
@@ -5,6 +5,7 @@ import {
convertToGraphQLIds,
convertFromGraphQLIds,
convertNodeIdsFromGraphQLIds,
+ getNodesOrDefault,
} from '~/graphql_shared/utils';
const mockType = 'Group';
@@ -134,3 +135,28 @@ describe('convertNodeIdsFromGraphQLIds', () => {
);
});
});
+
+describe('getNodesOrDefault', () => {
+ const mockDataWithNodes = {
+ users: {
+ nodes: [
+ { __typename: 'UserCore', id: 'gid://gitlab/User/44' },
+ { __typename: 'UserCore', id: 'gid://gitlab/User/42' },
+ { __typename: 'UserCore', id: 'gid://gitlab/User/41' },
+ ],
+ },
+ };
+
+ it.each`
+ desc | input | expected
+ ${'with nodes child'} | ${[mockDataWithNodes.users]} | ${mockDataWithNodes.users.nodes}
+ ${'with nodes child and "dne" as field'} | ${[mockDataWithNodes.users, 'dne']} | ${[]}
+ ${'with empty data object'} | ${[{ users: {} }]} | ${[]}
+ ${'with empty object'} | ${[{}]} | ${[]}
+ ${'with falsy value'} | ${[undefined]} | ${[]}
+ `('$desc', ({ input, expected }) => {
+ const result = getNodesOrDefault(...input);
+
+ expect(result).toEqual(expected);
+ });
+});
diff --git a/spec/frontend/groups/components/app_spec.js b/spec/frontend/groups/components/app_spec.js
index 140609161d4..4e6ddd89a55 100644
--- a/spec/frontend/groups/components/app_spec.js
+++ b/spec/frontend/groups/components/app_spec.js
@@ -11,6 +11,12 @@ import eventHub from '~/groups/event_hub';
import GroupsService from '~/groups/service/groups_service';
import GroupsStore from '~/groups/store/groups_store';
import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_BAD_REQUEST,
+ HTTP_STATUS_FORBIDDEN,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import * as urlUtilities from '~/lib/utils/url_utility';
import setWindowLocation from 'helpers/set_window_location_helper';
@@ -66,7 +72,7 @@ describe('AppComponent', () => {
beforeEach(async () => {
mock = new AxiosMockAdapter(axios);
- mock.onGet('/dashboard/groups.json').reply(200, mockGroups);
+ mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_OK, mockGroups);
Vue.component('GroupFolder', groupFolderComponent);
Vue.component('GroupItem', groupItemComponent);
setWindowLocation('?filter=foobar');
@@ -101,7 +107,7 @@ describe('AppComponent', () => {
});
it('should set headers to store for building pagination info when called with `updatePagination`', () => {
- mock.onGet('/dashboard/groups.json').reply(200, { headers: mockRawPageInfo });
+ mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_OK, { headers: mockRawPageInfo });
jest.spyOn(vm, 'updatePagination').mockImplementation(() => {});
@@ -112,7 +118,7 @@ describe('AppComponent', () => {
});
it('should show flash error when request fails', () => {
- mock.onGet('/dashboard/groups.json').reply(400);
+ mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_BAD_REQUEST);
jest.spyOn(window, 'scrollTo').mockImplementation(() => {});
return vm.fetchGroups({}).then(() => {
@@ -145,7 +151,7 @@ describe('AppComponent', () => {
});
it('should fetch matching set of groups when app is loaded with search query', () => {
- mock.onGet('/dashboard/groups.json').reply(200, mockSearchedGroups);
+ mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_OK, mockSearchedGroups);
const fetchPromise = vm.fetchAllGroups();
@@ -216,7 +222,7 @@ describe('AppComponent', () => {
});
it('should fetch children of given group and expand it if group is collapsed and children are not loaded', () => {
- mock.onGet('/dashboard/groups.json').reply(200, mockRawChildren);
+ mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_OK, mockRawChildren);
jest.spyOn(vm, 'fetchGroups');
jest.spyOn(vm.store, 'setGroupChildren').mockImplementation(() => {});
@@ -252,7 +258,7 @@ describe('AppComponent', () => {
});
it('should set `isChildrenLoading` back to `false` if load request fails', () => {
- mock.onGet('/dashboard/groups.json').reply(400);
+ mock.onGet('/dashboard/groups.json').reply(HTTP_STATUS_BAD_REQUEST);
vm.toggleChildren(groupItem);
@@ -321,7 +327,9 @@ describe('AppComponent', () => {
it('should show error flash message if request failed to leave group', () => {
const message = 'An error occurred. Please try again.';
- jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 500 });
+ jest
+ .spyOn(vm.service, 'leaveGroup')
+ .mockRejectedValue({ status: HTTP_STATUS_INTERNAL_SERVER_ERROR });
jest.spyOn(vm.store, 'removeGroup');
vm.leaveGroup();
@@ -336,7 +344,7 @@ describe('AppComponent', () => {
it('should show appropriate error flash message if request forbids to leave group', () => {
const message = 'Failed to leave the group. Please make sure you are not the only owner.';
- jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: 403 });
+ jest.spyOn(vm.service, 'leaveGroup').mockRejectedValue({ status: HTTP_STATUS_FORBIDDEN });
jest.spyOn(vm.store, 'removeGroup');
vm.leaveGroup(childGroupItem, groupItem);
diff --git a/spec/frontend/groups/components/invite_members_banner_spec.js b/spec/frontend/groups/components/invite_members_banner_spec.js
index d25b45bd662..4a385cb00ee 100644
--- a/spec/frontend/groups/components/invite_members_banner_spec.js
+++ b/spec/frontend/groups/components/invite_members_banner_spec.js
@@ -6,6 +6,7 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import InviteMembersBanner from '~/groups/components/invite_members_banner.vue';
import eventHub from '~/invite_members/event_hub';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/lib/utils/common_utils');
@@ -89,7 +90,7 @@ describe('InviteMembersBanner', () => {
it('sends the dismissEvent when the banner is dismissed', () => {
mockTrackingOnWrapper();
- mockAxios.onPost(provide.calloutsPath).replyOnce(200);
+ mockAxios.onPost(provide.calloutsPath).replyOnce(HTTP_STATUS_OK);
const dismissEvent = 'invite_members_banner_dismissed';
wrapper.findComponent(GlBanner).vm.$emit('close');
@@ -136,7 +137,7 @@ describe('InviteMembersBanner', () => {
});
it('should close the banner when dismiss is clicked', async () => {
- mockAxios.onPost(provide.calloutsPath).replyOnce(200);
+ mockAxios.onPost(provide.calloutsPath).replyOnce(HTTP_STATUS_OK);
expect(wrapper.findComponent(GlBanner).exists()).toBe(true);
wrapper.findComponent(GlBanner).vm.$emit('close');
diff --git a/spec/frontend/groups_projects/components/transfer_locations_spec.js b/spec/frontend/groups_projects/components/transfer_locations_spec.js
index 74424ee3230..77c0966ba1e 100644
--- a/spec/frontend/groups_projects/components/transfer_locations_spec.js
+++ b/spec/frontend/groups_projects/components/transfer_locations_spec.js
@@ -15,7 +15,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { __ } from '~/locale';
-import TransferLocations from '~/groups_projects/components/transfer_locations.vue';
+import TransferLocations, { i18n } from '~/groups_projects/components/transfer_locations.vue';
import { getTransferLocations } from '~/api/projects_api';
import currentUserNamespaceQuery from '~/projects/settings/graphql/queries/current_user_namespace.query.graphql';
@@ -374,4 +374,23 @@ describe('TransferLocations', () => {
expect(wrapper.findByRole('group', { name: label }).exists()).toBe(true);
});
});
+
+ describe('when there are no results', () => {
+ it('displays no results message', async () => {
+ mockResolvedGetTransferLocations({
+ data: [],
+ page: '1',
+ nextPage: null,
+ total: '0',
+ totalPages: '1',
+ prevPage: null,
+ });
+
+ createComponent({ propsData: { showUserTransferLocations: false } });
+
+ await showDropdown();
+
+ expect(wrapper.findComponent(GlDropdownItem).text()).toBe(i18n.NO_RESULTS_TEXT);
+ });
+ });
});
diff --git a/spec/frontend/header_search/components/app_spec.js b/spec/frontend/header_search/components/app_spec.js
index c714c269ca0..d6263c663d2 100644
--- a/spec/frontend/header_search/components/app_spec.js
+++ b/spec/frontend/header_search/components/app_spec.js
@@ -375,7 +375,7 @@ describe('HeaderSearchApp', () => {
expect(findHeaderSearchDropdown().exists()).toBe(true);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
label: 'global_search',
- property: 'top_navigation',
+ property: 'navigation_top',
});
});
@@ -388,7 +388,7 @@ describe('HeaderSearchApp', () => {
expect(findHeaderSearchDropdown().exists()).toBe(true);
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'focus_input', {
label: 'global_search',
- property: 'top_navigation',
+ property: 'navigation_top',
});
});
diff --git a/spec/frontend/header_search/store/actions_spec.js b/spec/frontend/header_search/store/actions_spec.js
index 1ae149128ca..bd93b0edadf 100644
--- a/spec/frontend/header_search/store/actions_spec.js
+++ b/spec/frontend/header_search/store/actions_spec.js
@@ -4,6 +4,7 @@ import * as actions from '~/header_search/store/actions';
import * as types from '~/header_search/store/mutation_types';
import initState from '~/header_search/store/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
MOCK_SEARCH,
MOCK_AUTOCOMPLETE_OPTIONS_RES,
@@ -37,9 +38,9 @@ describe('Header Search Store Actions', () => {
});
describe.each`
- axiosMock | type | expectedMutations
- ${{ method: 'onGet', code: 200, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]}
- ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]}
+ axiosMock | type | expectedMutations
+ ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_AUTOCOMPLETE_OPTIONS_RES }} | ${'success'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }, { type: types.RECEIVE_AUTOCOMPLETE_SUCCESS, payload: MOCK_AUTOCOMPLETE_OPTIONS_RES }]}
+ ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_AUTOCOMPLETE }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }, { type: types.RECEIVE_AUTOCOMPLETE_ERROR }]}
`('fetchAutocompleteOptions', ({ axiosMock, type, expectedMutations }) => {
describe(`on ${type}`, () => {
beforeEach(() => {
diff --git a/spec/frontend/helpers/init_simple_app_helper_spec.js b/spec/frontend/helpers/init_simple_app_helper_spec.js
new file mode 100644
index 00000000000..8dd3745e0ac
--- /dev/null
+++ b/spec/frontend/helpers/init_simple_app_helper_spec.js
@@ -0,0 +1,61 @@
+import { createWrapper } from '@vue/test-utils';
+import Vue from 'vue';
+import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import { initSimpleApp } from '~/helpers/init_simple_app_helper';
+
+const MockComponent = Vue.component('MockComponent', {
+ props: {
+ someKey: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ count: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ },
+ render: (createElement) => createElement('span'),
+});
+
+let wrapper;
+
+const findMock = () => wrapper.findComponent(MockComponent);
+
+const didCreateApp = () => wrapper !== undefined;
+
+const initMock = (html, props = {}) => {
+ setHTMLFixture(html);
+
+ const app = initSimpleApp('#mount-here', MockComponent, { props });
+
+ wrapper = app ? createWrapper(app) : undefined;
+};
+
+describe('helpers/init_simple_app_helper/initSimpleApp', () => {
+ afterEach(() => {
+ resetHTMLFixture();
+ });
+
+ it('mounts the component if the selector exists', async () => {
+ initMock('<div id="mount-here"></div>');
+
+ expect(findMock().exists()).toBe(true);
+ });
+
+ it('does not mount the component if selector does not exist', async () => {
+ initMock('<div id="do-not-mount-here"></div>');
+
+ expect(didCreateApp()).toBe(false);
+ });
+
+ it('passes the prop to the component if the prop exists', async () => {
+ initMock(`<div id="mount-here" data-view-model={"someKey":"thing","count":123}></div>`);
+
+ expect(findMock().props()).toEqual({
+ someKey: 'thing',
+ count: 123,
+ });
+ });
+});
diff --git a/spec/frontend/ide/components/panes/right_spec.js b/spec/frontend/ide/components/panes/right_spec.js
index 294f5eee863..1d81c3ea89d 100644
--- a/spec/frontend/ide/components/panes/right_spec.js
+++ b/spec/frontend/ide/components/panes/right_spec.js
@@ -68,31 +68,6 @@ describe('ide/components/panes/right.vue', () => {
});
});
- describe('clientside live preview tab', () => {
- it('is shown if there is a packageJson and clientsidePreviewEnabled', () => {
- Vue.set(store.state.entries, 'package.json', {
- name: 'package.json',
- });
- store.state.clientsidePreviewEnabled = true;
-
- createComponent();
-
- expect(wrapper.findComponent(CollapsibleSidebar).props('extensionTabs')).toEqual(
- expect.arrayContaining([
- expect.objectContaining({
- show: true,
- title: 'Live preview',
- views: expect.arrayContaining([
- expect.objectContaining({
- name: rightSidebarViews.clientSidePreview.name,
- }),
- ]),
- }),
- ]),
- );
- });
- });
-
describe('terminal tab', () => {
beforeEach(() => {
createComponent();
diff --git a/spec/frontend/ide/components/preview/clientside_spec.js b/spec/frontend/ide/components/preview/clientside_spec.js
deleted file mode 100644
index 51e6a9d9034..00000000000
--- a/spec/frontend/ide/components/preview/clientside_spec.js
+++ /dev/null
@@ -1,416 +0,0 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import Vue, { nextTick } from 'vue';
-import { dispatch } from 'codesandbox-api';
-import { SandpackClient } from '@codesandbox/sandpack-client';
-import Vuex from 'vuex';
-import waitForPromises from 'helpers/wait_for_promises';
-import Clientside from '~/ide/components/preview/clientside.vue';
-import { PING_USAGE_PREVIEW_KEY, PING_USAGE_PREVIEW_SUCCESS_KEY } from '~/ide/constants';
-import eventHub from '~/ide/eventhub';
-
-jest.mock('@codesandbox/sandpack-client', () => ({
- SandpackClient: jest.fn(),
-}));
-
-Vue.use(Vuex);
-
-const dummyPackageJson = () => ({
- raw: JSON.stringify({
- main: 'index.js',
- }),
-});
-const expectedSandpackOptions = () => ({
- files: {},
- entry: '/index.js',
- showOpenInCodeSandbox: true,
-});
-const expectedSandpackSettings = () => ({
- fileResolver: {
- isFile: expect.any(Function),
- readFile: expect.any(Function),
- },
-});
-
-describe('IDE clientside preview', () => {
- let wrapper;
- let store;
- const storeActions = {
- getFileData: jest.fn().mockReturnValue(Promise.resolve({})),
- getRawFileData: jest.fn().mockReturnValue(Promise.resolve('')),
- };
- const storeClientsideActions = {
- pingUsage: jest.fn().mockReturnValue(Promise.resolve({})),
- };
- const dispatchCodesandboxReady = () => dispatch({ type: 'done' });
-
- const createComponent = ({ state, getters } = {}) => {
- store = new Vuex.Store({
- state: {
- entries: {},
- links: {},
- ...state,
- },
- getters: {
- packageJson: () => '',
- currentProject: () => ({
- visibility: 'public',
- }),
- ...getters,
- },
- actions: storeActions,
- modules: {
- clientside: {
- namespaced: true,
- actions: storeClientsideActions,
- },
- },
- });
-
- wrapper = shallowMount(Clientside, {
- store,
- });
- };
-
- const createInitializedComponent = () => {
- createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- sandpackReady: true,
- client: {
- cleanup: jest.fn(),
- updatePreview: jest.fn(),
- },
- });
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('without main entry', () => {
- it('creates sandpack client', () => {
- createComponent();
- expect(SandpackClient).not.toHaveBeenCalled();
- });
- });
- describe('with main entry', () => {
- beforeEach(() => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
-
- return waitForPromises();
- });
-
- it('creates sandpack client', () => {
- expect(SandpackClient).toHaveBeenCalledWith(
- '#ide-preview',
- expectedSandpackOptions(),
- expectedSandpackSettings(),
- );
- });
-
- it('pings usage', () => {
- expect(storeClientsideActions.pingUsage).toHaveBeenCalledTimes(1);
- expect(storeClientsideActions.pingUsage).toHaveBeenCalledWith(
- expect.anything(),
- PING_USAGE_PREVIEW_KEY,
- );
- });
-
- it('pings usage success', async () => {
- dispatchCodesandboxReady();
- await nextTick();
- expect(storeClientsideActions.pingUsage).toHaveBeenCalledTimes(2);
- expect(storeClientsideActions.pingUsage).toHaveBeenCalledWith(
- expect.anything(),
- PING_USAGE_PREVIEW_SUCCESS_KEY,
- );
- });
- });
-
- describe('with codesandboxBundlerUrl', () => {
- const TEST_BUNDLER_URL = 'https://test.gitlab-static.test';
-
- beforeEach(() => {
- createComponent({
- getters: { packageJson: dummyPackageJson },
- state: { codesandboxBundlerUrl: TEST_BUNDLER_URL },
- });
-
- return waitForPromises();
- });
-
- it('creates sandpack client with bundlerURL', () => {
- expect(SandpackClient).toHaveBeenCalledWith('#ide-preview', expectedSandpackOptions(), {
- ...expectedSandpackSettings(),
- bundlerURL: TEST_BUNDLER_URL,
- });
- });
- });
-
- describe('with codesandboxBundlerURL', () => {
- beforeEach(() => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
-
- return waitForPromises();
- });
-
- it('creates sandpack client', () => {
- expect(SandpackClient).toHaveBeenCalledWith(
- '#ide-preview',
- {
- files: {},
- entry: '/index.js',
- showOpenInCodeSandbox: true,
- },
- {
- fileResolver: {
- isFile: expect.any(Function),
- readFile: expect.any(Function),
- },
- },
- );
- });
- });
-
- describe('computed', () => {
- describe('normalizedEntries', () => {
- it('returns flattened list of blobs with content', () => {
- createComponent({
- state: {
- entries: {
- 'index.js': { type: 'blob', raw: 'test' },
- 'index2.js': { type: 'blob', content: 'content' },
- tree: { type: 'tree' },
- empty: { type: 'blob' },
- },
- },
- });
-
- expect(wrapper.vm.normalizedEntries).toEqual({
- '/index.js': {
- code: 'test',
- },
- '/index2.js': {
- code: 'content',
- },
- });
- });
- });
-
- describe('mainEntry', () => {
- it('returns false when package.json is empty', () => {
- createComponent();
- expect(wrapper.vm.mainEntry).toBe(false);
- });
-
- it('returns main key from package.json', () => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
- expect(wrapper.vm.mainEntry).toBe('index.js');
- });
- });
-
- describe('showPreview', () => {
- it('returns false if no mainEntry', () => {
- createComponent();
- expect(wrapper.vm.showPreview).toBe(false);
- });
-
- it('returns false if loading and mainEntry exists', () => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: true });
-
- expect(wrapper.vm.showPreview).toBe(false);
- });
-
- it('returns true if not loading and mainEntry exists', () => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: false });
-
- expect(wrapper.vm.showPreview).toBe(true);
- });
- });
-
- describe('showEmptyState', () => {
- it('returns true if no mainEntry exists', () => {
- createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: false });
- expect(wrapper.vm.showEmptyState).toBe(true);
- });
-
- it('returns false if loading', () => {
- createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: true });
-
- expect(wrapper.vm.showEmptyState).toBe(false);
- });
-
- it('returns false if not loading and mainEntry exists', () => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: false });
-
- expect(wrapper.vm.showEmptyState).toBe(false);
- });
- });
-
- describe('showOpenInCodeSandbox', () => {
- it('returns true when visibility is public', () => {
- createComponent({ getters: { currentProject: () => ({ visibility: 'public' }) } });
-
- expect(wrapper.vm.showOpenInCodeSandbox).toBe(true);
- });
-
- it('returns false when visibility is private', () => {
- createComponent({ getters: { currentProject: () => ({ visibility: 'private' }) } });
-
- expect(wrapper.vm.showOpenInCodeSandbox).toBe(false);
- });
- });
-
- describe('sandboxOpts', () => {
- beforeEach(() => {
- createComponent({
- state: {
- entries: {
- 'index.js': { type: 'blob', raw: 'test' },
- 'package.json': dummyPackageJson(),
- },
- },
- getters: {
- packageJson: dummyPackageJson,
- },
- });
- });
-
- it('returns sandbox options', () => {
- expect(wrapper.vm.sandboxOpts).toEqual({
- files: {
- '/index.js': {
- code: 'test',
- },
- '/package.json': {
- code: '{"main":"index.js"}',
- },
- },
- entry: '/index.js',
- showOpenInCodeSandbox: true,
- });
- });
- });
- });
-
- describe('methods', () => {
- describe('loadFileContent', () => {
- beforeEach(() => {
- createComponent();
- return wrapper.vm.loadFileContent('package.json');
- });
-
- it('calls getFileData', () => {
- expect(storeActions.getFileData).toHaveBeenCalledWith(expect.any(Object), {
- path: 'package.json',
- makeFileActive: false,
- });
- });
-
- it('calls getRawFileData', () => {
- expect(storeActions.getRawFileData).toHaveBeenCalledWith(expect.any(Object), {
- path: 'package.json',
- });
- });
- });
-
- describe('update', () => {
- it('initializes client if client is empty', () => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ sandpackReady: true });
- wrapper.vm.update();
-
- return waitForPromises().then(() => {
- expect(SandpackClient).toHaveBeenCalled();
- });
- });
-
- it('calls updatePreview', () => {
- createInitializedComponent();
-
- wrapper.vm.update();
-
- expect(wrapper.vm.client.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
- });
- });
-
- describe('on ide.files.change event', () => {
- beforeEach(() => {
- createInitializedComponent();
-
- eventHub.$emit('ide.files.change');
- });
-
- it('calls updatePreview', () => {
- expect(wrapper.vm.client.updatePreview).toHaveBeenCalledWith(wrapper.vm.sandboxOpts);
- });
- });
- });
-
- describe('template', () => {
- it('renders ide-preview element when showPreview is true', async () => {
- createComponent({ getters: { packageJson: dummyPackageJson } });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: false });
-
- await nextTick();
- expect(wrapper.find('#ide-preview').exists()).toBe(true);
- });
-
- it('renders empty state', async () => {
- createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: false });
-
- await nextTick();
- expect(wrapper.text()).toContain(
- 'Preview your web application using Web IDE client-side evaluation.',
- );
- });
-
- it('renders loading icon', async () => {
- createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ loading: true });
-
- await nextTick();
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
- });
-
- describe('when destroyed', () => {
- let spy;
-
- beforeEach(() => {
- createInitializedComponent();
- spy = wrapper.vm.client.updatePreview;
- wrapper.destroy();
- });
-
- it('does not call updatePreview', () => {
- expect(spy).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/ide/components/preview/navigator_spec.js b/spec/frontend/ide/components/preview/navigator_spec.js
deleted file mode 100644
index 043dcade858..00000000000
--- a/spec/frontend/ide/components/preview/navigator_spec.js
+++ /dev/null
@@ -1,161 +0,0 @@
-import { GlLoadingIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-import { listen } from 'codesandbox-api';
-import { nextTick } from 'vue';
-import { TEST_HOST } from 'helpers/test_constants';
-import ClientsideNavigator from '~/ide/components/preview/navigator.vue';
-
-jest.mock('codesandbox-api', () => ({
- listen: jest.fn().mockReturnValue(jest.fn()),
-}));
-
-describe('IDE clientside preview navigator', () => {
- let wrapper;
- let client;
- let listenHandler;
-
- const findBackButton = () => wrapper.findAll('button').at(0);
- const findForwardButton = () => wrapper.findAll('button').at(1);
- const findRefreshButton = () => wrapper.findAll('button').at(2);
-
- beforeEach(() => {
- listen.mockClear();
- client = { bundlerURL: TEST_HOST, iframe: { src: '' } };
-
- wrapper = shallowMount(ClientsideNavigator, { propsData: { client } });
- [[listenHandler]] = listen.mock.calls;
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders readonly URL bar', async () => {
- listenHandler({ type: 'urlchange', url: client.bundlerURL });
- await nextTick();
- expect(wrapper.find('input[readonly]').element.value).toBe('/');
- });
-
- it('renders loading icon by default', () => {
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(true);
- });
-
- it('removes loading icon when done event is fired', async () => {
- listenHandler({ type: 'done' });
- await nextTick();
- expect(wrapper.findComponent(GlLoadingIcon).exists()).toBe(false);
- });
-
- it('does not count visiting same url multiple times', async () => {
- listenHandler({ type: 'done' });
- listenHandler({ type: 'done', url: `${TEST_HOST}/url1` });
- listenHandler({ type: 'done', url: `${TEST_HOST}/url1` });
- await nextTick();
- expect(findBackButton().attributes('disabled')).toBe('disabled');
- });
-
- it('unsubscribes from listen on destroy', () => {
- const unsubscribeFn = listen();
-
- wrapper.destroy();
- expect(unsubscribeFn).toHaveBeenCalled();
- });
-
- describe('back button', () => {
- beforeEach(async () => {
- listenHandler({ type: 'done' });
- listenHandler({ type: 'urlchange', url: TEST_HOST });
- await nextTick();
- });
-
- it('is disabled by default', () => {
- expect(findBackButton().attributes('disabled')).toBe('disabled');
- });
-
- it('is enabled when there is previous entry', async () => {
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
- await nextTick();
- findBackButton().trigger('click');
- expect(findBackButton().attributes()).not.toHaveProperty('disabled');
- });
-
- it('is disabled when there is no previous entry', async () => {
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
-
- await nextTick();
- findBackButton().trigger('click');
-
- await nextTick();
- expect(findBackButton().attributes('disabled')).toBe('disabled');
- });
-
- it('updates client iframe src', async () => {
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url2` });
- await nextTick();
- findBackButton().trigger('click');
-
- expect(client.iframe.src).toBe(`${TEST_HOST}/url1`);
- });
- });
-
- describe('forward button', () => {
- beforeEach(async () => {
- listenHandler({ type: 'done' });
- listenHandler({ type: 'urlchange', url: TEST_HOST });
- await nextTick();
- });
-
- it('is disabled by default', () => {
- expect(findForwardButton().attributes('disabled')).toBe('disabled');
- });
-
- it('is enabled when there is next entry', async () => {
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
-
- await nextTick();
- findBackButton().trigger('click');
-
- await nextTick();
- expect(findForwardButton().attributes()).not.toHaveProperty('disabled');
- });
-
- it('is disabled when there is no next entry', async () => {
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
-
- await nextTick();
- findBackButton().trigger('click');
-
- await nextTick();
- findForwardButton().trigger('click');
-
- await nextTick();
- expect(findForwardButton().attributes('disabled')).toBe('disabled');
- });
-
- it('updates client iframe src', async () => {
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url1` });
- listenHandler({ type: 'urlchange', url: `${TEST_HOST}/url2` });
- await nextTick();
- findBackButton().trigger('click');
-
- expect(client.iframe.src).toBe(`${TEST_HOST}/url1`);
- });
- });
-
- describe('refresh button', () => {
- const url = `${TEST_HOST}/some_url`;
- beforeEach(async () => {
- listenHandler({ type: 'done' });
- listenHandler({ type: 'urlchange', url });
- await nextTick();
- });
-
- it('calls refresh with current path', () => {
- client.iframe.src = 'something-other';
- findRefreshButton().trigger('click');
-
- expect(client.iframe.src).toBe(url);
- });
- });
-});
diff --git a/spec/frontend/ide/components/repo_editor_spec.js b/spec/frontend/ide/components/repo_editor_spec.js
index 9092d73571b..c9f033bffbb 100644
--- a/spec/frontend/ide/components/repo_editor_spec.js
+++ b/spec/frontend/ide/components/repo_editor_spec.js
@@ -22,6 +22,7 @@ import ModelManager from '~/ide/lib/common/model_manager';
import service from '~/ide/services';
import { createStoreOptions } from '~/ide/stores';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ContentViewer from '~/vue_shared/components/content_viewer/content_viewer.vue';
import SourceEditorInstance from '~/editor/source_editor_instance';
import { file } from '../helpers';
@@ -265,7 +266,7 @@ describe('RepoEditor', () => {
mock = new MockAdapter(axios);
- mock.onPost(/(.*)\/preview_markdown/).reply(200, {
+ mock.onPost(/(.*)\/preview_markdown/).reply(HTTP_STATUS_OK, {
body: `<p>${dummyFile.text.content}</p>`,
});
});
diff --git a/spec/frontend/ide/init_gitlab_web_ide_spec.js b/spec/frontend/ide/init_gitlab_web_ide_spec.js
index 97254ab680b..bfc87f17092 100644
--- a/spec/frontend/ide/init_gitlab_web_ide_spec.js
+++ b/spec/frontend/ide/init_gitlab_web_ide_spec.js
@@ -3,6 +3,7 @@ import { GITLAB_WEB_IDE_FEEDBACK_ISSUE } from '~/ide/constants';
import { initGitlabWebIDE } from '~/ide/init_gitlab_web_ide';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_action';
import { createAndSubmitForm } from '~/lib/utils/create_and_submit_form';
+import { handleTracking } from '~/ide/lib/gitlab_web_ide/handle_tracking_event';
import { TEST_HOST } from 'helpers/test_constants';
import setWindowLocation from 'helpers/set_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -32,6 +33,9 @@ const TEST_START_REMOTE_PARAMS = {
remotePath: '/test/projects/f oo',
connectionToken: '123abc',
};
+const TEST_EDITOR_FONT_SRC_URL = 'http://gitlab.test/assets/jetbrains-mono/JetBrainsMono.woff2';
+const TEST_EDITOR_FONT_FORMAT = 'woff2';
+const TEST_EDITOR_FONT_FAMILY = 'JebBrains Mono';
describe('ide/init_gitlab_web_ide', () => {
let resolveConfirm;
@@ -49,6 +53,9 @@ describe('ide/init_gitlab_web_ide', () => {
el.dataset.userPreferencesPath = TEST_USER_PREFERENCES_PATH;
el.dataset.mergeRequest = TEST_MR_ID;
el.dataset.filePath = TEST_FILE_PATH;
+ el.dataset.editorFontSrcUrl = TEST_EDITOR_FONT_SRC_URL;
+ el.dataset.editorFontFormat = TEST_EDITOR_FONT_FORMAT;
+ el.dataset.editorFontFamily = TEST_EDITOR_FONT_FAMILY;
document.body.append(el);
};
@@ -103,7 +110,13 @@ describe('ide/init_gitlab_web_ide', () => {
userPreferences: TEST_USER_PREFERENCES_PATH,
feedbackIssue: GITLAB_WEB_IDE_FEEDBACK_ISSUE,
},
+ editorFont: {
+ srcUrl: TEST_EDITOR_FONT_SRC_URL,
+ fontFamily: TEST_EDITOR_FONT_FAMILY,
+ format: TEST_EDITOR_FONT_FORMAT,
+ },
handleStartRemote: expect.any(Function),
+ handleTracking,
});
});
diff --git a/spec/frontend/ide/lib/gitlab_web_ide/handle_tracking_event_spec.js b/spec/frontend/ide/lib/gitlab_web_ide/handle_tracking_event_spec.js
new file mode 100644
index 00000000000..5dff9b6f118
--- /dev/null
+++ b/spec/frontend/ide/lib/gitlab_web_ide/handle_tracking_event_spec.js
@@ -0,0 +1,32 @@
+import { snakeCase } from 'lodash';
+import { handleTracking } from '~/ide/lib/gitlab_web_ide/handle_tracking_event';
+import { convertObjectPropsToSnakeCase } from '~/lib/utils/common_utils';
+import { mockTracking } from 'helpers/tracking_helper';
+
+describe('ide/handle_tracking_event', () => {
+ let trackingSpy;
+
+ beforeEach(() => {
+ trackingSpy = mockTracking(undefined, null, jest.spyOn);
+ });
+
+ describe('when the event does not contain data', () => {
+ it('does not send extra property to snowplow', () => {
+ const event = { name: 'event-name' };
+
+ handleTracking(event);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, snakeCase(event.name));
+ });
+ });
+
+ describe('when the event contains data', () => {
+ it('sends extra property to snowplow', () => {
+ const event = { name: 'event-name', data: { 'extra-details': 'details' } };
+
+ handleTracking(event);
+ expect(trackingSpy).toHaveBeenCalledWith(undefined, snakeCase(event.name), {
+ extra: convertObjectPropsToSnakeCase(event.data),
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ide/lib/mirror_spec.js b/spec/frontend/ide/lib/mirror_spec.js
index 8f417ea54dc..98e6b0deee6 100644
--- a/spec/frontend/ide/lib/mirror_spec.js
+++ b/spec/frontend/ide/lib/mirror_spec.js
@@ -7,6 +7,7 @@ import {
MSG_CONNECTION_ERROR,
SERVICE_DELAY,
} from '~/ide/lib/mirror';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { getWebSocketUrl } from '~/lib/utils/url_utility';
jest.mock('~/ide/lib/create_diff', () => jest.fn());
@@ -18,15 +19,18 @@ const TEST_DIFF = {
};
const TEST_ERROR = 'Something bad happened...';
const TEST_SUCCESS_RESPONSE = {
- data: JSON.stringify({ error: { code: 0 }, payload: { status_code: 200 } }),
+ data: JSON.stringify({ error: { code: 0 }, payload: { status_code: HTTP_STATUS_OK } }),
};
const TEST_ERROR_RESPONSE = {
- data: JSON.stringify({ error: { code: 1, Message: TEST_ERROR }, payload: { status_code: 200 } }),
+ data: JSON.stringify({
+ error: { code: 1, Message: TEST_ERROR },
+ payload: { status_code: HTTP_STATUS_OK },
+ }),
};
const TEST_ERROR_PAYLOAD_RESPONSE = {
data: JSON.stringify({
error: { code: 0 },
- payload: { status_code: 500, error_message: TEST_ERROR },
+ payload: { status_code: HTTP_STATUS_INTERNAL_SERVER_ERROR, error_message: TEST_ERROR },
}),
};
diff --git a/spec/frontend/ide/remote/index_spec.js b/spec/frontend/ide/remote/index_spec.js
index 0f23b0a4e45..413e7b2e4b7 100644
--- a/spec/frontend/ide/remote/index_spec.js
+++ b/spec/frontend/ide/remote/index_spec.js
@@ -3,6 +3,7 @@ import { getBaseConfig, setupRootElement } from '~/ide/lib/gitlab_web_ide';
import { mountRemoteIDE } from '~/ide/remote';
import { TEST_HOST } from 'helpers/test_constants';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
+import { handleTracking } from '~/ide/lib/gitlab_web_ide/handle_tracking_event';
jest.mock('@gitlab/web-ide');
jest.mock('~/ide/lib/gitlab_web_ide');
@@ -24,7 +25,6 @@ const TEST_RETURN_URL_SAME_ORIGIN = `${TEST_HOST}/foo/example`;
describe('~/ide/remote/index', () => {
useMockLocationHelper();
const originalHref = window.location.href;
-
let el;
let rootEl;
@@ -56,6 +56,7 @@ describe('~/ide/remote/index', () => {
hostPath: `/${TEST_DATA.remotePath}`,
handleError: expect.any(Function),
handleClose: expect.any(Function),
+ handleTracking,
});
});
});
diff --git a/spec/frontend/ide/services/index_spec.js b/spec/frontend/ide/services/index_spec.js
index 5847e8e1518..623dee387e5 100644
--- a/spec/frontend/ide/services/index_spec.js
+++ b/spec/frontend/ide/services/index_spec.js
@@ -5,6 +5,7 @@ import Api from '~/api';
import dismissUserCallout from '~/graphql_shared/mutations/dismiss_user_callout.mutation.graphql';
import services from '~/ide/services';
import { query, mutate } from '~/ide/services/gql';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { escapeFileUrl } from '~/lib/utils/url_utility';
import ciConfig from '~/ci/pipeline_editor/graphql/queries/ci_config.query.graphql';
import { projectData } from '../mock_data';
@@ -108,7 +109,7 @@ describe('IDE services', () => {
};
mock = new MockAdapter(axios);
- mock.onGet(file.rawPath).reply(200, 'raw content');
+ mock.onGet(file.rawPath).reply(HTTP_STATUS_OK, 'raw content');
jest.spyOn(axios, 'get');
});
@@ -205,7 +206,7 @@ describe('IDE services', () => {
filePath,
)}`,
)
- .reply(200, TEST_FILE_CONTENTS);
+ .reply(HTTP_STATUS_OK, TEST_FILE_CONTENTS);
});
it('fetches file content', () =>
@@ -230,7 +231,7 @@ describe('IDE services', () => {
mock
.onGet(`${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_ID}/-/files/${TEST_COMMIT_SHA}`)
- .reply(200, [TEST_FILE_PATH]);
+ .reply(HTTP_STATUS_OK, [TEST_FILE_PATH]);
});
afterEach(() => {
@@ -271,7 +272,7 @@ describe('IDE services', () => {
const TEST_PROJECT_PATH = 'foo/bar';
const axiosURL = `${TEST_RELATIVE_URL_ROOT}/${TEST_PROJECT_PATH}/service_ping/web_ide_pipelines_count`;
- mock.onPost(axiosURL).reply(200);
+ mock.onPost(axiosURL).reply(HTTP_STATUS_OK);
return services.pingUsage(TEST_PROJECT_PATH).then(() => {
expect(axios.post).toHaveBeenCalledWith(axiosURL);
diff --git a/spec/frontend/ide/services/terminals_spec.js b/spec/frontend/ide/services/terminals_spec.js
index 788fdb6471c..5f752197e13 100644
--- a/spec/frontend/ide/services/terminals_spec.js
+++ b/spec/frontend/ide/services/terminals_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import * as terminalService from '~/ide/services/terminals';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
const TEST_PROJECT_PATH = 'lorem/ipsum/dolar';
const TEST_BRANCH = 'ref';
@@ -11,7 +12,7 @@ describe('~/ide/services/terminals', () => {
const prevRelativeUrlRoot = gon.relative_url_root;
beforeEach(() => {
- axiosSpy = jest.fn().mockReturnValue([200, {}]);
+ axiosSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, {}]);
mock = new MockAdapter(axios);
mock.onPost(/.*/).reply((...args) => axiosSpy(...args));
diff --git a/spec/frontend/ide/stores/actions/file_spec.js b/spec/frontend/ide/stores/actions/file_spec.js
index 38a54e569a9..90ca8526698 100644
--- a/spec/frontend/ide/stores/actions/file_spec.js
+++ b/spec/frontend/ide/stores/actions/file_spec.js
@@ -7,6 +7,7 @@ import { createStore } from '~/ide/stores';
import * as actions from '~/ide/stores/actions/file';
import * as types from '~/ide/stores/mutation_types';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { stubPerformanceWebAPI } from 'helpers/performance';
import { file, createTriggerRenameAction, createTriggerUpdatePayload } from '../../helpers';
@@ -243,7 +244,7 @@ describe('IDE store file actions', () => {
describe('success', () => {
beforeEach(() => {
mock.onGet(`${RELATIVE_URL_ROOT}/test/test/-/7297abc/${localFile.path}`).replyOnce(
- 200,
+ HTTP_STATUS_OK,
{
raw_path: 'raw_path',
},
@@ -320,7 +321,7 @@ describe('IDE store file actions', () => {
store.state.entries[localFile.path] = localFile;
mock.onGet(`${RELATIVE_URL_ROOT}/test/test/-/7297abc/old-dull-file`).replyOnce(
- 200,
+ HTTP_STATUS_OK,
{
raw_path: 'raw_path',
},
@@ -377,7 +378,7 @@ describe('IDE store file actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(/(.*)/).replyOnce(200, 'raw');
+ mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_OK, 'raw');
});
it('calls getRawFileData service method', () => {
@@ -470,7 +471,7 @@ describe('IDE store file actions', () => {
describe('return JSON', () => {
beforeEach(() => {
- mock.onGet(/(.*)/).replyOnce(200, JSON.stringify({ test: '123' }));
+ mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_OK, JSON.stringify({ test: '123' }));
});
it('does not parse returned JSON', () => {
diff --git a/spec/frontend/ide/stores/actions/merge_request_spec.js b/spec/frontend/ide/stores/actions/merge_request_spec.js
index f1b2a7b881a..fbae84631ee 100644
--- a/spec/frontend/ide/stores/actions/merge_request_spec.js
+++ b/spec/frontend/ide/stores/actions/merge_request_spec.js
@@ -16,6 +16,7 @@ import {
} from '~/ide/stores/actions/merge_request';
import * as types from '~/ide/stores/mutation_types';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
const TEST_PROJECT = 'abcproject';
const TEST_PROJECT_ID = 17;
@@ -63,7 +64,9 @@ describe('IDE store merge request actions', () => {
describe('base case', () => {
beforeEach(() => {
jest.spyOn(service, 'getProjectMergeRequests');
- mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, mockData);
+ mock
+ .onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/)
+ .reply(HTTP_STATUS_OK, mockData);
});
it('calls getProjectMergeRequests service method', async () => {
@@ -113,7 +116,7 @@ describe('IDE store merge request actions', () => {
describe('no merge requests for branch available case', () => {
beforeEach(() => {
jest.spyOn(service, 'getProjectMergeRequests');
- mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(200, []);
+ mock.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests/).reply(HTTP_STATUS_OK, []);
});
it('does not fail if there are no merge requests for current branch', async () => {
@@ -155,7 +158,7 @@ describe('IDE store merge request actions', () => {
mock
.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1/)
- .reply(200, { title: 'mergerequest' });
+ .reply(HTTP_STATUS_OK, { title: 'mergerequest' });
});
it('calls getProjectMergeRequestData service method', async () => {
@@ -212,7 +215,7 @@ describe('IDE store merge request actions', () => {
mock
.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/changes/)
- .reply(200, { title: 'mergerequest' });
+ .reply(HTTP_STATUS_OK, { title: 'mergerequest' });
});
it('calls getProjectMergeRequestChanges service method', async () => {
@@ -276,7 +279,7 @@ describe('IDE store merge request actions', () => {
beforeEach(() => {
mock
.onGet(/api\/(.*)\/projects\/abcproject\/merge_requests\/1\/versions/)
- .reply(200, [{ id: 789 }]);
+ .reply(HTTP_STATUS_OK, [{ id: 789 }]);
jest.spyOn(service, 'getProjectMergeRequestVersions');
});
diff --git a/spec/frontend/ide/stores/actions/tree_spec.js b/spec/frontend/ide/stores/actions/tree_spec.js
index 6e8a03b47ad..47b6ebb3376 100644
--- a/spec/frontend/ide/stores/actions/tree_spec.js
+++ b/spec/frontend/ide/stores/actions/tree_spec.js
@@ -8,6 +8,7 @@ import { createStore } from '~/ide/stores';
import { showTreeEntry, getFiles, setDirectoryData } from '~/ide/stores/actions/tree';
import * as types from '~/ide/stores/mutation_types';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { file, createEntriesFromPaths } from '../../helpers';
describe('Multi-file store tree actions', () => {
@@ -52,7 +53,7 @@ describe('Multi-file store tree actions', () => {
mock
.onGet(/(.*)/)
- .replyOnce(200, [
+ .replyOnce(HTTP_STATUS_OK, [
'file.txt',
'folder/fileinfolder.js',
'folder/subfolder/fileinsubfolder.js',
@@ -98,7 +99,7 @@ describe('Multi-file store tree actions', () => {
findBranch: () => store.state.projects['abc/def'].branches['main-testing'],
};
- mock.onGet(/(.*)/).replyOnce(500);
+ mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await expect(
getFiles(
diff --git a/spec/frontend/ide/stores/actions_spec.js b/spec/frontend/ide/stores/actions_spec.js
index fd2c3d18813..1c90c0f943a 100644
--- a/spec/frontend/ide/stores/actions_spec.js
+++ b/spec/frontend/ide/stores/actions_spec.js
@@ -23,6 +23,7 @@ import {
} from '~/ide/stores/actions';
import * as types from '~/ide/stores/mutation_types';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_IM_A_TEAPOT, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import { file, createTriggerRenameAction, createTriggerChangeAction } from '../helpers';
@@ -917,7 +918,7 @@ describe('Multi-file store actions', () => {
});
it('passes the error further unchanged without dispatching any action when response is 404', async () => {
- mock.onGet(/(.*)/).replyOnce(404);
+ mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_NOT_FOUND);
await expect(getBranchData(...callParams)).rejects.toEqual(
new Error('Request failed with status code 404'),
@@ -927,7 +928,7 @@ describe('Multi-file store actions', () => {
});
it('does not pass the error further and flashes an alert if error is not 404', async () => {
- mock.onGet(/(.*)/).replyOnce(418);
+ mock.onGet(/(.*)/).replyOnce(HTTP_STATUS_IM_A_TEAPOT);
await expect(getBranchData(...callParams)).rejects.toEqual(
new Error('Branch not loaded - <strong>abc/def/main-testing</strong>'),
diff --git a/spec/frontend/ide/stores/getters_spec.js b/spec/frontend/ide/stores/getters_spec.js
index 24661e21cd0..d4166a3bd6d 100644
--- a/spec/frontend/ide/stores/getters_spec.js
+++ b/spec/frontend/ide/stores/getters_spec.js
@@ -294,18 +294,6 @@ describe('IDE store getters', () => {
});
});
- describe('packageJson', () => {
- it('returns package.json entry', () => {
- localState.entries['package.json'] = {
- name: 'package.json',
- };
-
- expect(getters.packageJson(localState)).toEqual({
- name: 'package.json',
- });
- });
- });
-
describe('canPushToBranch', () => {
it.each`
currentBranch | canPushCode | expectedValue
diff --git a/spec/frontend/ide/stores/modules/branches/actions_spec.js b/spec/frontend/ide/stores/modules/branches/actions_spec.js
index 306330e3ba2..c1c47ef7e9a 100644
--- a/spec/frontend/ide/stores/modules/branches/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/branches/actions_spec.js
@@ -10,6 +10,7 @@ import {
import * as types from '~/ide/stores/modules/branches/mutation_types';
import state from '~/ide/stores/modules/branches/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { branches, projectData } from '../../../mock_data';
describe('IDE branches actions', () => {
@@ -94,7 +95,9 @@ describe('IDE branches actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/).replyOnce(200, branches);
+ mock
+ .onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/)
+ .replyOnce(HTTP_STATUS_OK, branches);
});
it('calls API with params', () => {
@@ -124,7 +127,9 @@ describe('IDE branches actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/).replyOnce(500);
+ mock
+ .onGet(/\/api\/v4\/projects\/\d+\/repository\/branches(.*)$/)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
diff --git a/spec/frontend/ide/stores/modules/clientside/actions_spec.js b/spec/frontend/ide/stores/modules/clientside/actions_spec.js
deleted file mode 100644
index c2b9de192d9..00000000000
--- a/spec/frontend/ide/stores/modules/clientside/actions_spec.js
+++ /dev/null
@@ -1,38 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import { TEST_HOST } from 'helpers/test_constants';
-import testAction from 'helpers/vuex_action_helper';
-import { PING_USAGE_PREVIEW_KEY } from '~/ide/constants';
-import * as actions from '~/ide/stores/modules/clientside/actions';
-import axios from '~/lib/utils/axios_utils';
-
-const TEST_PROJECT_URL = `${TEST_HOST}/lorem/ipsum`;
-const TEST_USAGE_URL = `${TEST_PROJECT_URL}/service_ping/${PING_USAGE_PREVIEW_KEY}`;
-
-describe('IDE store module clientside actions', () => {
- let rootGetters;
- let mock;
-
- beforeEach(() => {
- rootGetters = {
- currentProject: {
- web_url: TEST_PROJECT_URL,
- },
- };
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- describe('pingUsage', () => {
- it('posts to usage endpoint', async () => {
- const usageSpy = jest.fn(() => [200]);
-
- mock.onPost(TEST_USAGE_URL).reply(() => usageSpy());
-
- await testAction(actions.pingUsage, PING_USAGE_PREVIEW_KEY, rootGetters, [], []);
- expect(usageSpy).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/frontend/ide/stores/modules/commit/actions_spec.js b/spec/frontend/ide/stores/modules/commit/actions_spec.js
index 8601e13f7ca..4068a9d0919 100644
--- a/spec/frontend/ide/stores/modules/commit/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/commit/actions_spec.js
@@ -14,6 +14,7 @@ import {
COMMIT_TO_NEW_BRANCH,
} from '~/ide/stores/modules/commit/constants';
import * as mutationTypes from '~/ide/stores/modules/commit/mutation_types';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
jest.mock('~/lib/utils/url_utility', () => ({
@@ -48,7 +49,7 @@ describe('IDE commit module actions', () => {
mock
.onGet('/api/v1/projects/abcproject/repository/branches/main')
- .reply(200, { commit: COMMIT_RESPONSE });
+ .reply(HTTP_STATUS_OK, { commit: COMMIT_RESPONSE });
});
afterEach(() => {
diff --git a/spec/frontend/ide/stores/modules/file_templates/actions_spec.js b/spec/frontend/ide/stores/modules/file_templates/actions_spec.js
index 1080a30d2d8..a5ce507bd3c 100644
--- a/spec/frontend/ide/stores/modules/file_templates/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/file_templates/actions_spec.js
@@ -4,6 +4,7 @@ import * as actions from '~/ide/stores/modules/file_templates/actions';
import * as types from '~/ide/stores/modules/file_templates/mutation_types';
import createState from '~/ide/stores/modules/file_templates/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('IDE file templates actions', () => {
let state;
@@ -74,7 +75,7 @@ describe('IDE file templates actions', () => {
const page = pages[pageNum - 1];
const hasNextPage = pageNum < pages.length;
- return [200, page, hasNextPage ? { 'X-NEXT-PAGE': pageNum + 1 } : {}];
+ return [HTTP_STATUS_OK, page, hasNextPage ? { 'X-NEXT-PAGE': pageNum + 1 } : {}];
});
});
@@ -108,7 +109,7 @@ describe('IDE file templates actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(/api\/(.*)\/templates\/licenses/).replyOnce(500);
+ mock.onGet(/api\/(.*)\/templates\/licenses/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches actions', () => {
@@ -199,10 +200,10 @@ describe('IDE file templates actions', () => {
beforeEach(() => {
mock
.onGet(/api\/(.*)\/templates\/licenses\/mit/)
- .replyOnce(200, { content: 'MIT content' });
+ .replyOnce(HTTP_STATUS_OK, { content: 'MIT content' });
mock
.onGet(/api\/(.*)\/templates\/licenses\/testing/)
- .replyOnce(200, { content: 'testing content' });
+ .replyOnce(HTTP_STATUS_OK, { content: 'testing content' });
});
it('dispatches setFileTemplate if template already has content', () => {
@@ -248,7 +249,9 @@ describe('IDE file templates actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(/api\/(.*)\/templates\/licenses\/mit/).replyOnce(500);
+ mock
+ .onGet(/api\/(.*)\/templates\/licenses\/mit/)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
diff --git a/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js b/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js
index 344fe3a41c3..56901383f7b 100644
--- a/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/merge_requests/actions_spec.js
@@ -10,6 +10,7 @@ import {
import * as types from '~/ide/stores/modules/merge_requests/mutation_types';
import state from '~/ide/stores/modules/merge_requests/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { mergeRequests } from '../../../mock_data';
describe('IDE merge requests actions', () => {
@@ -80,7 +81,7 @@ describe('IDE merge requests actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/merge_requests\/?/).replyOnce(200, mergeRequests);
+ mock.onGet(/\/api\/v4\/merge_requests\/?/).replyOnce(HTTP_STATUS_OK, mergeRequests);
});
it('calls API with params', () => {
@@ -132,7 +133,9 @@ describe('IDE merge requests actions', () => {
describe('success without type', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/projects\/.+\/merge_requests\/?$/).replyOnce(200, mergeRequests);
+ mock
+ .onGet(/\/api\/v4\/projects\/.+\/merge_requests\/?$/)
+ .replyOnce(HTTP_STATUS_OK, mergeRequests);
});
it('calls API with project', () => {
@@ -169,7 +172,7 @@ describe('IDE merge requests actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(500);
+ mock.onGet(/\/api\/v4\/merge_requests(.*)$/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
diff --git a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
index b76b673c3a2..f49ff75ba7e 100644
--- a/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
+++ b/spec/frontend/ide/stores/modules/pipelines/actions_spec.js
@@ -25,6 +25,11 @@ import {
import * as types from '~/ide/stores/modules/pipelines/mutation_types';
import state from '~/ide/stores/modules/pipelines/state';
import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import waitForPromises from 'helpers/wait_for_promises';
import { pipelines, jobs } from '../../../mock_data';
@@ -60,7 +65,7 @@ describe('IDE pipelines actions', () => {
it('commits error', () => {
return testAction(
receiveLatestPipelineError,
- { status: 404 },
+ { status: HTTP_STATUS_NOT_FOUND },
mockedState,
[{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }],
[{ type: 'stopPipelinePolling' }],
@@ -70,7 +75,7 @@ describe('IDE pipelines actions', () => {
it('dispatches setErrorMessage is not 404', () => {
return testAction(
receiveLatestPipelineError,
- { status: 500 },
+ { status: HTTP_STATUS_INTERNAL_SERVER_ERROR },
mockedState,
[{ type: types.RECEIVE_LASTEST_PIPELINE_ERROR }],
[
@@ -118,7 +123,7 @@ describe('IDE pipelines actions', () => {
beforeEach(() => {
mock
.onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines')
- .reply(200, { data: { foo: 'bar' } }, { 'poll-interval': '10000' });
+ .reply(HTTP_STATUS_OK, { data: { foo: 'bar' } }, { 'poll-interval': '10000' });
});
it('dispatches request', async () => {
@@ -151,7 +156,9 @@ describe('IDE pipelines actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines').reply(500);
+ mock
+ .onGet('/abc/def/commit/abc123def456ghi789jkl/pipelines')
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', async () => {
@@ -238,7 +245,7 @@ describe('IDE pipelines actions', () => {
describe('success', () => {
beforeEach(() => {
- mock.onGet(stage.dropdownPath).replyOnce(200, jobs);
+ mock.onGet(stage.dropdownPath).replyOnce(HTTP_STATUS_OK, jobs);
});
it('dispatches request', () => {
@@ -257,7 +264,7 @@ describe('IDE pipelines actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(stage.dropdownPath).replyOnce(500);
+ mock.onGet(stage.dropdownPath).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
@@ -367,7 +374,7 @@ describe('IDE pipelines actions', () => {
describe('success', () => {
beforeEach(() => {
jest.spyOn(axios, 'get');
- mock.onGet(`${TEST_HOST}/project/builds/trace`).replyOnce(200, { html: 'html' });
+ mock.onGet(`${TEST_HOST}/project/builds/trace`).replyOnce(HTTP_STATUS_OK, { html: 'html' });
});
it('dispatches request', () => {
@@ -397,7 +404,9 @@ describe('IDE pipelines actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/project/builds/trace`).replyOnce(500);
+ mock
+ .onGet(`${TEST_HOST}/project/builds/trace`)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
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 09be1e333b3..8d8afda7014 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/checks_spec.js
@@ -11,8 +11,11 @@ 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 {
+ HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_FORBIDDEN,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_OK,
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
@@ -129,7 +132,7 @@ describe('IDE store terminal check actions', () => {
describe('fetchConfigCheck', () => {
it('dispatches request and receive', () => {
- mock.onPost(/.*\/ide_terminals\/check_config/).reply(200, {});
+ mock.onPost(/.*\/ide_terminals\/check_config/).reply(HTTP_STATUS_OK, {});
return testAction(
actions.fetchConfigCheck,
@@ -144,7 +147,7 @@ describe('IDE store terminal check actions', () => {
});
it('when error, dispatches request and receive', () => {
- mock.onPost(/.*\/ide_terminals\/check_config/).reply(400, {});
+ mock.onPost(/.*\/ide_terminals\/check_config/).reply(HTTP_STATUS_BAD_REQUEST, {});
return testAction(
actions.fetchConfigCheck,
@@ -252,7 +255,9 @@ describe('IDE store terminal check actions', () => {
describe('fetchRunnersCheck', () => {
it('dispatches request and receive', () => {
- mock.onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }).reply(200, []);
+ mock
+ .onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } })
+ .reply(HTTP_STATUS_OK, []);
return testAction(
actions.fetchRunnersCheck,
@@ -264,7 +269,9 @@ describe('IDE store terminal check actions', () => {
});
it('does not dispatch request when background is true', () => {
- mock.onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }).reply(200, []);
+ mock
+ .onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } })
+ .reply(HTTP_STATUS_OK, []);
return testAction(
actions.fetchRunnersCheck,
@@ -276,7 +283,9 @@ describe('IDE store terminal check actions', () => {
});
it('dispatches request and receive, when error', () => {
- mock.onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } }).reply(500, []);
+ mock
+ .onGet(/api\/.*\/projects\/.*\/runners/, { params: { scope: 'active' } })
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, []);
return testAction(
actions.fetchRunnersCheck,
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 9fd5f1a38d7..0287e5269ee 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,12 @@ 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 { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_BAD_REQUEST,
+ HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_OK,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
+} from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -111,7 +116,7 @@ describe('IDE store terminal session controls actions', () => {
});
it('dispatches request and receive on success', () => {
- mock.onPost(/.*\/ide_terminals/).reply(200, TEST_SESSION);
+ mock.onPost(/.*\/ide_terminals/).reply(HTTP_STATUS_OK, TEST_SESSION);
return testAction(
actions.startSession,
@@ -126,7 +131,7 @@ describe('IDE store terminal session controls actions', () => {
});
it('dispatches request and receive on error', () => {
- mock.onPost(/.*\/ide_terminals/).reply(400);
+ mock.onPost(/.*\/ide_terminals/).reply(HTTP_STATUS_BAD_REQUEST);
return testAction(
actions.startSession,
@@ -175,7 +180,7 @@ describe('IDE store terminal session controls actions', () => {
describe('stopSession', () => {
it('dispatches request and receive on success', () => {
- mock.onPost(TEST_SESSION.cancel_path).reply(200, {});
+ mock.onPost(TEST_SESSION.cancel_path).reply(HTTP_STATUS_OK, {});
const state = {
session: { cancelPath: TEST_SESSION.cancel_path },
@@ -191,7 +196,7 @@ describe('IDE store terminal session controls actions', () => {
});
it('dispatches request and receive on error', () => {
- mock.onPost(TEST_SESSION.cancel_path).reply(400);
+ mock.onPost(TEST_SESSION.cancel_path).reply(HTTP_STATUS_BAD_REQUEST);
const state = {
session: { cancelPath: TEST_SESSION.cancel_path },
@@ -254,7 +259,7 @@ describe('IDE store terminal session controls actions', () => {
it('dispatches request and receive on success', () => {
mock
.onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' })
- .reply(200, TEST_SESSION);
+ .reply(HTTP_STATUS_OK, TEST_SESSION);
return testAction(
actions.restartSession,
@@ -271,7 +276,7 @@ describe('IDE store terminal session controls actions', () => {
it('dispatches request and receive on error', () => {
mock
.onPost(state.session.retryPath, { branch: rootState.currentBranchId, format: 'json' })
- .reply(400);
+ .reply(HTTP_STATUS_BAD_REQUEST);
return testAction(
actions.restartSession,
diff --git a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
index fe2328f25c2..9616733f052 100644
--- a/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
+++ b/spec/frontend/ide/stores/modules/terminal/actions/session_status_spec.js
@@ -6,6 +6,7 @@ import { PENDING, RUNNING, STOPPING, STOPPED } from '~/ide/stores/modules/termin
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 { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -145,7 +146,7 @@ describe('IDE store terminal session controls actions', () => {
});
it('dispatches success on success', () => {
- mock.onGet(state.session.showPath).reply(200, TEST_SESSION);
+ mock.onGet(state.session.showPath).reply(HTTP_STATUS_OK, TEST_SESSION);
return testAction(
actions.fetchSessionStatus,
@@ -157,7 +158,7 @@ describe('IDE store terminal session controls actions', () => {
});
it('dispatches error on error', () => {
- mock.onGet(state.session.showPath).reply(400);
+ mock.onGet(state.session.showPath).reply(HTTP_STATUS_BAD_REQUEST);
return testAction(
actions.fetchSessionStatus,
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 da7fb4e060d..163a60bae36 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
@@ -39,7 +39,7 @@ describe('import actions cell', () => {
describe('when group is finished', () => {
beforeEach(() => {
- createComponent({ isAvailableForImport: true, isFinished: true });
+ createComponent({ isAvailableForImport: false, isFinished: true });
});
it('renders re-import button', () => {
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 bd79e20e698..c7bda5a60ec 100644
--- a/spec/frontend/import_entities/import_groups/components/import_table_spec.js
+++ b/spec/frontend/import_entities/import_groups/components/import_table_spec.js
@@ -1,4 +1,4 @@
-import { GlAlert, GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
+import { GlEmptyState, GlIcon, GlLoadingIcon } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
@@ -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 { HTTP_STATUS_BAD_REQUEST } from '~/lib/utils/http_status';
+import { HTTP_STATUS_OK, HTTP_STATUS_TOO_MANY_REQUESTS } 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';
@@ -38,7 +38,9 @@ describe('import table', () => {
const FAKE_GROUPS = [
generateFakeEntry({ id: 1, status: STATUSES.NONE }),
generateFakeEntry({ id: 2, status: STATUSES.FINISHED }),
+ generateFakeEntry({ id: 3, status: STATUSES.NONE }),
];
+
const FAKE_PAGE_INFO = { page: 1, perPage: 20, total: 40, totalPages: 2 };
const FAKE_VERSION_VALIDATION = {
features: {
@@ -59,12 +61,14 @@ describe('import table', () => {
const findPaginationDropdownText = () => findPaginationDropdown().find('button').text();
const findSelectionCount = () => wrapper.find('[data-test-id="selection-count"]');
const findNewPathCol = () => wrapper.find('[data-test-id="new-path-col"]');
+ const findUnavailableFeaturesWarning = () =>
+ wrapper.find('[data-testid="unavailable-features-alert"]');
const triggerSelectAllCheckbox = (checked = true) =>
wrapper.find('thead input[type=checkbox]').setChecked(checked);
- const selectRow = (idx) =>
- wrapper.findAll('tbody td input[type=checkbox]').at(idx).setChecked(true);
+ const findRowCheckbox = (idx) => wrapper.findAll('tbody td input[type=checkbox]').at(idx);
+ const selectRow = (idx) => findRowCheckbox(idx).setChecked(true);
const createComponent = ({
bulkImportSourceGroups,
@@ -113,7 +117,7 @@ describe('import table', () => {
beforeEach(() => {
axiosMock = new MockAdapter(axios);
- axiosMock.onGet(/.*\/exists$/, () => []).reply(200);
+ axiosMock.onGet(/.*\/exists$/, () => []).reply(HTTP_STATUS_OK, { exists: false });
});
afterEach(() => {
@@ -268,8 +272,6 @@ describe('import table', () => {
},
});
- axiosMock.onPost('/import/bulk_imports.json').reply(HTTP_STATUS_BAD_REQUEST);
-
await waitForPromises();
await findImportButtons()[0].trigger('click');
await waitForPromises();
@@ -281,6 +283,28 @@ describe('import table', () => {
);
});
+ it('displays inline error if importing group reports rate limit', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [FAKE_GROUP],
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ importGroups: () => {
+ const error = new Error();
+ error.response = { status: HTTP_STATUS_TOO_MANY_REQUESTS };
+ throw error;
+ },
+ });
+
+ await waitForPromises();
+ await findImportButtons()[0].trigger('click');
+ await waitForPromises();
+
+ expect(createAlert).not.toHaveBeenCalled();
+ expect(wrapper.find('tbody tr').text()).toContain(i18n.ERROR_TOO_MANY_REQUESTS);
+ });
+
describe('pagination', () => {
const bulkImportSourceGroupsQueryMock = jest.fn().mockResolvedValue({
nodes: [FAKE_GROUP],
@@ -587,6 +611,40 @@ describe('import table', () => {
expect(tooltip.value).toBe('Path of the new group.');
});
+ describe('re-import', () => {
+ it('renders finished row as disabled by default', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [generateFakeEntry({ id: 5, status: STATUSES.FINISHED })],
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ });
+ await waitForPromises();
+
+ expect(findRowCheckbox(0).attributes('disabled')).toBeDefined();
+ });
+
+ it('enables row after clicking re-import', async () => {
+ createComponent({
+ bulkImportSourceGroups: () => ({
+ nodes: [generateFakeEntry({ id: 5, status: STATUSES.FINISHED })],
+ pageInfo: FAKE_PAGE_INFO,
+ versionValidation: FAKE_VERSION_VALIDATION,
+ }),
+ });
+ await waitForPromises();
+
+ const reimportButton = wrapper
+ .findAll('tbody td button')
+ .wrappers.find((w) => w.text().includes('Re-import'));
+
+ await reimportButton.trigger('click');
+
+ expect(findRowCheckbox(0).attributes('disabled')).toBeUndefined();
+ });
+ });
+
describe('unavailable features warning', () => {
it('renders alert when there are unavailable features', async () => {
createComponent({
@@ -598,8 +656,8 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.findComponent(GlAlert).exists()).toBe(true);
- expect(wrapper.findComponent(GlAlert).text()).toContain('projects (require v14.8.0)');
+ expect(findUnavailableFeaturesWarning().exists()).toBe(true);
+ expect(findUnavailableFeaturesWarning().text()).toContain('projects (require v14.8.0)');
});
it('does not renders alert when there are no unavailable features', async () => {
@@ -617,7 +675,7 @@ describe('import table', () => {
});
await waitForPromises();
- expect(wrapper.findComponent(GlAlert).exists()).toBe(false);
+ expect(findUnavailableFeaturesWarning().exists()).toBe(false);
});
});
diff --git a/spec/frontend/import_entities/import_groups/services/status_poller_spec.js b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
index 13d2a95ca14..4a1b85d24e3 100644
--- a/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
+++ b/spec/frontend/import_entities/import_groups/services/status_poller_spec.js
@@ -4,6 +4,7 @@ import { createAlert } from '~/flash';
import { STATUSES } from '~/import_entities/constants';
import { StatusPoller } from '~/import_entities/import_groups/services/status_poller';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
jest.mock('visibilityjs');
@@ -21,7 +22,7 @@ describe('Bulk import status poller', () => {
beforeEach(() => {
mockAdapter = new MockAdapter(axios);
- mockAdapter.onGet(FAKE_POLL_PATH).reply(200, {});
+ mockAdapter.onGet(FAKE_POLL_PATH).reply(HTTP_STATUS_OK, {});
updateImportStatus = jest.fn();
poller = new StatusPoller({ updateImportStatus, pollPath: FAKE_POLL_PATH });
});
diff --git a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
index d686036781f..e613b9756af 100644
--- a/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
+++ b/spec/frontend/import_entities/import_projects/components/provider_repo_table_row_spec.js
@@ -1,4 +1,4 @@
-import { GlBadge, GlButton, GlDropdown } from '@gitlab/ui';
+import { GlBadge, GlButton } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
@@ -31,12 +31,16 @@ describe('ProviderRepoTableRow', () => {
return store;
}
- const findImportButton = () => {
- const buttons = wrapper.findAllComponents(GlButton).filter((node) => node.text() === 'Import');
+ const findButton = (text) => {
+ const buttons = wrapper.findAllComponents(GlButton).filter((node) => node.text() === text);
return buttons.length ? buttons.at(0) : buttons;
};
+ const findImportButton = () => findButton('Import');
+ const findReimportButton = () => findButton('Re-import');
+ const findGroupDropdown = () => wrapper.findComponent(ImportGroupDropdown);
+
const findCancelButton = () => {
const buttons = wrapper
.findAllComponents(GlButton)
@@ -117,6 +121,10 @@ describe('ProviderRepoTableRow', () => {
optionalStages: OPTIONAL_STAGES,
});
});
+
+ it('does not render re-import button', () => {
+ expect(findReimportButton().exists()).toBe(false);
+ });
});
describe('when rendering importing project', () => {
@@ -200,19 +208,68 @@ describe('ProviderRepoTableRow', () => {
);
});
- it('does not renders a namespace select', () => {
- expect(wrapper.findComponent(GlDropdown).exists()).toBe(false);
+ it('does not render a namespace select', () => {
+ expect(findGroupDropdown().exists()).toBe(false);
});
it('does not render import button', () => {
expect(findImportButton().exists()).toBe(false);
});
+ it('renders re-import button', () => {
+ expect(findReimportButton().exists()).toBe(true);
+ });
+
+ it('renders namespace select after clicking re-import', async () => {
+ findReimportButton().vm.$emit('click');
+
+ await nextTick();
+
+ expect(findGroupDropdown().exists()).toBe(true);
+ });
+
+ it('imports repo when clicking re-import button', async () => {
+ findReimportButton().vm.$emit('click');
+
+ await nextTick();
+
+ findReimportButton().vm.$emit('click');
+
+ expect(fetchImport).toHaveBeenCalledWith(expect.anything(), {
+ repoId: repo.importSource.id,
+ optionalStages: {},
+ });
+ });
+
it('passes stats to import status component', () => {
expect(wrapper.findComponent(ImportStatus).props().stats).toBe(FAKE_STATS);
});
});
+ describe('when rendering failed project', () => {
+ const repo = {
+ importSource: {
+ id: 'remote-1',
+ fullName: 'fullName',
+ providerLink: 'providerLink',
+ },
+ importedProject: {
+ id: 1,
+ fullPath: 'fullPath',
+ importSource: 'importSource',
+ importStatus: STATUSES.FAILED,
+ },
+ };
+
+ beforeEach(() => {
+ mountComponent({ repo });
+ });
+
+ it('render import button', () => {
+ expect(findImportButton().exists()).toBe(true);
+ });
+ });
+
describe('when rendering incompatible project', () => {
const repo = {
importSource: {
diff --git a/spec/frontend/import_entities/import_projects/store/actions_spec.js b/spec/frontend/import_entities/import_projects/store/actions_spec.js
index 4b34c21daa3..990587d4af7 100644
--- a/spec/frontend/import_entities/import_projects/store/actions_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/actions_spec.js
@@ -21,6 +21,11 @@ import {
import state from '~/import_entities/import_projects/store/state';
import axios from '~/lib/utils/axios_utils';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_OK,
+ HTTP_STATUS_TOO_MANY_REQUESTS,
+} from '~/lib/utils/http_status';
jest.mock('~/flash');
@@ -93,7 +98,7 @@ describe('import_projects store actions', () => {
describe('with a successful request', () => {
it('commits REQUEST_REPOS, SET_PAGE, RECEIVE_REPOS_SUCCESS mutations', () => {
- mock.onGet(MOCK_ENDPOINT).reply(200, payload);
+ mock.onGet(MOCK_ENDPOINT).reply(HTTP_STATUS_OK, payload);
return testAction(
fetchRepos,
@@ -117,7 +122,7 @@ describe('import_projects store actions', () => {
});
it('commits SET_PAGE_CURSORS instead of SET_PAGE', () => {
- mock.onGet(MOCK_ENDPOINT).reply(200, payload);
+ mock.onGet(MOCK_ENDPOINT).reply(HTTP_STATUS_OK, payload);
return testAction(
fetchRepos,
@@ -141,7 +146,7 @@ describe('import_projects store actions', () => {
});
it('commits REQUEST_REPOS, RECEIVE_REPOS_ERROR mutations on an unsuccessful request', () => {
- mock.onGet(MOCK_ENDPOINT).reply(500);
+ mock.onGet(MOCK_ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
fetchRepos,
@@ -157,7 +162,7 @@ describe('import_projects store actions', () => {
let requestedUrl;
mock.onGet().reply((config) => {
requestedUrl = config.url;
- return [200, payload];
+ return [HTTP_STATUS_OK, payload];
});
const localStateWithPage = { ...localState, pageInfo: { page: 2 } };
@@ -182,7 +187,7 @@ describe('import_projects store actions', () => {
let requestedUrl;
mock.onGet().reply((config) => {
requestedUrl = config.url;
- return [200, payload];
+ return [HTTP_STATUS_OK, payload];
});
const localStateWithPage = { ...localState, pageInfo: { endCursor: 'endTest' } };
@@ -201,7 +206,7 @@ describe('import_projects store actions', () => {
});
it('correctly keeps current page on an unsuccessful request', () => {
- mock.onGet(MOCK_ENDPOINT).reply(500);
+ mock.onGet(MOCK_ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
const CURRENT_PAGE = 5;
return testAction(
@@ -215,7 +220,7 @@ describe('import_projects store actions', () => {
describe('when rate limited', () => {
it('commits RECEIVE_REPOS_ERROR and shows rate limited error message', async () => {
- mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(429);
+ mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(HTTP_STATUS_TOO_MANY_REQUESTS);
await testAction(
fetchRepos,
@@ -233,7 +238,7 @@ describe('import_projects store actions', () => {
describe('when filtered', () => {
it('fetches repos with filter applied', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, payload);
+ mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(HTTP_STATUS_OK, payload);
return testAction(
fetchRepos,
@@ -264,7 +269,7 @@ describe('import_projects store actions', () => {
it('commits REQUEST_IMPORT and REQUEST_IMPORT_SUCCESS mutations on a successful request', () => {
const importedProject = { name: 'imported/project' };
- mock.onPost(MOCK_ENDPOINT).reply(200, importedProject);
+ mock.onPost(MOCK_ENDPOINT).reply(HTTP_STATUS_OK, importedProject);
return testAction(
fetchImport,
@@ -288,7 +293,7 @@ describe('import_projects store actions', () => {
});
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows generic error message on an unsuccessful request', async () => {
- mock.onPost(MOCK_ENDPOINT).reply(500);
+ mock.onPost(MOCK_ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await testAction(
fetchImport,
@@ -311,7 +316,9 @@ describe('import_projects store actions', () => {
it('commits REQUEST_IMPORT and RECEIVE_IMPORT_ERROR and shows detailed error message on an unsuccessful request with errors fields in response', async () => {
const ERROR_MESSAGE = 'dummy';
- mock.onPost(MOCK_ENDPOINT).reply(500, { errors: ERROR_MESSAGE });
+ mock
+ .onPost(MOCK_ENDPOINT)
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, { errors: ERROR_MESSAGE });
await testAction(
fetchImport,
@@ -349,7 +356,7 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
it('commits RECEIVE_JOBS_SUCCESS mutation on a successful request', async () => {
- mock.onGet(MOCK_ENDPOINT).reply(200, updatedProjects);
+ mock.onGet(MOCK_ENDPOINT).reply(HTTP_STATUS_OK, updatedProjects);
await testAction(
fetchJobs,
@@ -371,7 +378,9 @@ describe('import_projects store actions', () => {
});
it('fetches realtime changes with filter applied', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json?filter=filter`).reply(200, updatedProjects);
+ mock
+ .onGet(`${TEST_HOST}/endpoint.json?filter=filter`)
+ .reply(HTTP_STATUS_OK, updatedProjects);
return testAction(
fetchJobs,
@@ -433,7 +442,7 @@ describe('import_projects store actions', () => {
afterEach(() => mock.restore());
it('commits CANCEL_IMPORT_SUCCESS on success', async () => {
- mock.onPost(MOCK_ENDPOINT).reply(200);
+ mock.onPost(MOCK_ENDPOINT).reply(HTTP_STATUS_OK);
await testAction(
cancelImport,
@@ -450,7 +459,7 @@ describe('import_projects store actions', () => {
});
it('shows generic error message on an unsuccessful request', async () => {
- mock.onPost(MOCK_ENDPOINT).reply(500);
+ mock.onPost(MOCK_ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await testAction(cancelImport, { repoId: importRepoId }, localState, [], []);
@@ -461,7 +470,9 @@ describe('import_projects store actions', () => {
it('shows detailed error message on an unsuccessful request with errors fields in response', async () => {
const ERROR_MESSAGE = 'dummy';
- mock.onPost(MOCK_ENDPOINT).reply(500, { errors: ERROR_MESSAGE });
+ mock
+ .onPost(MOCK_ENDPOINT)
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, { errors: ERROR_MESSAGE });
await testAction(cancelImport, { repoId: importRepoId }, localState, [], []);
diff --git a/spec/frontend/import_entities/import_projects/store/mutations_spec.js b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
index 7884e9b4307..514a168553a 100644
--- a/spec/frontend/import_entities/import_projects/store/mutations_spec.js
+++ b/spec/frontend/import_entities/import_projects/store/mutations_spec.js
@@ -8,14 +8,14 @@ describe('import_projects store mutations', () => {
const SOURCE_PROJECT = {
id: 1,
- full_name: 'full/name',
- sanitized_name: 'name',
- provider_link: 'https://demo.link/full/name',
+ fullName: 'full/name',
+ sanitizedName: 'name',
+ providerLink: 'https://demo.link/full/name',
};
const IMPORTED_PROJECT = {
name: 'demo',
importSource: 'something',
- providerLink: 'custom-link',
+ providerLink: 'https://demo.link/full/name',
importStatus: 'status',
fullName: 'fullName',
};
@@ -64,21 +64,15 @@ describe('import_projects store mutations', () => {
describe('for imported projects', () => {
const response = {
importedProjects: [IMPORTED_PROJECT],
- providerRepos: [],
+ providerRepos: [SOURCE_PROJECT],
};
- it('recreates importSource from response', () => {
+ it('adds importedProject to relevant provider repo', () => {
state = getInitialState();
mutations[types.RECEIVE_REPOS_SUCCESS](state, response);
- expect(state.repositories[0].importSource).toStrictEqual(
- expect.objectContaining({
- fullName: IMPORTED_PROJECT.importSource,
- sanitizedName: IMPORTED_PROJECT.name,
- providerLink: IMPORTED_PROJECT.providerLink,
- }),
- );
+ expect(state.repositories[0].importedProject).toStrictEqual(IMPORTED_PROJECT);
});
it('passes project to importProject', () => {
@@ -216,13 +210,13 @@ describe('import_projects store mutations', () => {
describe(`${types.RECEIVE_IMPORT_ERROR}`, () => {
beforeEach(() => {
const REPO_ID = 1;
- state = { repositories: [{ importSource: { id: REPO_ID } }] };
+ state = { repositories: [{ importSource: { id: REPO_ID }, importedProject: {} }] };
mutations[types.RECEIVE_IMPORT_ERROR](state, REPO_ID);
});
- it(`removes importedProject entry`, () => {
- expect(state.repositories[0].importedProject).toBeNull();
+ it('sets status to failed', () => {
+ expect(state.repositories[0].importedProject.importStatus).toBe(STATUSES.FAILED);
});
});
diff --git a/spec/frontend/import_entities/import_projects/utils_spec.js b/spec/frontend/import_entities/import_projects/utils_spec.js
index d705f0acbfe..42cdf0f5a19 100644
--- a/spec/frontend/import_entities/import_projects/utils_spec.js
+++ b/spec/frontend/import_entities/import_projects/utils_spec.js
@@ -19,7 +19,7 @@ describe('import_projects utils', () => {
it.each`
status | result
${STATUSES.FINISHED} | ${false}
- ${STATUSES.FAILED} | ${false}
+ ${STATUSES.FAILED} | ${true}
${STATUSES.SCHEDULED} | ${false}
${STATUSES.STARTED} | ${false}
${STATUSES.NONE} | ${true}
diff --git a/spec/frontend/integrations/index/components/integrations_table_spec.js b/spec/frontend/integrations/index/components/integrations_table_spec.js
index bfe0a5987b4..976c7b74890 100644
--- a/spec/frontend/integrations/index/components/integrations_table_spec.js
+++ b/spec/frontend/integrations/index/components/integrations_table_spec.js
@@ -1,5 +1,6 @@
-import { GlTable, GlIcon } from '@gitlab/ui';
+import { GlTable, GlIcon, GlLink } from '@gitlab/ui';
import { mount } from '@vue/test-utils';
+import { INTEGRATION_TYPE_SLACK } from '~/integrations/constants';
import IntegrationsTable from '~/integrations/index/components/integrations_table.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -10,12 +11,17 @@ describe('IntegrationsTable', () => {
const findTable = () => wrapper.findComponent(GlTable);
- const createComponent = (propsData = {}) => {
+ const createComponent = (propsData = {}, flagIsOn = false) => {
wrapper = mount(IntegrationsTable, {
propsData: {
integrations: mockActiveIntegrations,
...propsData,
},
+ provide: {
+ glFeatures: {
+ integrationSlackAppNotifications: flagIsOn,
+ },
+ },
});
};
@@ -50,4 +56,51 @@ describe('IntegrationsTable', () => {
expect(findTable().findComponent(GlIcon).exists()).toBe(shouldRenderActiveIcon);
});
});
+
+ describe('integrations filtering', () => {
+ const slackActive = {
+ ...mockActiveIntegrations[0],
+ name: INTEGRATION_TYPE_SLACK,
+ title: 'Slack',
+ };
+ const slackInactive = {
+ ...mockInactiveIntegrations[0],
+ name: INTEGRATION_TYPE_SLACK,
+ title: 'Slack',
+ };
+
+ describe.each`
+ desc | flagIsOn | integrations | expectedIntegrations
+ ${'only active'} | ${false} | ${mockActiveIntegrations} | ${mockActiveIntegrations}
+ ${'only active'} | ${true} | ${mockActiveIntegrations} | ${mockActiveIntegrations}
+ ${'only inactive'} | ${true} | ${mockInactiveIntegrations} | ${mockInactiveIntegrations}
+ ${'only inactive'} | ${false} | ${mockInactiveIntegrations} | ${mockInactiveIntegrations}
+ ${'active and inactive'} | ${true} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]}
+ ${'active and inactive'} | ${false} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]}
+ ${'Slack active with active'} | ${false} | ${[slackActive, ...mockActiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations]}
+ ${'Slack active with active'} | ${true} | ${[slackActive, ...mockActiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations]}
+ ${'Slack active with inactive'} | ${false} | ${[slackActive, ...mockInactiveIntegrations]} | ${[slackActive, ...mockInactiveIntegrations]}
+ ${'Slack active with inactive'} | ${true} | ${[slackActive, ...mockInactiveIntegrations]} | ${[slackActive, ...mockInactiveIntegrations]}
+ ${'Slack inactive with active'} | ${false} | ${[slackInactive, ...mockActiveIntegrations]} | ${[slackInactive, ...mockActiveIntegrations]}
+ ${'Slack inactive with active'} | ${true} | ${[slackInactive, ...mockActiveIntegrations]} | ${mockActiveIntegrations}
+ ${'Slack inactive with inactive'} | ${false} | ${[slackInactive, ...mockInactiveIntegrations]} | ${[slackInactive, ...mockInactiveIntegrations]}
+ ${'Slack inactive with inactive'} | ${true} | ${[slackInactive, ...mockInactiveIntegrations]} | ${mockInactiveIntegrations}
+ ${'Slack active with active and inactive'} | ${true} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]}
+ ${'Slack active with active and inactive'} | ${false} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackActive, ...mockActiveIntegrations, ...mockInactiveIntegrations]}
+ ${'Slack inactive with active and inactive'} | ${true} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[...mockActiveIntegrations, ...mockInactiveIntegrations]}
+ ${'Slack inactive with active and inactive'} | ${false} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]} | ${[slackInactive, ...mockActiveIntegrations, ...mockInactiveIntegrations]}
+ `('when $desc and flag "$flagIsOn"', ({ flagIsOn, integrations, expectedIntegrations }) => {
+ beforeEach(() => {
+ createComponent({ integrations }, flagIsOn);
+ });
+
+ it('renders correctly', () => {
+ const links = wrapper.findAllComponents(GlLink);
+ expect(links).toHaveLength(expectedIntegrations.length);
+ expectedIntegrations.forEach((integration, index) => {
+ expect(links.at(index).text()).toBe(integration.title);
+ });
+ });
+ });
+ });
});
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 b6b34e1063b..9687d528321 100644
--- a/spec/frontend/invite_members/components/invite_members_modal_spec.js
+++ b/spec/frontend/invite_members/components/invite_members_modal_spec.js
@@ -2,6 +2,7 @@ import { GlLink, GlModal, GlSprintf, GlFormGroup, GlCollapse, GlIcon } from '@gi
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { stubComponent } from 'helpers/stub_component';
+import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
@@ -20,6 +21,8 @@ import {
LEARN_GITLAB,
EXPANDED_ERRORS,
EMPTY_INVITES_ALERT_TEXT,
+ ON_CELEBRATION_TRACK_LABEL,
+ INVITE_MEMBER_MODAL_TRACKING_CATEGORY,
} from '~/invite_members/constants';
import eventHub from '~/invite_members/event_hub';
import ContentTransition from '~/vue_shared/components/content_transition.vue';
@@ -58,6 +61,13 @@ jest.mock('~/lib/utils/url_utility', () => ({
describe('InviteMembersModal', () => {
let wrapper;
let mock;
+ let trackingSpy;
+
+ const expectTracking = (
+ action,
+ label = undefined,
+ category = INVITE_MEMBER_MODAL_TRACKING_CATEGORY,
+ ) => expect(trackingSpy).toHaveBeenCalledWith(category, action, { label, category });
const createComponent = (props = {}, stubs = {}) => {
wrapper = shallowMountExtended(InviteMembersModal, {
@@ -66,6 +76,7 @@ describe('InviteMembersModal', () => {
},
propsData: {
usersLimitDataset: {},
+ activeTrialDataset: {},
fullPath: 'project',
...propsData,
...props,
@@ -83,12 +94,20 @@ describe('InviteMembersModal', () => {
});
};
- const createInviteMembersToProjectWrapper = (usersLimitDataset = {}, stubs = {}) => {
- createComponent({ usersLimitDataset, isProject: true }, stubs);
+ const createInviteMembersToProjectWrapper = (
+ usersLimitDataset = {},
+ activeTrialDataset = {},
+ stubs = {},
+ ) => {
+ createComponent({ usersLimitDataset, activeTrialDataset, isProject: true }, stubs);
};
- const createInviteMembersToGroupWrapper = (usersLimitDataset = {}, stubs = {}) => {
- createComponent({ usersLimitDataset, isProject: false }, stubs);
+ const createInviteMembersToGroupWrapper = (
+ usersLimitDataset = {},
+ activeTrialDataset = {},
+ stubs = {},
+ ) => {
+ createComponent({ usersLimitDataset, activeTrialDataset, isProject: false }, stubs);
};
beforeEach(() => {
@@ -129,7 +148,7 @@ describe('InviteMembersModal', () => {
const findProjectSelect = () => wrapper.findByTestId('invite-members-modal-project-select');
const findNoProjectsAlert = () => wrapper.findByTestId('invite-members-modal-no-projects-alert');
const findCelebrationEmoji = () => wrapper.findComponent(GlEmoji);
- const triggerOpenModal = async ({ mode = 'default', source }) => {
+ const triggerOpenModal = async ({ mode = 'default', source } = {}) => {
eventHub.$emit('openModal', { mode, source });
await nextTick();
};
@@ -291,7 +310,7 @@ describe('InviteMembersModal', () => {
});
});
- describe('displaying the correct introText and form group description', () => {
+ describe('rendering with tracking considerations', () => {
describe('when inviting to a project', () => {
describe('when inviting members', () => {
beforeEach(() => {
@@ -318,7 +337,7 @@ describe('InviteMembersModal', () => {
describe('when inviting members with celebration', () => {
beforeEach(async () => {
createInviteMembersToProjectWrapper();
- await triggerOpenModal({ mode: 'celebrate' });
+ await triggerOpenModal({ mode: 'celebrate', source: ON_CELEBRATION_TRACK_LABEL });
});
it('renders the modal with confetti', () => {
@@ -344,6 +363,26 @@ describe('InviteMembersModal', () => {
expect(membersFormGroupDescription()).toContain(MEMBERS_PLACEHOLDER);
});
});
+
+ describe('tracking', () => {
+ it('tracks actions', async () => {
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ const mockEvent = { preventDefault: jest.fn() };
+
+ await triggerOpenModal({ mode: 'celebrate', source: ON_CELEBRATION_TRACK_LABEL });
+
+ expectTracking('render', ON_CELEBRATION_TRACK_LABEL);
+
+ findModal().vm.$emit('cancel', mockEvent);
+ expectTracking('click_cancel', ON_CELEBRATION_TRACK_LABEL);
+
+ findModal().vm.$emit('close');
+ expectTracking('click_x', ON_CELEBRATION_TRACK_LABEL);
+
+ unmockTracking();
+ });
+ });
});
});
@@ -361,6 +400,32 @@ describe('InviteMembersModal', () => {
});
});
});
+
+ describe('tracking', () => {
+ it.each`
+ desc | source | label
+ ${'unknown'} | ${{}} | ${'unknown'}
+ ${'known'} | ${{ source: '_invite_source_' }} | ${'_invite_source_'}
+ `('tracks actions with $desc source', async ({ source, label }) => {
+ createInviteMembersToProjectWrapper();
+
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
+
+ const mockEvent = { preventDefault: jest.fn() };
+
+ await triggerOpenModal(source);
+
+ expectTracking('render', label);
+
+ findModal().vm.$emit('cancel', mockEvent);
+ expectTracking('click_cancel', label);
+
+ findModal().vm.$emit('close');
+ expectTracking('click_x', label);
+
+ unmockTracking();
+ });
+ });
});
describe('rendering the user limit notification', () => {
@@ -625,6 +690,7 @@ describe('InviteMembersModal', () => {
createComponent();
await triggerMembersTokenSelect([user3]);
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
});
@@ -842,17 +908,23 @@ describe('InviteMembersModal', () => {
createComponent();
await triggerMembersTokenSelect([user1, user3]);
+ trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
wrapper.vm.$toast = { show: jest.fn() };
jest.spyOn(Api, 'inviteGroupMembers').mockResolvedValue({ data: postData });
});
describe('when triggered from regular mounting', () => {
- beforeEach(() => {
+ beforeEach(async () => {
+ await triggerOpenModal({ source: '_invite_source_' });
+
clickInviteButton();
});
- it('calls Api inviteGroupMembers with the correct params', () => {
- expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
+ it('calls Api inviteGroupMembers with the correct params and invite source', () => {
+ expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, {
+ ...postData,
+ invite_source: '_invite_source_',
+ });
});
it('displays the successful toastMessage', () => {
@@ -866,17 +938,20 @@ describe('InviteMembersModal', () => {
it('does not call reloadOnInvitationSuccess', () => {
expect(reloadOnInvitationSuccess).not.toHaveBeenCalled();
});
+
+ it('tracks successful invite when source is known', () => {
+ expectTracking('invite_successful', '_invite_source_');
+
+ unmockTracking();
+ });
});
- it('calls Apis with the invite source passed through to openModal', async () => {
- await triggerOpenModal({ source: '_invite_source_' });
+ it('calls Apis without the invite source passed through to openModal', async () => {
+ await triggerOpenModal();
clickInviteButton();
- expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, {
- ...postData,
- invite_source: '_invite_source_',
- });
+ expect(Api.inviteGroupMembers).toHaveBeenCalledWith(propsData.id, postData);
});
});
});
diff --git a/spec/frontend/invite_members/components/invite_modal_base_spec.js b/spec/frontend/invite_members/components/invite_modal_base_spec.js
index db2afbbd141..f34f9902514 100644
--- a/spec/frontend/invite_members/components/invite_modal_base_spec.js
+++ b/spec/frontend/invite_members/components/invite_modal_base_spec.js
@@ -70,6 +70,11 @@ describe('InviteModalBase', () => {
const findActionButton = () => wrapper.find('.js-modal-action-primary');
describe('rendering the modal', () => {
+ let trackingSpy;
+
+ const expectTracking = (action, label = undefined, category = undefined) =>
+ expect(trackingSpy).toHaveBeenCalledWith(category, action, { label, category });
+
beforeEach(() => {
createComponent();
});
@@ -151,14 +156,6 @@ describe('InviteModalBase', () => {
});
describe('when users limit is reached', () => {
- let trackingSpy;
-
- const expectTracking = (action, label) =>
- expect(trackingSpy).toHaveBeenCalledWith('default', action, {
- label,
- category: 'default',
- });
-
beforeEach(() => {
createComponent(
{ props: { usersLimitDataset: { membersPath, purchasePath, reachedLimit: true } } },
@@ -176,7 +173,7 @@ describe('InviteModalBase', () => {
const modal = wrapper.findComponent(GlModal);
modal.vm.$emit('shown');
- expectTracking('render', ON_SHOW_TRACK_LABEL);
+ expectTracking('render', ON_SHOW_TRACK_LABEL, 'default');
unmockTracking();
});
diff --git a/spec/frontend/invite_members/components/project_select_spec.js b/spec/frontend/invite_members/components/project_select_spec.js
index acc062b5fff..6fbf95362fa 100644
--- a/spec/frontend/invite_members/components/project_select_spec.js
+++ b/spec/frontend/invite_members/components/project_select_spec.js
@@ -1,4 +1,4 @@
-import { GlSearchBoxByType, GlAvatarLabeled, GlDropdownItem } from '@gitlab/ui';
+import { GlAvatarLabeled, GlCollapsibleListbox } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import * as projectsApi from '~/api/projects_api';
@@ -9,7 +9,12 @@ describe('ProjectSelect', () => {
let wrapper;
const createComponent = () => {
- wrapper = shallowMountExtended(ProjectSelect, {});
+ wrapper = shallowMountExtended(ProjectSelect, {
+ stubs: {
+ GlCollapsibleListbox,
+ GlAvatarLabeled,
+ },
+ });
};
beforeEach(() => {
@@ -22,16 +27,24 @@ describe('ProjectSelect', () => {
wrapper.destroy();
});
- const findSearchBoxByType = () => wrapper.findComponent(GlSearchBoxByType);
- const findDropdownItem = (index) => wrapper.findAllComponents(GlDropdownItem).at(index);
- const findAvatarLabeled = (index) => findDropdownItem(index).findComponent(GlAvatarLabeled);
- const findEmptyResultMessage = () => wrapper.findByTestId('empty-result-message');
- const findErrorMessage = () => wrapper.findByTestId('error-message');
-
- it('renders GlSearchBoxByType with default attributes', () => {
- expect(findSearchBoxByType().exists()).toBe(true);
- expect(findSearchBoxByType().vm.$attrs).toMatchObject({
- placeholder: 'Search projects',
+ const findGlCollapsibleListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findAvatarLabeled = (index) => wrapper.findAllComponents(GlAvatarLabeled).at(index);
+
+ it('renders GlCollapsibleListbox with default props', () => {
+ expect(findGlCollapsibleListbox().exists()).toBe(true);
+ expect(findGlCollapsibleListbox().props()).toMatchObject({
+ items: [],
+ loading: false,
+ multiple: false,
+ noResultsText: 'No matching results',
+ placement: 'left',
+ searchPlaceholder: 'Search projects',
+ searchable: true,
+ searching: false,
+ size: 'medium',
+ toggleText: 'Select a project',
+ totalItems: null,
+ variant: 'default',
});
});
@@ -48,7 +61,7 @@ describe('ProjectSelect', () => {
}),
);
- findSearchBoxByType().vm.$emit('input', project1.name);
+ findGlCollapsibleListbox().vm.$emit('search', project1.name);
});
it('calls the API', () => {
@@ -61,14 +74,12 @@ describe('ProjectSelect', () => {
});
it('displays loading icon while waiting for API call to resolve and then sets loading false', async () => {
- expect(findSearchBoxByType().props('isLoading')).toBe(true);
+ expect(findGlCollapsibleListbox().props('searching')).toBe(true);
resolveApiRequest({ data: allProjects });
await waitForPromises();
- expect(findSearchBoxByType().props('isLoading')).toBe(false);
- expect(findEmptyResultMessage().exists()).toBe(false);
- expect(findErrorMessage().exists()).toBe(false);
+ expect(findGlCollapsibleListbox().props('searching')).toBe(false);
});
it('displays a dropdown item and avatar for each project fetched', async () => {
@@ -76,11 +87,11 @@ describe('ProjectSelect', () => {
await waitForPromises();
allProjects.forEach((project, index) => {
- expect(findDropdownItem(index).attributes('name')).toBe(project.name_with_namespace);
expect(findAvatarLabeled(index).attributes()).toMatchObject({
src: project.avatar_url,
'entity-id': String(project.id),
'entity-name': project.name_with_namespace,
+ size: '32',
});
expect(findAvatarLabeled(index).props('label')).toBe(project.name_with_namespace);
});
@@ -90,16 +101,17 @@ describe('ProjectSelect', () => {
resolveApiRequest({ data: [] });
await waitForPromises();
- expect(findEmptyResultMessage().text()).toBe('No matching results');
+ expect(findGlCollapsibleListbox().text()).toBe('No matching results');
});
it('displays the error message when the fetch fails', async () => {
rejectApiRequest();
await waitForPromises();
- expect(findErrorMessage().text()).toBe(
- 'There was an error fetching the projects. Please try again.',
- );
+ // To be displayed in GlCollapsibleListbox once we implement
+ // https://gitlab.com/gitlab-org/gitlab-ui/-/issues/2132
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/389974
+ expect(findGlCollapsibleListbox().text()).toBe('No matching results');
});
});
});
diff --git a/spec/frontend/invite_members/mock_data/api_response_data.js b/spec/frontend/invite_members/mock_data/api_response_data.js
index 9509422b603..4ab9026c531 100644
--- a/spec/frontend/invite_members/mock_data/api_response_data.js
+++ b/spec/frontend/invite_members/mock_data/api_response_data.js
@@ -6,7 +6,7 @@ export const project1 = {
};
export const project2 = {
id: 2,
- name: 'Project One',
+ name: 'Project Two',
name_with_namespace: 'Project Two',
avatar_url: 'test2',
};
diff --git a/spec/frontend/issuable/helpers.js b/spec/frontend/issuable/helpers.js
new file mode 100644
index 00000000000..632d69c2c88
--- /dev/null
+++ b/spec/frontend/issuable/helpers.js
@@ -0,0 +1,18 @@
+export function getSaveableFormChildren(form, exclude = ['input.js-toggle-draft']) {
+ const children = Array.from(form.children);
+ const saveable = children.filter((e) => {
+ const isFiltered = exclude.reduce(
+ ({ isFiltered: filtered, element }, selector) => {
+ return {
+ isFiltered: filtered || element.matches(selector),
+ element,
+ };
+ },
+ { isFiltered: false, element: e },
+ );
+
+ return !isFiltered.isFiltered;
+ });
+
+ return saveable;
+}
diff --git a/spec/frontend/issuable/issuable_form_spec.js b/spec/frontend/issuable/issuable_form_spec.js
index 28ec0e22d8b..3e778e50fb8 100644
--- a/spec/frontend/issuable/issuable_form_spec.js
+++ b/spec/frontend/issuable/issuable_form_spec.js
@@ -4,6 +4,8 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import IssuableForm from '~/issuable/issuable_form';
import setWindowLocation from 'helpers/set_window_location_helper';
+import { getSaveableFormChildren } from './helpers';
+
jest.mock('~/autosave');
const createIssuable = (form) => {
@@ -18,6 +20,7 @@ describe('IssuableForm', () => {
setHTMLFixture(`
<form>
<input name="[title]" />
+ <input type="checkbox" class="js-toggle-draft" />
<textarea name="[description]"></textarea>
</form>
`);
@@ -99,10 +102,11 @@ describe('IssuableForm', () => {
])('creates $id autosave when $id input exist', ({ id, input, selector }) => {
$form.append(input);
const $input = $form.find(selector);
- const totalAutosaveFormFields = $form.children().length;
createIssuable($form);
- expect(Autosave).toHaveBeenCalledTimes(totalAutosaveFormFields);
+ const children = getSaveableFormChildren($form[0]);
+
+ expect(Autosave).toHaveBeenCalledTimes(children.length);
expect(Autosave).toHaveBeenLastCalledWith(
$input.get(0),
['/', '', id],
@@ -153,12 +157,17 @@ describe('IssuableForm', () => {
});
});
- describe('wip', () => {
+ describe('draft', () => {
+ let titleField;
+ let toggleDraft;
+
beforeEach(() => {
instance = createIssuable($form);
+ titleField = document.querySelector('input[name="[title]"]');
+ toggleDraft = document.querySelector('input.js-toggle-draft');
});
- describe('removeWip', () => {
+ describe('removeDraft', () => {
it.each`
prefix
${'draFT: '}
@@ -169,25 +178,25 @@ describe('IssuableForm', () => {
${' (DrafT)'}
${'draft: [draft] (draft)'}
`('removes "$prefix" from the beginning of the title', ({ prefix }) => {
- instance.titleField.val(`${prefix}The Issuable's Title Value`);
+ titleField.value = `${prefix}The Issuable's Title Value`;
- instance.removeWip();
+ instance.removeDraft();
- expect(instance.titleField.val()).toBe("The Issuable's Title Value");
+ expect(titleField.value).toBe("The Issuable's Title Value");
});
});
- describe('addWip', () => {
+ describe('addDraft', () => {
it("properly adds the work in progress prefix to the Issuable's title", () => {
- instance.titleField.val("The Issuable's Title Value");
+ titleField.value = "The Issuable's Title Value";
- instance.addWip();
+ instance.addDraft();
- expect(instance.titleField.val()).toBe("Draft: The Issuable's Title Value");
+ expect(titleField.value).toBe("Draft: The Issuable's Title Value");
});
});
- describe('workInProgress', () => {
+ describe('isMarkedDraft', () => {
it.each`
title | expected
${'draFT: something is happening'} | ${true}
@@ -195,10 +204,45 @@ describe('IssuableForm', () => {
${'something is happening to drafts'} | ${false}
${'something is happening'} | ${false}
`('returns $expected with "$title"', ({ title, expected }) => {
- instance.titleField.val(title);
+ titleField.value = title;
- expect(instance.workInProgress()).toBe(expected);
+ expect(instance.isMarkedDraft()).toBe(expected);
});
});
+
+ describe('readDraftStatus', () => {
+ it.each`
+ title | checked
+ ${'Draft: my title'} | ${true}
+ ${'my title'} | ${false}
+ `(
+ 'sets the draft checkbox checked status to $checked when the title is $title',
+ ({ title, checked }) => {
+ titleField.value = title;
+
+ instance.readDraftStatus();
+
+ expect(toggleDraft.checked).toBe(checked);
+ },
+ );
+ });
+
+ describe('writeDraftStatus', () => {
+ it.each`
+ checked | title
+ ${true} | ${'Draft: my title'}
+ ${false} | ${'my title'}
+ `(
+ 'updates the title to $title when the draft checkbox checked status is $checked',
+ ({ checked, title }) => {
+ titleField.value = 'my title';
+ toggleDraft.checked = checked;
+
+ instance.writeDraftStatus();
+
+ expect(titleField.value).toBe(title);
+ },
+ );
+ });
});
});
diff --git a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
index 16d4459f597..72fcab63ba7 100644
--- a/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
+++ b/spec/frontend/issuable/related_issues/components/add_issuable_form_spec.js
@@ -1,9 +1,10 @@
import { GlFormGroup } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
+import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue';
import IssueToken from '~/related_issues/components/issue_token.vue';
-import { issuableTypesMap, linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants';
+import { linkedIssueTypesMap, PathIdSeparator } from '~/related_issues/constants';
const issuable1 = {
id: 200,
@@ -125,7 +126,7 @@ describe('AddIssuableForm', () => {
wrapper = mount(AddIssuableForm, {
propsData: {
inputValue: '',
- issuableType: issuableTypesMap.ISSUE,
+ issuableType: TYPE_ISSUE,
pathIdSeparator,
pendingReferences: [],
},
@@ -142,7 +143,7 @@ describe('AddIssuableForm', () => {
wrapper = shallowMount(AddIssuableForm, {
propsData: {
inputValue: '',
- issuableType: issuableTypesMap.EPIC,
+ issuableType: TYPE_EPIC,
pathIdSeparator,
pendingReferences: [],
},
@@ -156,9 +157,9 @@ describe('AddIssuableForm', () => {
describe('categorized issuables', () => {
it.each`
- issuableType | pathIdSeparator | contextHeader | contextFooter
- ${issuableTypesMap.ISSUE} | ${PathIdSeparator.Issue} | ${'The current issue'} | ${'the following issues'}
- ${issuableTypesMap.EPIC} | ${PathIdSeparator.Epic} | ${'The current epic'} | ${'the following epics'}
+ issuableType | pathIdSeparator | contextHeader | contextFooter
+ ${TYPE_ISSUE} | ${PathIdSeparator.Issue} | ${'The current issue'} | ${'the following issues'}
+ ${TYPE_EPIC} | ${PathIdSeparator.Epic} | ${'The current epic'} | ${'the following epics'}
`(
'show header text as "$contextHeader" and footer text as "$contextFooter" issuableType is set to $issuableType',
({ issuableType, contextHeader, contextFooter }) => {
@@ -184,7 +185,7 @@ describe('AddIssuableForm', () => {
propsData: {
inputValue: '',
showCategorizedIssues: true,
- issuableType: issuableTypesMap.ISSUE,
+ issuableType: TYPE_ISSUE,
pathIdSeparator,
pendingReferences: [],
},
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
index 996b2406240..ff8d5073005 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_block_spec.js
@@ -6,10 +6,10 @@ import {
issuable2,
issuable3,
} from 'jest/issuable/components/related_issuable_mock_data';
+import { TYPE_ISSUE } from '~/issues/constants';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import AddIssuableForm from '~/related_issues/components/add_issuable_form.vue';
import {
- issuableTypesMap,
linkedIssueTypesMap,
linkedIssueTypesTextMap,
PathIdSeparator,
@@ -34,7 +34,7 @@ describe('RelatedIssuesBlock', () => {
wrapper = mountExtended(RelatedIssuesBlock, {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
- issuableType: issuableTypesMap.ISSUE,
+ issuableType: TYPE_ISSUE,
},
});
});
@@ -237,7 +237,7 @@ describe('RelatedIssuesBlock', () => {
propsData: {
pathIdSeparator: PathIdSeparator.Issue,
relatedIssues: [issuable1, issuable2, issuable3],
- issuableType: issuableTypesMap.ISSUE,
+ issuableType: TYPE_ISSUE,
},
});
});
diff --git a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
index bedf8bcaf34..96c0b87e2cb 100644
--- a/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
+++ b/spec/frontend/issuable/related_issues/components/related_issues_root_spec.js
@@ -9,6 +9,11 @@ import {
} from 'jest/issuable/components/related_issuable_mock_data';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_CONFLICT,
+ HTTP_STATUS_OK,
+ HTTP_STATUS_UNPROCESSABLE_ENTITY,
+} from '~/lib/utils/http_status';
import { linkedIssueTypesMap } from '~/related_issues/constants';
import RelatedIssuesBlock from '~/related_issues/components/related_issues_block.vue';
import RelatedIssuesRoot from '~/related_issues/components/related_issues_root.vue';
@@ -24,7 +29,7 @@ describe('RelatedIssuesRoot', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(defaultProps.endpoint).reply(200, []);
+ mock.onGet(defaultProps.endpoint).reply(HTTP_STATUS_OK, []);
});
afterEach(() => {
@@ -59,7 +64,7 @@ describe('RelatedIssuesRoot', () => {
});
it('removes related issue on API success', async () => {
- mock.onDelete(issuable1.referencePath).reply(200, { issues: [] });
+ mock.onDelete(issuable1.referencePath).reply(HTTP_STATUS_OK, { issues: [] });
findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', issuable1.id);
await axios.waitForAll();
@@ -68,7 +73,7 @@ describe('RelatedIssuesRoot', () => {
});
it('does not remove related issue on API error', async () => {
- mock.onDelete(issuable1.referencePath).reply(422, {});
+ mock.onDelete(issuable1.referencePath).reply(HTTP_STATUS_UNPROCESSABLE_ENTITY, {});
findRelatedIssuesBlock().vm.$emit('relatedIssueRemoveRequest', issuable1.id);
await axios.waitForAll();
@@ -163,7 +168,7 @@ describe('RelatedIssuesRoot', () => {
});
it('submits pending issue as related issue', async () => {
- mock.onPost(defaultProps.endpoint).reply(200, {
+ mock.onPost(defaultProps.endpoint).reply(HTTP_STATUS_OK, {
issuables: [issuable1],
result: {
message: 'something was successfully related',
@@ -182,7 +187,7 @@ describe('RelatedIssuesRoot', () => {
});
it('submits multiple pending issues as related issues', async () => {
- mock.onPost(defaultProps.endpoint).reply(200, {
+ mock.onPost(defaultProps.endpoint).reply(HTTP_STATUS_OK, {
issuables: [issuable1, issuable2],
result: {
message: 'something was successfully related',
@@ -204,7 +209,7 @@ describe('RelatedIssuesRoot', () => {
it('passes an error message from the backend upon error', async () => {
const input = '#123';
const message = 'error';
- mock.onPost(defaultProps.endpoint).reply(409, { message });
+ mock.onPost(defaultProps.endpoint).reply(HTTP_STATUS_CONFLICT, { message });
wrapper.vm.store.setPendingReferences([issuable1.reference, issuable2.reference]);
expect(findRelatedIssuesBlock().props('hasError')).toBe(false);
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 841cea28ffc..77d5a0579a4 100644
--- a/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
+++ b/spec/frontend/issues/dashboard/components/issues_dashboard_app_spec.js
@@ -19,6 +19,7 @@ import {
setSortPreferenceMutationResponseWithErrors,
} from 'jest/issues/list/mock_data';
import IssuesDashboardApp from '~/issues/dashboard/components/issues_dashboard_app.vue';
+import getIssuesCountsQuery from '~/issues/dashboard/queries/get_issues_counts.query.graphql';
import { CREATED_DESC, i18n, UPDATED_DESC, urlSortParams } from '~/issues/list/constants';
import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
import { getSortKey, getSortOptions } from '~/issues/list/utils';
@@ -27,13 +28,20 @@ import { scrollUp } from '~/lib/utils/scroll_utils';
import {
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
+ TOKEN_TYPE_CONFIDENTIAL,
TOKEN_TYPE_LABEL,
TOKEN_TYPE_MILESTONE,
TOKEN_TYPE_MY_REACTION,
+ TOKEN_TYPE_SEARCH_WITHIN,
+ TOKEN_TYPE_TYPE,
} 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';
-import { emptyIssuesQueryResponse, issuesQueryResponse } from '../mock_data';
+import {
+ emptyIssuesQueryResponse,
+ issuesCountsQueryResponse,
+ issuesQueryResponse,
+} from '../mock_data';
jest.mock('@sentry/browser');
jest.mock('~/lib/utils/scroll_utils', () => ({ scrollUp: jest.fn() }));
@@ -69,24 +77,24 @@ describe('IssuesDashboardApp component', () => {
defaultQueryResponse.data.issues.nodes[0].weight = 5;
}
- const findCalendarButton = () =>
- wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.calendarButtonText });
+ const findCalendarButton = () => wrapper.findByRole('link', { name: i18n.calendarLabel });
const findEmptyState = () => wrapper.findComponent(GlEmptyState);
const findIssuableList = () => wrapper.findComponent(IssuableList);
const findIssueCardStatistics = () => wrapper.findComponent(IssueCardStatistics);
const findIssueCardTimeInfo = () => wrapper.findComponent(IssueCardTimeInfo);
- const findRssButton = () =>
- wrapper.findByRole('link', { name: IssuesDashboardApp.i18n.rssButtonText });
+ const findRssButton = () => wrapper.findByRole('link', { name: i18n.rssLabel });
const mountComponent = ({
provide = {},
issuesQueryHandler = jest.fn().mockResolvedValue(defaultQueryResponse),
- sortPreferenceMutationResponse = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
+ issuesCountsQueryHandler = jest.fn().mockResolvedValue(issuesCountsQueryResponse),
+ sortPreferenceMutationHandler = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse),
} = {}) => {
wrapper = mountExtended(IssuesDashboardApp, {
apolloProvider: createMockApollo([
[getIssuesQuery, issuesQueryHandler],
- [setSortPreferenceMutation, sortPreferenceMutationResponse],
+ [getIssuesCountsQuery, issuesCountsQueryHandler],
+ [setSortPreferenceMutation, sortPreferenceMutationHandler],
]),
provide: {
...defaultProvide,
@@ -112,7 +120,9 @@ describe('IssuesDashboardApp component', () => {
return waitForPromises();
});
- it('renders IssuableList component', () => {
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/391722
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('renders IssuableList component', () => {
expect(findIssuableList().props()).toMatchObject({
currentTab: IssuableStates.Opened,
hasNextPage: true,
@@ -123,13 +133,18 @@ describe('IssuesDashboardApp component', () => {
issuablesLoading: false,
namespace: 'dashboard',
recentSearchesStorageKey: 'issues',
- searchInputPlaceholder: IssuesDashboardApp.i18n.searchInputPlaceholder,
+ searchInputPlaceholder: i18n.searchPlaceholder,
showPaginationControls: true,
sortOptions: getSortOptions({
hasBlockedIssuesFeature: defaultProvide.hasBlockedIssuesFeature,
hasIssuableHealthStatusFeature: defaultProvide.hasIssuableHealthStatusFeature,
hasIssueWeightsFeature: defaultProvide.hasIssueWeightsFeature,
}),
+ tabCounts: {
+ opened: 1,
+ closed: 2,
+ all: 3,
+ },
tabs: IssuesDashboardApp.IssuableListTabs,
urlParams: {
sort: urlSortParams[CREATED_DESC],
@@ -192,9 +207,9 @@ describe('IssuesDashboardApp component', () => {
it('renders empty state', () => {
expect(findEmptyState().props()).toMatchObject({
- description: IssuesDashboardApp.i18n.emptyStateWithFilterDescription,
+ description: i18n.noSearchResultsDescription,
svgPath: defaultProvide.emptyStateWithFilterSvgPath,
- title: IssuesDashboardApp.i18n.emptyStateWithFilterTitle,
+ title: i18n.noSearchResultsTitle,
});
});
});
@@ -217,7 +232,7 @@ describe('IssuesDashboardApp component', () => {
expect(findEmptyState().props()).toMatchObject({
description: null,
svgPath: defaultProvide.emptyStateWithoutFilterSvgPath,
- title: IssuesDashboardApp.i18n.emptyStateWithoutFilterTitle,
+ title: i18n.noSearchNoFilterTitle,
});
});
});
@@ -286,20 +301,28 @@ 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();
- });
+ describe('errors', () => {
+ describe.each`
+ error | mountOption | message
+ ${'fetching issues'} | ${'issuesQueryHandler'} | ${i18n.errorFetchingIssues}
+ ${'fetching issue counts'} | ${'issuesCountsQueryHandler'} | ${i18n.errorFetchingCounts}
+ `('when there is an error $error', ({ mountOption, message }) => {
+ beforeEach(() => {
+ setWindowLocation(locationSearch);
+ mountComponent({ [mountOption]: jest.fn().mockRejectedValue(new Error('ERROR')) });
+ jest.runOnlyPendingTimers();
+ return waitForPromises();
+ });
- it('shows an error message', () => {
- expect(findIssuableList().props('error')).toBe(i18n.errorFetchingIssues);
- expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
+ it('shows an error message', () => {
+ expect(findIssuableList().props('error')).toBe(message);
+ expect(Sentry.captureException).toHaveBeenCalledWith(new Error('ERROR'));
+ });
});
it('clears error message when "dismiss-alert" event is emitted from IssuableList', async () => {
+ mountComponent({ issuesQueryHandler: jest.fn().mockRejectedValue(new Error()) });
+
findIssuableList().vm.$emit('dismiss-alert');
await nextTick();
@@ -337,9 +360,12 @@ describe('IssuesDashboardApp component', () => {
expect(findIssuableList().props('searchTokens')).toMatchObject([
{ type: TOKEN_TYPE_ASSIGNEE, preloadedUsers },
{ type: TOKEN_TYPE_AUTHOR, preloadedUsers },
+ { type: TOKEN_TYPE_CONFIDENTIAL },
{ type: TOKEN_TYPE_LABEL },
{ type: TOKEN_TYPE_MILESTONE },
{ type: TOKEN_TYPE_MY_REACTION },
+ { type: TOKEN_TYPE_SEARCH_WITHIN },
+ { type: TOKEN_TYPE_TYPE },
]);
});
});
@@ -401,7 +427,7 @@ describe('IssuesDashboardApp component', () => {
describe('when user is signed in', () => {
it('calls mutation to save sort preference', () => {
const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
- mountComponent({ sortPreferenceMutationResponse: mutationMock });
+ mountComponent({ sortPreferenceMutationHandler: mutationMock });
findIssuableList().vm.$emit('sort', UPDATED_DESC);
@@ -412,7 +438,7 @@ describe('IssuesDashboardApp component', () => {
const mutationMock = jest
.fn()
.mockResolvedValue(setSortPreferenceMutationResponseWithErrors);
- mountComponent({ sortPreferenceMutationResponse: mutationMock });
+ mountComponent({ sortPreferenceMutationHandler: mutationMock });
findIssuableList().vm.$emit('sort', UPDATED_DESC);
await waitForPromises();
@@ -426,7 +452,7 @@ describe('IssuesDashboardApp component', () => {
const mutationMock = jest.fn().mockResolvedValue(setSortPreferenceMutationResponse);
mountComponent({
provide: { isSignedIn: false },
- sortPreferenceMutationResponse: mutationMock,
+ sortPreferenceMutationHandler: mutationMock,
});
findIssuableList().vm.$emit('sort', CREATED_DESC);
diff --git a/spec/frontend/issues/dashboard/mock_data.js b/spec/frontend/issues/dashboard/mock_data.js
index feb4cb80bd8..e789360d1d5 100644
--- a/spec/frontend/issues/dashboard/mock_data.js
+++ b/spec/frontend/issues/dashboard/mock_data.js
@@ -86,3 +86,17 @@ export const emptyIssuesQueryResponse = {
},
},
};
+
+export const issuesCountsQueryResponse = {
+ data: {
+ openedIssues: {
+ count: 1,
+ },
+ closedIssues: {
+ count: 2,
+ },
+ allIssues: {
+ count: 3,
+ },
+ },
+};
diff --git a/spec/frontend/issues/dashboard/utils_spec.js b/spec/frontend/issues/dashboard/utils_spec.js
index 08d00eee3e3..6a1fe6e4d70 100644
--- a/spec/frontend/issues/dashboard/utils_spec.js
+++ b/spec/frontend/issues/dashboard/utils_spec.js
@@ -3,6 +3,7 @@ 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';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('AutocompleteCache', () => {
let autocompleteCache;
@@ -42,7 +43,7 @@ describe('AutocompleteCache', () => {
let response;
beforeEach(async () => {
- axiosMock.onGet(url).replyOnce(200, data);
+ axiosMock.onGet(url).replyOnce(HTTP_STATUS_OK, data);
response = await autocompleteCache.fetch({ url, cacheName, searchProperty });
});
@@ -59,7 +60,7 @@ describe('AutocompleteCache', () => {
let response;
beforeEach(async () => {
- axiosMock.onGet(url).replyOnce(200, data);
+ axiosMock.onGet(url).replyOnce(HTTP_STATUS_OK, data);
jest.spyOn(fuzzaldrinPlus, 'filter');
// Populate cache
await autocompleteCache.fetch({ url, cacheName, searchProperty });
diff --git a/spec/frontend/issues/issue_spec.js b/spec/frontend/issues/issue_spec.js
index 089ea8dbbad..f04e766a78c 100644
--- a/spec/frontend/issues/issue_spec.js
+++ b/spec/frontend/issues/issue_spec.js
@@ -4,6 +4,7 @@ import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import Issue from '~/issues/issue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Issue', () => {
let testContext;
@@ -11,7 +12,7 @@ describe('Issue', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(/(.*)\/related_branches$/).reply(200, {});
+ mock.onGet(/(.*)\/related_branches$/).reply(HTTP_STATUS_OK, {});
testContext = {};
testContext.issue = new Issue();
diff --git a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
index 065139f10f4..0a2e4e7c671 100644
--- a/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
+++ b/spec/frontend/issues/list/components/empty_state_without_any_issues_spec.js
@@ -2,7 +2,7 @@ import { GlEmptyState, GlLink } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue';
-import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
+import NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue';
import { i18n } from '~/issues/list/constants';
describe('EmptyStateWithoutAnyIssues component', () => {
@@ -32,7 +32,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
wrapper.findByRole('link', { name: i18n.noIssuesDescription });
const findJiraDocsLink = () =>
wrapper.findByRole('link', { name: 'Enable the Jira integration' });
- const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown);
+ const findNewResourceDropdown = () => wrapper.findComponent(NewResourceDropdown);
const findNewIssueLink = () => wrapper.findByRole('link', { name: i18n.newIssueLabel });
const findNewProjectLink = () => wrapper.findByRole('link', { name: i18n.newProjectLabel });
@@ -47,7 +47,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
...provide,
},
stubs: {
- NewIssueDropdown: true,
+ NewResourceDropdown: true,
},
});
};
@@ -156,7 +156,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
it('renders', () => {
mountComponent({ props: { showNewIssueDropdown: true } });
- expect(findNewIssueDropdown().exists()).toBe(true);
+ expect(findNewResourceDropdown().exists()).toBe(true);
});
});
@@ -164,7 +164,7 @@ describe('EmptyStateWithoutAnyIssues component', () => {
it('does not render', () => {
mountComponent({ props: { showNewIssueDropdown: false } });
- expect(findNewIssueDropdown().exists()).toBe(false);
+ expect(findNewResourceDropdown().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/issues/list/components/issue_card_time_info_spec.js b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
index b0d3a63a8cf..ab4d023ee39 100644
--- a/spec/frontend/issues/list/components/issue_card_time_info_spec.js
+++ b/spec/frontend/issues/list/components/issue_card_time_info_spec.js
@@ -1,7 +1,7 @@
import { GlIcon, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { useFakeDate } from 'helpers/fake_date';
-import { IssuableStatus } from '~/issues/constants';
+import { STATUS_CLOSED, STATUS_OPEN } from '~/issues/constants';
import IssueCardTimeInfo from '~/issues/list/components/issue_card_time_info.vue';
describe('CE IssueCardTimeInfo component', () => {
@@ -25,7 +25,7 @@ describe('CE IssueCardTimeInfo component', () => {
const findDueDate = () => wrapper.find('[data-testid="issuable-due-date"]');
const mountComponent = ({
- state = IssuableStatus.Open,
+ state = STATUS_OPEN,
dueDate = issue.dueDate,
milestoneDueDate = issue.milestone.dueDate,
milestoneStartDate = issue.milestone.startDate,
@@ -102,7 +102,7 @@ describe('CE IssueCardTimeInfo component', () => {
it('does not render in red', () => {
wrapper = mountComponent({
dueDate: '2020-10-10',
- state: IssuableStatus.Closed,
+ state: STATUS_CLOSED,
});
expect(findDueDate().classes()).not.toContain('gl-text-red-500');
diff --git a/spec/frontend/issues/list/components/issues_list_app_spec.js b/spec/frontend/issues/list/components/issues_list_app_spec.js
index 4c5d8ce3cd1..8281ce0ed1a 100644
--- a/spec/frontend/issues/list/components/issues_list_app_spec.js
+++ b/spec/frontend/issues/list/components/issues_list_app_spec.js
@@ -22,6 +22,7 @@ import {
urlParams,
} from 'jest/issues/list/mock_data';
import { createAlert, VARIANT_INFO } from '~/flash';
+import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId, getIdFromGraphQLId } from '~/graphql_shared/utils';
import CsvImportExportButtons from '~/issuable/components/csv_import_export_buttons.vue';
import IssuableByEmail from '~/issuable/components/issuable_by_email.vue';
@@ -30,7 +31,7 @@ import { IssuableListTabs, IssuableStates } from '~/vue_shared/issuable/list/con
import EmptyStateWithAnyIssues from '~/issues/list/components/empty_state_with_any_issues.vue';
import EmptyStateWithoutAnyIssues from '~/issues/list/components/empty_state_without_any_issues.vue';
import IssuesListApp from '~/issues/list/components/issues_list_app.vue';
-import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
+import NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue';
import {
CREATED_DESC,
RELATIVE_POSITION,
@@ -42,6 +43,7 @@ import eventHub from '~/issues/list/eventhub';
import setSortPreferenceMutation from '~/issues/list/queries/set_sort_preference.mutation.graphql';
import { getSortKey, getSortOptions } from '~/issues/list/utils';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import { scrollUp } from '~/lib/utils/scroll_utils';
import { joinPaths } from '~/lib/utils/url_utility';
import {
@@ -51,7 +53,6 @@ import {
WORK_ITEM_TYPE_ENUM_TEST_CASE,
} from '~/work_items/constants';
import {
- FILTERED_SEARCH_TERM,
TOKEN_TYPE_ASSIGNEE,
TOKEN_TYPE_AUTHOR,
TOKEN_TYPE_CONFIDENTIAL,
@@ -98,7 +99,6 @@ describe('CE IssuesListApp component', () => {
hasScopedLabelsFeature: true,
initialEmail: 'email@example.com',
initialSort: CREATED_DESC,
- isAnonymousSearchDisabled: false,
isIssueRepositioningDisabled: false,
isProject: true,
isPublicVisibilityRestricted: false,
@@ -129,7 +129,12 @@ describe('CE IssuesListApp component', () => {
const findGlButtons = () => wrapper.findAllComponents(GlButton);
const findGlButtonAt = (index) => findGlButtons().at(index);
const findIssuableList = () => wrapper.findComponent(IssuableList);
- const findNewIssueDropdown = () => wrapper.findComponent(NewIssueDropdown);
+ const findNewResourceDropdown = () => wrapper.findComponent(NewResourceDropdown);
+
+ const findLabelsToken = () =>
+ findIssuableList()
+ .props('searchTokens')
+ .find((token) => token.type === TOKEN_TYPE_LABEL);
const mountComponent = ({
provide = {},
@@ -179,7 +184,7 @@ describe('CE IssuesListApp component', () => {
return waitForPromises();
});
- it('renders', () => {
+ it('renders', async () => {
expect(findIssuableList().props()).toMatchObject({
namespace: defaultProvide.fullPath,
recentSearchesStorageKey: 'issues',
@@ -314,13 +319,13 @@ describe('CE IssuesListApp component', () => {
it('does not render in a project context', () => {
wrapper = mountComponent({ provide: { isProject: true }, mountFn: mount });
- expect(findNewIssueDropdown().exists()).toBe(false);
+ expect(findNewResourceDropdown().exists()).toBe(false);
});
it('renders in a group context', () => {
wrapper = mountComponent({ provide: { isProject: false }, mountFn: mount });
- expect(findNewIssueDropdown().exists()).toBe(true);
+ expect(findNewResourceDropdown().exists()).toBe(true);
});
});
});
@@ -426,27 +431,6 @@ describe('CE IssuesListApp component', () => {
expect(findIssuableList().props('initialFilterValue')).toEqual(filteredTokens);
});
-
- describe('when anonymous searching is performed', () => {
- beforeEach(() => {
- setWindowLocation(locationSearch);
- wrapper = mountComponent({
- provide: { isAnonymousSearchDisabled: true, isSignedIn: false },
- });
- });
-
- it('is set from url params and removes search terms', () => {
- const expected = filteredTokens.filter((token) => token.type !== FILTERED_SEARCH_TERM);
- expect(findIssuableList().props('initialFilterValue')).toEqual(expected);
- });
-
- it('shows an alert to tell the user they must be signed in to search', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: IssuesListApp.i18n.anonymousSearchingMessage,
- variant: VARIANT_INFO,
- });
- });
- });
});
});
@@ -585,7 +569,7 @@ describe('CE IssuesListApp component', () => {
it('renders all tokens alphabetically', () => {
const preloadedUsers = [
- { ...mockCurrentUser, id: convertToGraphQLId('User', mockCurrentUser.id) },
+ { ...mockCurrentUser, id: convertToGraphQLId(TYPENAME_USER, mockCurrentUser.id) },
];
expect(findIssuableList().props('searchTokens')).toMatchObject([
@@ -782,7 +766,9 @@ describe('CE IssuesListApp component', () => {
});
it('displays an error message', async () => {
- axiosMock.onPut(joinPaths(issueOne.webPath, 'reorder')).reply(500);
+ axiosMock
+ .onPut(joinPaths(issueOne.webPath, 'reorder'))
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
findIssuableList().vm.$emit('reorder', { oldIndex: 0, newIndex: 1 });
await waitForPromises();
@@ -903,29 +889,6 @@ describe('CE IssuesListApp component', () => {
query: expect.objectContaining(urlParams),
});
});
-
- describe('when anonymous searching is performed', () => {
- beforeEach(() => {
- wrapper = mountComponent({
- provide: { isAnonymousSearchDisabled: true, isSignedIn: false },
- });
- router.push = jest.fn();
-
- findIssuableList().vm.$emit('filter', filteredTokens);
- });
-
- it('removes search terms', () => {
- const expected = filteredTokens.filter((token) => token.type !== FILTERED_SEARCH_TERM);
- expect(findIssuableList().props('initialFilterValue')).toEqual(expected);
- });
-
- it('shows an alert to tell the user they must be signed in to search', () => {
- expect(createAlert).toHaveBeenCalledWith({
- message: IssuesListApp.i18n.anonymousSearchingMessage,
- variant: VARIANT_INFO,
- });
- });
- });
});
describe('when "page-size-change" event is emitted by IssuableList', () => {
@@ -983,4 +946,30 @@ describe('CE IssuesListApp component', () => {
);
});
});
+
+ describe('when providing token for labels', () => {
+ it('passes function to fetchLatestLabels property if frontend caching is enabled', () => {
+ wrapper = mountComponent({
+ provide: {
+ glFeatures: {
+ frontendCaching: true,
+ },
+ },
+ });
+
+ expect(typeof findLabelsToken().fetchLatestLabels).toBe('function');
+ });
+
+ it('passes null to fetchLatestLabels property if frontend caching is disabled', () => {
+ wrapper = mountComponent({
+ provide: {
+ glFeatures: {
+ frontendCaching: false,
+ },
+ },
+ });
+
+ expect(findLabelsToken().fetchLatestLabels).toBe(null);
+ });
+ });
});
diff --git a/spec/frontend/issues/list/components/new_issue_dropdown_spec.js b/spec/frontend/issues/list/components/new_issue_dropdown_spec.js
deleted file mode 100644
index 2c8cf9caf5d..00000000000
--- a/spec/frontend/issues/list/components/new_issue_dropdown_spec.js
+++ /dev/null
@@ -1,133 +0,0 @@
-import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
-import { mount, shallowMount } from '@vue/test-utils';
-import Vue from 'vue';
-import VueApollo from 'vue-apollo';
-import createMockApollo from 'helpers/mock_apollo_helper';
-import waitForPromises from 'helpers/wait_for_promises';
-import NewIssueDropdown from '~/issues/list/components/new_issue_dropdown.vue';
-import searchProjectsQuery from '~/issues/list/queries/search_projects.query.graphql';
-import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
-import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
-import {
- emptySearchProjectsQueryResponse,
- project1,
- project3,
- searchProjectsQueryResponse,
-} from '../mock_data';
-
-describe('NewIssueDropdown component', () => {
- let wrapper;
-
- Vue.use(VueApollo);
-
- const mountComponent = ({
- search = '',
- queryResponse = searchProjectsQueryResponse,
- mountFn = shallowMount,
- } = {}) => {
- const requestHandlers = [[searchProjectsQuery, jest.fn().mockResolvedValue(queryResponse)]];
- const apolloProvider = createMockApollo(requestHandlers);
-
- return mountFn(NewIssueDropdown, {
- apolloProvider,
- provide: {
- fullPath: 'mushroom-kingdom',
- },
- data() {
- return { search };
- },
- });
- };
-
- const findDropdown = () => wrapper.findComponent(GlDropdown);
- const findInput = () => wrapper.findComponent(GlSearchBoxByType);
- const showDropdown = async () => {
- findDropdown().vm.$emit('shown');
- await waitForPromises();
- jest.advanceTimersByTime(DEBOUNCE_DELAY);
- await waitForPromises();
- };
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('renders a split dropdown', () => {
- wrapper = mountComponent();
-
- expect(findDropdown().props('split')).toBe(true);
- });
-
- it('renders a label for the dropdown toggle button', () => {
- wrapper = mountComponent();
-
- expect(findDropdown().attributes('toggle-text')).toBe(NewIssueDropdown.i18n.toggleButtonLabel);
- });
-
- it('focuses on input when dropdown is shown', async () => {
- wrapper = mountComponent({ mountFn: mount });
-
- const inputSpy = jest.spyOn(findInput().vm, 'focusInput');
-
- await showDropdown();
-
- expect(inputSpy).toHaveBeenCalledTimes(1);
- });
-
- it('renders projects with issues enabled', async () => {
- wrapper = mountComponent({ mountFn: mount });
- await showDropdown();
-
- const listItems = wrapper.findAll('li');
-
- expect(listItems.at(0).text()).toBe(project1.nameWithNamespace);
- expect(listItems.at(1).text()).toBe(project3.nameWithNamespace);
- });
-
- it('renders `No matches found` when there are no matches', async () => {
- wrapper = mountComponent({
- search: 'no matches',
- queryResponse: emptySearchProjectsQueryResponse,
- mountFn: mount,
- });
-
- await showDropdown();
-
- expect(wrapper.find('li').text()).toBe(NewIssueDropdown.i18n.noMatchesFound);
- });
-
- describe('when no project is selected', () => {
- beforeEach(() => {
- wrapper = mountComponent();
- });
-
- it('dropdown button is not a link', () => {
- expect(findDropdown().attributes('split-href')).toBeUndefined();
- });
-
- it('displays default text on the dropdown button', () => {
- expect(findDropdown().props('text')).toBe(NewIssueDropdown.i18n.defaultDropdownText);
- });
- });
-
- describe('when a project is selected', () => {
- beforeEach(async () => {
- wrapper = mountComponent({ mountFn: mount });
- await waitForPromises();
- await showDropdown();
-
- wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
- await waitForPromises();
- });
-
- it('dropdown button is a link', () => {
- const href = joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new');
-
- expect(findDropdown().attributes('split-href')).toBe(href);
- });
-
- it('displays project name on the dropdown button', () => {
- expect(findDropdown().props('text')).toBe(`New issue in ${project1.name}`);
- });
- });
-});
diff --git a/spec/frontend/issues/list/mock_data.js b/spec/frontend/issues/list/mock_data.js
index 70b1521ff70..1e8a81116f3 100644
--- a/spec/frontend/issues/list/mock_data.js
+++ b/spec/frontend/issues/list/mock_data.js
@@ -25,6 +25,7 @@ export const getIssuesQueryResponse = {
id: '1',
__typename: 'Project',
issues: {
+ __persist: true,
pageInfo: {
__typename: 'PageInfo',
hasNextPage: true,
@@ -34,6 +35,7 @@ export const getIssuesQueryResponse = {
},
nodes: [
{
+ __persist: true,
__typename: 'Issue',
id: 'gid://gitlab/Issue/123456',
iid: '789',
@@ -57,6 +59,7 @@ export const getIssuesQueryResponse = {
assignees: {
nodes: [
{
+ __persist: true,
__typename: 'UserCore',
id: 'gid://gitlab/User/234',
avatarUrl: 'avatar/url',
@@ -67,6 +70,7 @@ export const getIssuesQueryResponse = {
],
},
author: {
+ __persist: true,
__typename: 'UserCore',
id: 'gid://gitlab/User/456',
avatarUrl: 'avatar/url',
@@ -77,6 +81,7 @@ export const getIssuesQueryResponse = {
labels: {
nodes: [
{
+ __persist: true,
id: 'gid://gitlab/ProjectLabel/456',
color: '#333',
title: 'Label title',
@@ -343,49 +348,3 @@ export const urlParamsWithSpecialValues = {
weight: 'None',
health_status: 'None',
};
-
-export const project1 = {
- id: 'gid://gitlab/Group/26',
- issuesEnabled: true,
- name: 'Super Mario Project',
- nameWithNamespace: 'Mushroom Kingdom / Super Mario Project',
- webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/super-mario-project',
-};
-
-export const project2 = {
- id: 'gid://gitlab/Group/59',
- issuesEnabled: false,
- name: 'Mario Kart Project',
- nameWithNamespace: 'Mushroom Kingdom / Mario Kart Project',
- webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-kart-project',
-};
-
-export const project3 = {
- id: 'gid://gitlab/Group/103',
- issuesEnabled: true,
- name: 'Mario Party Project',
- nameWithNamespace: 'Mushroom Kingdom / Mario Party Project',
- webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-party-project',
-};
-
-export const searchProjectsQueryResponse = {
- data: {
- group: {
- id: '1',
- projects: {
- nodes: [project1, project2, project3],
- },
- },
- },
-};
-
-export const emptySearchProjectsQueryResponse = {
- data: {
- group: {
- id: '1',
- projects: {
- nodes: [],
- },
- },
- },
-};
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 8413b8463c1..010c719bd84 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
@@ -2,6 +2,7 @@ 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';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import RelatedMergeRequests from '~/issues/related_merge_requests/components/related_merge_requests.vue';
import createStore from '~/issues/related_merge_requests/store/index';
import RelatedIssuableItem from '~/issuable/components/related_issuable_item.vue';
@@ -18,7 +19,7 @@ describe('RelatedMergeRequests', () => {
document.getElementById('js-issuable-app').dataset.initial = JSON.stringify(mockData);
mock = new MockAdapter(axios);
- mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(200, mockData, { 'x-total': 2 });
+ mock.onGet(`${API_ENDPOINT}?per_page=100`).reply(HTTP_STATUS_OK, mockData, { 'x-total': 2 });
wrapper = shallowMount(RelatedMergeRequests, {
store: createStore(),
diff --git a/spec/frontend/issues/related_merge_requests/store/actions_spec.js b/spec/frontend/issues/related_merge_requests/store/actions_spec.js
index d3ec6c3bc9d..7339372a8d1 100644
--- a/spec/frontend/issues/related_merge_requests/store/actions_spec.js
+++ b/spec/frontend/issues/related_merge_requests/store/actions_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/issues/related_merge_requests/store/actions';
import * as types from '~/issues/related_merge_requests/store/mutation_types';
@@ -72,7 +73,9 @@ describe('RelatedMergeRequest store actions', () => {
describe('for a successful request', () => {
it('should dispatch success action', () => {
const data = { a: 1 };
- mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(200, data, { 'x-total': 2 });
+ mock
+ .onGet(`${state.apiEndpoint}?per_page=100`)
+ .replyOnce(HTTP_STATUS_OK, data, { 'x-total': 2 });
return testAction(
actions.fetchMergeRequests,
@@ -86,7 +89,7 @@ describe('RelatedMergeRequest store actions', () => {
describe('for a failing request', () => {
it('should dispatch error action', async () => {
- mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(400);
+ mock.onGet(`${state.apiEndpoint}?per_page=100`).replyOnce(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.fetchMergeRequests,
diff --git a/spec/frontend/issues/show/components/app_spec.js b/spec/frontend/issues/show/components/app_spec.js
index 6cf44e60092..9fa0ce6f93d 100644
--- a/spec/frontend/issues/show/components/app_spec.js
+++ b/spec/frontend/issues/show/components/app_spec.js
@@ -6,7 +6,14 @@ import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import { IssuableStatus, IssuableStatusText, IssuableType } from '~/issues/constants';
+import {
+ IssuableStatusText,
+ STATUS_CLOSED,
+ STATUS_OPEN,
+ STATUS_REOPENED,
+ TYPE_EPIC,
+ TYPE_ISSUE,
+} from '~/issues/constants';
import IssuableApp from '~/issues/show/components/app.vue';
import DescriptionComponent from '~/issues/show/components/description.vue';
import EditedComponent from '~/issues/show/components/edited.vue';
@@ -17,6 +24,7 @@ import PinnedLinks from '~/issues/show/components/pinned_links.vue';
import { POLLING_DELAY } from '~/issues/show/constants';
import eventHub from '~/issues/show/event_hub';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { visitUrl } from '~/lib/utils/url_utility';
import {
appProps,
@@ -94,7 +102,7 @@ describe('Issuable output', () => {
mock
.onGet('/gitlab-org/gitlab-shell/-/issues/9/realtime_changes/realtime_changes')
.reply(() => {
- const res = Promise.resolve([200, REALTIME_REQUEST_STACK[realtimeRequestCount]]);
+ const res = Promise.resolve([HTTP_STATUS_OK, REALTIME_REQUEST_STACK[realtimeRequestCount]]);
realtimeRequestCount += 1;
return res;
});
@@ -330,7 +338,9 @@ describe('Issuable output', () => {
const mockData = {
test: [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }],
};
- mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
+ mock
+ .onGet('/issuable-templates-path')
+ .reply(() => Promise.resolve([HTTP_STATUS_OK, mockData]));
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
expect(formSpy).toHaveBeenCalledWith(mockData);
@@ -339,7 +349,9 @@ describe('Issuable output', () => {
it('shows the form if template names as array request is successful', () => {
const mockData = [{ name: 'test', id: 'test', project_path: '/', namespace_path: '/' }];
- mock.onGet('/issuable-templates-path').reply(() => Promise.resolve([200, mockData]));
+ mock
+ .onGet('/issuable-templates-path')
+ .reply(() => Promise.resolve([HTTP_STATUS_OK, mockData]));
return wrapper.vm.requestTemplatesAndShowForm().then(() => {
expect(formSpy).toHaveBeenCalledWith(mockData);
@@ -473,11 +485,11 @@ describe('Issuable output', () => {
});
it.each`
- issuableType | issuableStatus | statusIcon
- ${IssuableType.Issue} | ${IssuableStatus.Open} | ${'issues'}
- ${IssuableType.Issue} | ${IssuableStatus.Closed} | ${'issue-closed'}
- ${IssuableType.Epic} | ${IssuableStatus.Open} | ${'epic'}
- ${IssuableType.Epic} | ${IssuableStatus.Closed} | ${'epic-closed'}
+ issuableType | issuableStatus | statusIcon
+ ${TYPE_ISSUE} | ${STATUS_OPEN} | ${'issues'}
+ ${TYPE_ISSUE} | ${STATUS_CLOSED} | ${'issue-closed'}
+ ${TYPE_EPIC} | ${STATUS_OPEN} | ${'epic'}
+ ${TYPE_EPIC} | ${STATUS_CLOSED} | ${'epic-closed'}
`(
'shows with state icon "$statusIcon" for $issuableType when status is $issuableStatus',
async ({ issuableType, issuableStatus, statusIcon }) => {
@@ -491,9 +503,9 @@ describe('Issuable output', () => {
it.each`
title | state
- ${'shows with Open when status is opened'} | ${IssuableStatus.Open}
- ${'shows with Closed when status is closed'} | ${IssuableStatus.Closed}
- ${'shows with Open when status is reopened'} | ${IssuableStatus.Reopened}
+ ${'shows with Open when status is opened'} | ${STATUS_OPEN}
+ ${'shows with Closed when status is closed'} | ${STATUS_CLOSED}
+ ${'shows with Open when status is reopened'} | ${STATUS_REOPENED}
`('$title', async ({ state }) => {
wrapper.setProps({ issuableStatus: state });
@@ -645,10 +657,10 @@ describe('Issuable output', () => {
});
});
- describe('listItemReorder event', () => {
+ describe('saveDescription event', () => {
it('makes request to update issue', async () => {
const description = 'I have been updated!';
- findDescription().vm.$emit('listItemReorder', description);
+ findDescription().vm.$emit('saveDescription', description);
await waitForPromises();
expect(mock.history.put[0].data).toContain(description);
diff --git a/spec/frontend/issues/show/components/description_spec.js b/spec/frontend/issues/show/components/description_spec.js
index 889ff450825..3f4513e6bfa 100644
--- a/spec/frontend/issues/show/components/description_spec.js
+++ b/spec/frontend/issues/show/components/description_spec.js
@@ -1,8 +1,8 @@
import $ from 'jquery';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
-import { GlTooltip, GlModal } from '@gitlab/ui';
-
+import { GlModal } from '@gitlab/ui';
+import getIssueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
import { TEST_HOST } from 'helpers/test_constants';
@@ -10,23 +10,26 @@ import { mockTracking } from 'helpers/tracking_helper';
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 Description from '~/issues/show/components/description.vue';
+import eventHub from '~/issues/show/event_hub';
import { updateHistory } from '~/lib/utils/url_utility';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import createWorkItemMutation from '~/work_items/graphql/create_work_item.mutation.graphql';
import workItemTypesQuery from '~/work_items/graphql/project_work_item_types.query.graphql';
-import createWorkItemFromTaskMutation from '~/work_items/graphql/create_work_item_from_task.mutation.graphql';
import TaskList from '~/task_list';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
import {
+ createWorkItemMutationErrorResponse,
+ createWorkItemMutationResponse,
+ getIssueDetailsResponse,
projectWorkItemTypesQueryResponse,
- createWorkItemFromTaskMutationResponse,
} from 'jest/work_items/mock_data';
import {
descriptionProps as initialProps,
+ descriptionHtmlWithList,
descriptionHtmlWithCheckboxes,
descriptionHtmlWithTask,
} from '../mock_data/mock_data';
@@ -39,6 +42,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
jest.mock('~/task_list');
jest.mock('~/behaviors/markdown/render_gfm');
+const mockSpriteIcons = '/icons.svg';
const showModal = jest.fn();
const hideModal = jest.fn();
const showDetailsModal = jest.fn();
@@ -46,6 +50,7 @@ const $toast = {
show: jest.fn(),
};
+const issueDetailsResponse = getIssueDetailsResponse();
const workItemQueryResponse = {
data: {
workItem: null,
@@ -54,44 +59,45 @@ const workItemQueryResponse = {
const queryHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
const workItemTypesQueryHandler = jest.fn().mockResolvedValue(projectWorkItemTypesQueryResponse);
-const createWorkItemFromTaskSuccessHandler = jest
- .fn()
- .mockResolvedValue(createWorkItemFromTaskMutationResponse);
describe('Description component', () => {
let wrapper;
+ let originalGon;
Vue.use(VueApollo);
const findGfmContent = () => wrapper.find('[data-testid="gfm-content"]');
const findTextarea = () => wrapper.find('[data-testid="textarea"]');
- const findTaskActionButtons = () => wrapper.findAll('.js-add-task');
- const findConvertToTaskButton = () => wrapper.find('.js-add-task');
+ const findListItems = () => findGfmContent().findAll('ul > li');
+ const findTaskActionButtons = () => wrapper.findAll('.task-list-item-actions');
const findTaskLink = () => wrapper.find('a.gfm-issue');
-
- const findTooltips = () => wrapper.findAllComponents(GlTooltip);
const findModal = () => wrapper.findComponent(GlModal);
const findWorkItemDetailModal = () => wrapper.findComponent(WorkItemDetailModal);
function createComponent({
props = {},
provide,
- createWorkItemFromTaskHandler = createWorkItemFromTaskSuccessHandler,
+ issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse),
+ createWorkItemMutationHandler,
+ ...options
} = {}) {
wrapper = shallowMountExtended(Description, {
propsData: {
issueId: 1,
+ issueIid: 1,
...initialProps,
...props,
},
provide: {
fullPath: 'gitlab-org/gitlab-test',
+ hasIterationsFeature: true,
...provide,
},
apolloProvider: createMockApollo([
[workItemQuery, queryHandler],
[workItemTypesQuery, workItemTypesQueryHandler],
- [createWorkItemFromTaskMutation, createWorkItemFromTaskHandler],
+ [getIssueDetailsQuery, issueDetailsQueryHandler],
+ [createWorkItemMutation, createWorkItemMutationHandler],
]),
mocks: {
$toast,
@@ -109,10 +115,14 @@ describe('Description component', () => {
},
}),
},
+ ...options,
});
}
beforeEach(() => {
+ originalGon = window.gon;
+ window.gon = { sprite_icons: mockSpriteIcons };
+
setWindowLocation(TEST_HOST);
if (!document.querySelector('.issuable-meta')) {
@@ -125,11 +135,9 @@ describe('Description component', () => {
}
});
- afterEach(() => {
- wrapper.destroy();
- });
-
afterAll(() => {
+ window.gon = originalGon;
+
$('.issuable-meta .flash-container').remove();
});
@@ -271,7 +279,38 @@ describe('Description component', () => {
});
});
- describe('with work_items_create_from_markdown feature flag enabled', () => {
+ describe('with list', () => {
+ beforeEach(async () => {
+ createComponent({
+ props: {
+ descriptionHtml: descriptionHtmlWithList,
+ },
+ attachTo: document.body,
+ });
+ await nextTick();
+ });
+
+ it('shows list items', () => {
+ expect(findListItems()).toHaveLength(3);
+ });
+
+ it('shows list items drag icons', () => {
+ const dragIcon = findListItems().at(0).find('.drag-icon');
+
+ expect(dragIcon.classes()).toEqual(
+ expect.arrayContaining(['s14', 'gl-icon', 'gl-cursor-grab', 'gl-opacity-0']),
+ );
+ expect(dragIcon.attributes()).toMatchObject({
+ 'aria-hidden': 'true',
+ role: 'img',
+ });
+ expect(dragIcon.find('use').attributes()).toEqual({
+ href: `${mockSpriteIcons}#grip`,
+ });
+ });
+ });
+
+ describe('with work_items_mvc feature flag enabled', () => {
describe('empty description', () => {
beforeEach(() => {
createComponent({
@@ -280,7 +319,7 @@ describe('Description component', () => {
},
provide: {
glFeatures: {
- workItemsCreateFromMarkdown: true,
+ workItemsMvc: true,
},
},
});
@@ -300,7 +339,7 @@ describe('Description component', () => {
},
provide: {
glFeatures: {
- workItemsCreateFromMarkdown: true,
+ workItemsMvc: true,
},
},
});
@@ -311,13 +350,6 @@ describe('Description component', () => {
expect(findTaskActionButtons()).toHaveLength(3);
});
- it('renders a list of tooltips corresponding to checkboxes in description HTML', () => {
- expect(findTooltips()).toHaveLength(3);
- expect(findTooltips().at(0).props('target')).toBe(
- findTaskActionButtons().at(0).attributes('id'),
- );
- });
-
it('does not show a modal by default', () => {
expect(findModal().exists()).toBe(false);
});
@@ -331,50 +363,123 @@ describe('Description component', () => {
});
});
- describe('creating work item from checklist item', () => {
- it('emits `updateDescription` after creating new work item', async () => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithCheckboxes,
- },
- provide: {
- glFeatures: {
- workItemsCreateFromMarkdown: true,
- },
- },
- });
+ describe('task list item actions', () => {
+ describe('converting the task list item to a task', () => {
+ describe('when successful', () => {
+ let createWorkItemMutationHandler;
- const newDescription = `<p>New description</p>`;
+ beforeEach(async () => {
+ createWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(createWorkItemMutationResponse);
+ const descriptionText = `Tasks
- await findConvertToTaskButton().trigger('click');
+1. [ ] item 1
+ 1. [ ] item 2
- await waitForPromises();
+ paragraph text
- expect(wrapper.emitted('updateDescription')).toEqual([[newDescription]]);
- });
+ 1. [ ] item 3
+ 1. [ ] item 4;`;
+ createComponent({
+ props: { descriptionText },
+ provide: { glFeatures: { workItemsMvc: true } },
+ createWorkItemMutationHandler,
+ });
+ await waitForPromises();
- it('shows flash message when creating task fails', async () => {
- createComponent({
- props: {
- descriptionHtml: descriptionHtmlWithCheckboxes,
- },
- provide: {
- glFeatures: {
- workItemsCreateFromMarkdown: true,
- },
- },
- createWorkItemFromTaskHandler: jest.fn().mockRejectedValue({}),
+ eventHub.$emit('convert-task-list-item', '4:4-8:19');
+ await waitForPromises();
+ });
+
+ it('emits an event to update the description with the deleted task list item omitted', () => {
+ const newDescriptionText = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 3
+ 1. [ ] item 4;`;
+
+ expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
+ });
+
+ it('calls a mutation to create a task', () => {
+ const {
+ confidential,
+ iteration,
+ milestone,
+ } = issueDetailsResponse.data.workspace.issuable;
+ expect(createWorkItemMutationHandler).toHaveBeenCalledWith({
+ input: {
+ confidential,
+ description: '\nparagraph text\n',
+ hierarchyWidget: {
+ parentId: 'gid://gitlab/WorkItem/1',
+ },
+ iterationWidget: {
+ iterationId: IS_EE ? iteration.id : null,
+ },
+ milestoneWidget: {
+ milestoneId: milestone.id,
+ },
+ projectPath: 'gitlab-org/gitlab-test',
+ title: 'item 2',
+ workItemTypeId: 'gid://gitlab/WorkItems::Type/3',
+ },
+ });
+ });
+
+ it('shows a toast to confirm the creation of the task', () => {
+ expect($toast.show).toHaveBeenCalledWith('Converted to task', expect.any(Object));
+ });
});
- await findConvertToTaskButton().trigger('click');
+ describe('when unsuccessful', () => {
+ beforeEach(async () => {
+ createComponent({
+ props: { descriptionText: 'description' },
+ provide: { glFeatures: { workItemsMvc: true } },
+ createWorkItemMutationHandler: jest
+ .fn()
+ .mockResolvedValue(createWorkItemMutationErrorResponse),
+ });
+ await waitForPromises();
- await waitForPromises();
+ eventHub.$emit('convert-task-list-item', '1:1-1:11');
+ await waitForPromises();
+ });
- expect(createAlert).toHaveBeenCalledWith(
- expect.objectContaining({
- message: 'Something went wrong when creating task. Please try again.',
- }),
- );
+ it('shows an alert with an error message', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'Something went wrong when creating task. Please try again.',
+ error: new Error('an error'),
+ captureError: true,
+ });
+ });
+ });
+ });
+
+ describe('deleting the task list item', () => {
+ it('emits an event to update the description with the deleted task list item', () => {
+ const descriptionText = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4;`;
+ const newDescriptionText = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 3
+ 1. [ ] item 4;`;
+ createComponent({
+ props: { descriptionText },
+ provide: { glFeatures: { workItemsMvc: true } },
+ });
+
+ eventHub.$emit('delete-task-list-item', '4:4-5:19');
+
+ expect(wrapper.emitted('saveDescription')).toEqual([[newDescriptionText]]);
+ });
});
});
@@ -386,7 +491,7 @@ describe('Description component', () => {
descriptionHtml: descriptionHtmlWithTask,
},
provide: {
- glFeatures: { workItemsCreateFromMarkdown: true },
+ glFeatures: { workItemsMvc: true },
},
});
return nextTick();
@@ -448,7 +553,7 @@ describe('Description component', () => {
createComponent({
props: { descriptionHtml: descriptionHtmlWithTask },
- provide: { glFeatures: { workItemsCreateFromMarkdown: true } },
+ provide: { glFeatures: { workItemsMvc: true } },
});
expect(showDetailsModal).toHaveBeenCalledTimes(modalOpened);
@@ -464,7 +569,7 @@ describe('Description component', () => {
descriptionHtml: descriptionHtmlWithTask,
},
provide: {
- glFeatures: { workItemsCreateFromMarkdown: true },
+ glFeatures: { workItemsMvc: true },
},
});
return nextTick();
diff --git a/spec/frontend/issues/show/components/fields/type_spec.js b/spec/frontend/issues/show/components/fields/type_spec.js
index 3333ceffca9..27ac0e1baf3 100644
--- a/spec/frontend/issues/show/components/fields/type_spec.js
+++ b/spec/frontend/issues/show/components/fields/type_spec.js
@@ -1,5 +1,5 @@
-import { GlFormGroup, GlDropdown, GlDropdownItem, GlIcon } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
+import { GlFormGroup, GlListbox, GlIcon } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -32,17 +32,16 @@ describe('Issue type field component', () => {
},
};
- const findTypeFromGroup = () => wrapper.findComponent(GlFormGroup);
- const findTypeFromDropDown = () => wrapper.findComponent(GlDropdown);
- const findTypeFromDropDownItems = () => wrapper.findAllComponents(GlDropdownItem);
- const findTypeFromDropDownItemAt = (at) => findTypeFromDropDownItems().at(at);
- const findTypeFromDropDownItemIconAt = (at) =>
- findTypeFromDropDownItems().at(at).findComponent(GlIcon);
+ const findListBox = () => wrapper.findComponent(GlListbox);
+ const findFormGroup = () => wrapper.findComponent(GlFormGroup);
+ const findAllIssueItems = () => wrapper.findAll('[data-testid="issue-type-list-item"]');
+ const findIssueItemAt = (at) => findAllIssueItems().at(at);
+ const findIssueItemAtIcon = (at) => findAllIssueItems().at(at).findComponent(GlIcon);
- const createComponent = ({ data } = {}, provide) => {
+ const createComponent = (mountFn = mount, { data } = {}, provide) => {
fakeApollo = createMockApollo([], mockResolvers);
- wrapper = shallowMount(IssueTypeField, {
+ wrapper = mountFn(IssueTypeField, {
apolloProvider: fakeApollo,
data() {
return {
@@ -59,7 +58,6 @@ describe('Issue type field component', () => {
beforeEach(() => {
mockIssueStateData = jest.fn();
- createComponent();
});
afterEach(() => {
@@ -71,48 +69,60 @@ describe('Issue type field component', () => {
${0} | ${issuableTypes[0].text} | ${issuableTypes[0].icon}
${1} | ${issuableTypes[1].text} | ${issuableTypes[1].icon}
`(`renders the issue type $text with an icon in the dropdown`, ({ at, text, icon }) => {
- expect(findTypeFromDropDownItemIconAt(at).attributes('name')).toBe(icon);
- expect(findTypeFromDropDownItemAt(at).text()).toBe(text);
+ createComponent();
+
+ expect(findIssueItemAtIcon(at).props('name')).toBe(icon);
+ expect(findIssueItemAt(at).text()).toBe(text);
});
it('renders a form group with the correct label', () => {
- expect(findTypeFromGroup().attributes('label')).toBe(i18n.label);
+ createComponent(shallowMount);
+
+ expect(findFormGroup().attributes('label')).toBe(i18n.label);
});
it('renders a form select with the `issue_type` value', () => {
- expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
+ createComponent();
+
+ expect(findListBox().attributes('value')).toBe(issuableTypes.issue);
});
describe('with Apollo cache mock', () => {
it('renders the selected issueType', async () => {
+ createComponent();
+
mockIssueStateData.mockResolvedValue(getIssueStateQueryResponse);
await waitForPromises();
- expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
+ expect(findListBox().attributes('value')).toBe(issuableTypes.issue);
});
it('updates the `issue_type` in the apollo cache when the value is changed', async () => {
- findTypeFromDropDownItems().at(1).vm.$emit('click', issuableTypes.incident);
+ createComponent();
+
+ wrapper.vm.$emit('select', issuableTypes.incident);
await nextTick();
- expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.incident);
+ expect(findListBox().attributes('value')).toBe(issuableTypes.incident);
});
describe('when user is a guest', () => {
it('hides the incident type from the dropdown', async () => {
- createComponent({}, { canCreateIncident: false, issueType: 'issue' });
+ createComponent(mount, {}, { canCreateIncident: false, issueType: 'issue' });
+
await waitForPromises();
- expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
- expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(false);
- expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.issue);
+ expect(findIssueItemAt(0).isVisible()).toBe(true);
+ expect(findIssueItemAt(1).isVisible()).toBe(false);
+ expect(findListBox().attributes('value')).toBe(issuableTypes.issue);
});
it('and incident is selected, includes incident in the dropdown', async () => {
- createComponent({}, { canCreateIncident: false, issueType: 'incident' });
+ createComponent(mount, {}, { canCreateIncident: false, issueType: 'incident' });
+
await waitForPromises();
- expect(findTypeFromDropDownItemAt(0).isVisible()).toBe(true);
- expect(findTypeFromDropDownItemAt(1).isVisible()).toBe(true);
- expect(findTypeFromDropDown().attributes('value')).toBe(issuableTypes.incident);
+ expect(findIssueItemAt(0).isVisible()).toBe(true);
+ expect(findIssueItemAt(1).isVisible()).toBe(true);
+ expect(findListBox().attributes('value')).toBe(issuableTypes.incident);
});
});
});
diff --git a/spec/frontend/issues/show/components/header_actions_spec.js b/spec/frontend/issues/show/components/header_actions_spec.js
index aaf228ae181..3d9dad3a721 100644
--- a/spec/frontend/issues/show/components/header_actions_spec.js
+++ b/spec/frontend/issues/show/components/header_actions_spec.js
@@ -4,7 +4,7 @@ import { shallowMount } from '@vue/test-utils';
import Vuex from 'vuex';
import { mockTracking } from 'helpers/tracking_helper';
import { createAlert, VARIANT_SUCCESS } from '~/flash';
-import { IssuableStatus, IssueType } from '~/issues/constants';
+import { IssueType, STATUS_CLOSED, STATUS_OPEN } 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';
@@ -40,7 +40,7 @@ describe('HeaderActions component', () => {
newIssuePath: 'gitlab-org/gitlab-test/-/issues/new',
projectPath: 'gitlab-org/gitlab-test',
reportAbusePath: '-/abuse_reports/add_category',
- reportedUserId: '1',
+ reportedUserId: 1,
reportedFromUrl: 'http://localhost:/gitlab-org/-/issues/32',
submitAsSpamPath: 'gitlab-org/gitlab-test/-/issues/32/submit_as_spam',
};
@@ -81,7 +81,7 @@ describe('HeaderActions component', () => {
const mountComponent = ({
props = {},
- issueState = IssuableStatus.Open,
+ issueState = STATUS_OPEN,
blockedByIssues = [],
mutateResponse = {},
} = {}) => {
@@ -123,9 +123,9 @@ describe('HeaderActions component', () => {
`('when issue type is $issueType', ({ issueType }) => {
describe('close/reopen button', () => {
describe.each`
- description | issueState | buttonText | newIssueState
- ${`when the ${issueType} is open`} | ${IssuableStatus.Open} | ${`Close ${issueType}`} | ${ISSUE_STATE_EVENT_CLOSE}
- ${`when the ${issueType} is closed`} | ${IssuableStatus.Closed} | ${`Reopen ${issueType}`} | ${ISSUE_STATE_EVENT_REOPEN}
+ description | issueState | buttonText | newIssueState
+ ${`when the ${issueType} is open`} | ${STATUS_OPEN} | ${`Close ${issueType}`} | ${ISSUE_STATE_EVENT_CLOSE}
+ ${`when the ${issueType} is closed`} | ${STATUS_CLOSED} | ${`Reopen ${issueType}`} | ${ISSUE_STATE_EVENT_REOPEN}
`('$description', ({ issueState, buttonText, newIssueState }) => {
beforeEach(() => {
dispatchEventSpy = jest.spyOn(document, 'dispatchEvent');
@@ -411,9 +411,8 @@ describe('HeaderActions component', () => {
wrapper = mountComponent({ props: { isIssueAuthor: false } });
});
- it('renders', () => {
- expect(findAbuseCategorySelector().exists()).toBe(true);
- expect(findAbuseCategorySelector().props('showDrawer')).toEqual(false);
+ it("doesn't render", async () => {
+ expect(findAbuseCategorySelector().exists()).toEqual(false);
});
it('opens the drawer', async () => {
@@ -425,9 +424,10 @@ describe('HeaderActions component', () => {
});
it('closes the drawer', async () => {
+ await findDesktopDropdownItems().at(2).vm.$emit('click');
await findAbuseCategorySelector().vm.$emit('close-drawer');
- expect(findAbuseCategorySelector().props('showDrawer')).toEqual(false);
+ expect(findAbuseCategorySelector().exists()).toEqual(false);
});
});
});
diff --git a/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js b/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js
index 81c3c30bf8a..9159b742106 100644
--- a/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js
+++ b/spec/frontend/issues/show/components/incidents/edit_timeline_event_spec.js
@@ -22,7 +22,11 @@ describe('Edit Timeline events', () => {
const findTimelineEventsForm = () => wrapper.findComponent(TimelineEventsForm);
- const mockSaveData = { ...fakeEventData, ...mockInputData };
+ const mockSaveData = {
+ ...fakeEventData,
+ ...mockInputData,
+ timelineEventTags: ['Start time', 'End time'],
+ };
describe('editTimelineEvent', () => {
const saveEventEvent = { 'handle-save-edit': [[mockSaveData, false]] };
diff --git a/spec/frontend/issues/show/components/incidents/mock_data.js b/spec/frontend/issues/show/components/incidents/mock_data.js
index 6606bed1567..f6951864344 100644
--- a/spec/frontend/issues/show/components/incidents/mock_data.js
+++ b/spec/frontend/issues/show/components/incidents/mock_data.js
@@ -1,3 +1,16 @@
+export const mockTimelineEventTags = {
+ nodes: [
+ {
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
+ name: 'Start time',
+ },
+ {
+ id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
+ name: 'End time',
+ },
+ ],
+};
+
export const mockEvents = [
{
action: 'comment',
@@ -32,18 +45,7 @@ export const mockEvents = [
noteHtml: '<p>Dummy event 2</p>',
occurredAt: '2022-03-23T14:57:00Z',
updatedAt: '2022-03-23T14:57:08Z',
- timelineEventTags: {
- nodes: [
- {
- id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
- name: 'Start time',
- },
- {
- id: 'gid://gitlab/IncidentManagement::TimelineEvent/132',
- name: 'End time',
- },
- ],
- },
+ timelineEventTags: mockTimelineEventTags,
__typename: 'TimelineEventType',
},
{
@@ -187,5 +189,12 @@ export const mockInputData = {
occurredAt: '2020-08-10T02:30:00.000Z',
};
-const { id, note, occurredAt } = mockEvents[0];
-export const fakeEventData = { id, note, occurredAt };
+const { id, note, occurredAt, timelineEventTags } = mockEvents[0];
+export const fakeEventData = { id, note, occurredAt, timelineEventTags };
+export const fakeEventSaveData = {
+ id,
+ note,
+ occurredAt,
+ timelineEventTagNames: timelineEventTags,
+ ...mockInputData,
+};
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 f06d968a4c5..e352f9708e4 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,6 +1,6 @@
import VueApollo from 'vue-apollo';
import Vue, { nextTick } from 'vue';
-import { GlDatepicker, GlListbox } from '@gitlab/ui';
+import { GlDatepicker, GlCollapsibleListbox } 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';
@@ -62,7 +62,7 @@ describe('Timeline events form', () => {
const findDatePicker = () => wrapper.findComponent(GlDatepicker);
const findHourInput = () => wrapper.findByTestId('input-hours');
const findMinuteInput = () => wrapper.findByTestId('input-minutes');
- const findTagDropdown = () => wrapper.findComponent(GlListbox);
+ const findTagsListbox = () => wrapper.findComponent(GlCollapsibleListbox);
const findTextarea = () => wrapper.findByTestId('input-note');
const findTextareaValue = () => findTextarea().element.value;
const findCountNumeric = (count) => wrapper.findByText(count);
@@ -75,7 +75,7 @@ describe('Timeline events form', () => {
findMinuteInput().setValue(45);
};
const selectTags = async (tags) => {
- findTagDropdown().vm.$emit(
+ findTagsListbox().vm.$emit(
'select',
tags.map((x) => x.value),
);
@@ -125,31 +125,31 @@ describe('Timeline events form', () => {
);
});
- describe('event tag dropdown', () => {
+ describe('event tags listbox', () => {
it('should render option list from provided array', () => {
- expect(findTagDropdown().props('items')).toEqual(mockTags);
+ expect(findTagsListbox().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));
+ expect(findTagsListbox().props('selected')).toEqual(mockTags.map((x) => x.value));
});
it('should show default option, when none is chosen', () => {
- expect(findTagDropdown().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ expect(findTagsListbox().props('toggleText')).toBe(timelineFormI18n.selectTags);
});
it('should show the tag, when one is selected', async () => {
await selectOneTag();
- expect(findTagDropdown().props('toggleText')).toBe(timelineEventTagsI18n.startTime);
+ expect(findTagsListbox().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');
+ expect(findTagsListbox().props('toggleText')).toBe(`${mockTags.length} tags`);
});
it('should be cleared when clear is triggered', async () => {
@@ -159,8 +159,8 @@ describe('Timeline events form', () => {
wrapper.vm.clear();
await nextTick();
- expect(findTagDropdown().props('toggleText')).toBe(timelineFormI18n.selectTags);
- expect(findTagDropdown().props('selected')).toEqual([]);
+ expect(findTagsListbox().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ expect(findTagsListbox().props('selected')).toEqual([]);
});
it('should populate incident note with tags if a note was empty', async () => {
@@ -190,6 +190,33 @@ describe('Timeline events form', () => {
expect(findTextareaValue()).toBe('hello');
});
});
+
+ describe('form button behaviour', () => {
+ it('should enable the save buttons when event does not include tags', async () => {
+ await findTextarea().setValue('hello');
+
+ expect(findTagsListbox().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ expect(findSubmitButton().props('disabled')).toBe(false);
+ expect(findSubmitAndAddButton().props('disabled')).toBe(false);
+ });
+
+ it('should clear the form', async () => {
+ setDatetime();
+ await nextTick();
+
+ expect(findDatePicker().props('value')).toBe(mockInputDate);
+ expect(findHourInput().element.value).toBe('5');
+ expect(findMinuteInput().element.value).toBe('45');
+
+ wrapper.vm.clear();
+ await nextTick();
+
+ expect(findDatePicker().props('value')).toStrictEqual(new Date(fakeDate));
+ expect(findHourInput().element.value).toBe('0');
+ expect(findMinuteInput().element.value).toBe('0');
+ expect(findTagsListbox().props('toggleText')).toBe(timelineFormI18n.selectTags);
+ });
+ });
});
describe('form button behaviour', () => {
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
index ba0527e5395..24653a23036 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_item_spec.js
@@ -9,8 +9,8 @@ import { mockEvents } from './mock_data';
describe('IncidentTimelineEventList', () => {
let wrapper;
- const mountComponent = ({ propsData, provide } = {}) => {
- const { action, noteHtml, occurredAt } = mockEvents[0];
+ const mountComponent = ({ propsData, provide, mockEvent = mockEvents[0] } = {}) => {
+ const { action, noteHtml, occurredAt } = mockEvent;
wrapper = mountExtended(IncidentTimelineEventItem, {
propsData: {
action,
@@ -27,9 +27,10 @@ describe('IncidentTimelineEventList', () => {
const findCommentIcon = () => wrapper.findComponent(GlIcon);
const findEventTime = () => wrapper.findByTestId('event-time');
- const findEventTag = () => wrapper.findComponent(GlBadge);
+ const findEventTags = () => wrapper.findAllComponents(GlBadge);
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findDeleteButton = () => wrapper.findByText(timelineItemI18n.delete);
+ const findEditButton = () => wrapper.findByText(timelineItemI18n.edit);
describe('template', () => {
beforeEach(() => {
@@ -69,15 +70,16 @@ describe('IncidentTimelineEventList', () => {
});
});
- describe('timeline event tag', () => {
- it('does not show when tag is not provided', () => {
- expect(findEventTag().exists()).toBe(false);
- });
-
- it('shows when tag is provided', () => {
- mountComponent({ propsData: { eventTag: 'Start time' } });
+ describe.each([
+ { eventTags: [], expected: 0 },
+ { eventTags: ['Start time'], expected: 1 },
+ { eventTags: ['Start time', 'End time'], expected: 2 },
+ ])('timeline event tags', ({ eventTags, expected }) => {
+ it(`shows ${expected} badges when ${expected} tags are provided`, () => {
+ mountComponent({ propsData: { eventTags } });
- expect(findEventTag().exists()).toBe(true);
+ expect(findEventTags().exists()).toBe(Boolean(expected));
+ expect(findEventTags().length).toBe(eventTags.length);
});
});
@@ -87,6 +89,21 @@ describe('IncidentTimelineEventList', () => {
expect(findDeleteButton().exists()).toBe(false);
});
+ it('does not show edit item when event was system generated', () => {
+ const systemGeneratedMockEvent = {
+ ...mockEvents[0],
+ action: 'status',
+ };
+
+ mountComponent({
+ provide: { canUpdateTimelineEvent: true },
+ mockEvent: systemGeneratedMockEvent,
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ expect(findEditButton().exists()).toBe(false);
+ });
+
it('shows dropdown and delete item when user has update permission', () => {
mountComponent({ provide: { canUpdateTimelineEvent: true } });
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
index a7250e8ad0d..26fda877089 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_list_spec.js
@@ -20,6 +20,7 @@ import {
timelineEventsEditEventError,
fakeDate,
fakeEventData,
+ fakeEventSaveData,
mockInputData,
} from './mock_data';
@@ -92,9 +93,7 @@ describe('IncidentTimelineEventList', () => {
expect(findItems().at(1).props('occurredAt')).toBe(mockEvents[1].occurredAt);
expect(findItems().at(1).props('action')).toBe(mockEvents[1].action);
expect(findItems().at(1).props('noteHtml')).toBe(mockEvents[1].noteHtml);
- expect(findItems().at(1).props('eventTag')).toBe(
- mockEvents[1].timelineEventTags.nodes[0].name,
- );
+ expect(findItems().at(1).props('eventTags')).toBe(mockEvents[1].timelineEventTags.nodes);
});
it('formats dates correctly', () => {
@@ -123,20 +122,6 @@ describe('IncidentTimelineEventList', () => {
});
});
- describe('getFirstTag', () => {
- it('returns undefined, when timelineEventTags contains an empty array', () => {
- const returnedTag = wrapper.vm.getFirstTag(mockEvents[0].timelineEventTags);
-
- expect(returnedTag).toEqual(undefined);
- });
-
- it('returns the first string, when timelineEventTags contains array with at least one tag', () => {
- const returnedTag = wrapper.vm.getFirstTag(mockEvents[1].timelineEventTags);
-
- expect(returnedTag).toBe(mockEvents[1].timelineEventTags.nodes[0].name);
- });
- });
-
describe('delete functionality', () => {
beforeEach(() => {
mockConfirmAction({ confirmed: true });
@@ -183,20 +168,20 @@ describe('IncidentTimelineEventList', () => {
});
const findEditEvent = () => wrapper.findComponent(EditTimelineEvent);
- const mockSaveData = { ...fakeEventData, ...mockInputData };
+ const mockHandleSaveEventData = { ...fakeEventData, ...mockInputData };
describe('editTimelineEvent', () => {
it('should call the mutation with the right variables', async () => {
- await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await findEditEvent().vm.$emit('handle-save-edit', mockHandleSaveEventData);
await waitForPromises();
expect(editResponseSpy).toHaveBeenCalledWith({
- input: mockSaveData,
+ input: fakeEventSaveData,
});
});
it('should close the form on successful addition', async () => {
- await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await findEditEvent().vm.$emit('handle-save-edit', fakeEventSaveData);
await waitForPromises();
expect(findEditEvent().exists()).toBe(false);
@@ -217,7 +202,7 @@ describe('IncidentTimelineEventList', () => {
};
editResponseSpy.mockResolvedValueOnce(timelineEventsEditEventError);
- await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await findEditEvent().vm.$emit('handle-save-edit', fakeEventSaveData);
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
@@ -231,7 +216,7 @@ describe('IncidentTimelineEventList', () => {
};
editResponseSpy.mockRejectedValueOnce();
- await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await findEditEvent().vm.$emit('handle-save-edit', fakeEventSaveData);
await waitForPromises();
expect(createAlert).toHaveBeenCalledWith(expectedAlertArgs);
@@ -240,7 +225,7 @@ describe('IncidentTimelineEventList', () => {
it('should keep the form open on failed addition', async () => {
editResponseSpy.mockResolvedValueOnce(timelineEventsEditEventError);
- await findEditEvent().vm.$emit('handle-save-edit', mockSaveData);
+ await findEditEvent().vm.$emit('handle-save-edit', fakeEventSaveData);
await waitForPromises();
expect(findEditEvent().exists()).toBe(true);
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
index 5bac1d6e7ad..63474070701 100644
--- a/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_tab_spec.js
@@ -112,7 +112,9 @@ describe('TimelineEventsTab', () => {
await waitForPromises();
expect(findEmptyState().exists()).toBe(false);
- expect(findTimelineEventsList().props('timelineEvents')).toHaveLength(3);
+ expect(findTimelineEventsList().props('timelineEvents')).toHaveLength(
+ timelineEventsQueryListResponse.data.project.incidentManagementTimelineEvents.nodes.length,
+ );
});
});
diff --git a/spec/frontend/issues/show/components/incidents/timeline_events_tags_popover_spec.js b/spec/frontend/issues/show/components/incidents/timeline_events_tags_popover_spec.js
new file mode 100644
index 00000000000..b39e96127c3
--- /dev/null
+++ b/spec/frontend/issues/show/components/incidents/timeline_events_tags_popover_spec.js
@@ -0,0 +1,48 @@
+import { nextTick } from 'vue';
+import { GlIcon, GlPopover, GlLink } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import TimelineEventsTagsPopover from '~/issues/show/components/incidents/timeline_events_tags_popover.vue';
+
+describe('TimelineEventsTagsPopover component', () => {
+ let wrapper;
+
+ const mountComponent = () => {
+ wrapper = shallowMount(TimelineEventsTagsPopover, {
+ stubs: {
+ GlPopover,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ const findQuestionIcon = () => wrapper.findComponent(GlIcon);
+ const findPopover = () => wrapper.findComponent(GlPopover);
+ const findDocumentationLink = () => findPopover().findComponent(GlLink);
+
+ describe('question icon', () => {
+ it('should open a popover with a link when hovered', async () => {
+ findQuestionIcon().vm.$emit('hover');
+ await nextTick();
+
+ expect(findPopover().exists()).toBe(true);
+ expect(findDocumentationLink().exists()).toBe(true);
+ });
+ });
+
+ describe('documentation link', () => {
+ it('redirects to a correct documentation page', async () => {
+ findQuestionIcon().vm.$emit('hover');
+ await nextTick();
+
+ expect(findDocumentationLink().attributes('href')).toBe(
+ helpPagePath('/ee/operations/incident_management/incident_timeline_events', {
+ anchor: 'incident-tags',
+ }),
+ );
+ });
+ });
+});
diff --git a/spec/frontend/issues/show/components/incidents/utils_spec.js b/spec/frontend/issues/show/components/incidents/utils_spec.js
index f0494591e95..75be17f9889 100644
--- a/spec/frontend/issues/show/components/incidents/utils_spec.js
+++ b/spec/frontend/issues/show/components/incidents/utils_spec.js
@@ -3,8 +3,10 @@ import {
displayAndLogError,
getEventIcon,
getUtcShiftedDate,
+ getPreviousEventTags,
} from '~/issues/show/components/incidents/utils';
import { createAlert } from '~/flash';
+import { mockTimelineEventTags } from './mock_data';
jest.mock('~/flash');
@@ -51,4 +53,20 @@ describe('incident utils', () => {
expect(shiftedDate > date).toBe(true);
});
});
+
+ describe('getPreviousEventTags', () => {
+ it('should return an empty array, when passed object contains no tags', () => {
+ const nodes = [];
+ const previousTags = getPreviousEventTags(nodes);
+
+ expect(previousTags.length).toBe(0);
+ });
+
+ it('should return an array of strings, when passed object containing tags', () => {
+ const previousTags = getPreviousEventTags(mockTimelineEventTags.nodes);
+ expect(previousTags.length).toBe(2);
+ expect(previousTags).toContain(mockTimelineEventTags.nodes[0].name);
+ expect(previousTags).toContain(mockTimelineEventTags.nodes[1].name);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/locked_warning_spec.js b/spec/frontend/issues/show/components/locked_warning_spec.js
index 08f0338d41b..dd3c7c58380 100644
--- a/spec/frontend/issues/show/components/locked_warning_spec.js
+++ b/spec/frontend/issues/show/components/locked_warning_spec.js
@@ -1,7 +1,7 @@
import { GlAlert, GlLink } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import { sprintf } from '~/locale';
-import { IssuableType } from '~/issues/constants';
+import { TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import LockedWarning, { i18n } from '~/issues/show/components/locked_warning.vue';
describe('LockedWarning component', () => {
@@ -21,35 +21,32 @@ describe('LockedWarning component', () => {
const findAlert = () => wrapper.findComponent(GlAlert);
const findLink = () => wrapper.findComponent(GlLink);
- describe.each([IssuableType.Issue, IssuableType.Epic])(
- 'with issuableType set to %s',
- (issuableType) => {
- let alert;
- let link;
- beforeEach(() => {
- createComponent({ issuableType });
- alert = findAlert();
- link = findLink();
- });
-
- afterEach(() => {
- alert = null;
- link = null;
- });
-
- it('displays a non-closable alert', () => {
- expect(alert.exists()).toBe(true);
- expect(alert.props('dismissible')).toBe(false);
- });
-
- it(`displays correct message`, async () => {
- expect(alert.text()).toMatchInterpolatedText(sprintf(i18n.alertMessage, { issuableType }));
- });
-
- it(`displays a link with correct text`, async () => {
- expect(link.exists()).toBe(true);
- expect(link.text()).toBe(`the ${issuableType}`);
- });
- },
- );
+ describe.each([TYPE_ISSUE, TYPE_EPIC])('with issuableType set to %s', (issuableType) => {
+ let alert;
+ let link;
+ beforeEach(() => {
+ createComponent({ issuableType });
+ alert = findAlert();
+ link = findLink();
+ });
+
+ afterEach(() => {
+ alert = null;
+ link = null;
+ });
+
+ it('displays a non-closable alert', () => {
+ expect(alert.exists()).toBe(true);
+ expect(alert.props('dismissible')).toBe(false);
+ });
+
+ it(`displays correct message`, async () => {
+ expect(alert.text()).toMatchInterpolatedText(sprintf(i18n.alertMessage, { issuableType }));
+ });
+
+ it(`displays a link with correct text`, async () => {
+ expect(link.exists()).toBe(true);
+ expect(link.text()).toBe(`the ${issuableType}`);
+ });
+ });
});
diff --git a/spec/frontend/issues/show/components/task_list_item_actions_spec.js b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
new file mode 100644
index 00000000000..d52f9d57453
--- /dev/null
+++ b/spec/frontend/issues/show/components/task_list_item_actions_spec.js
@@ -0,0 +1,54 @@
+import { GlDropdown, GlDropdownItem } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import TaskListItemActions from '~/issues/show/components/task_list_item_actions.vue';
+import eventHub from '~/issues/show/event_hub';
+
+describe('TaskListItemActions component', () => {
+ let wrapper;
+
+ const findGlDropdown = () => wrapper.findComponent(GlDropdown);
+ const findConvertToTaskItem = () => wrapper.findAllComponents(GlDropdownItem).at(0);
+ const findDeleteItem = () => wrapper.findAllComponents(GlDropdownItem).at(1);
+
+ const mountComponent = () => {
+ const li = document.createElement('li');
+ li.dataset.sourcepos = '3:1-3:10';
+ li.appendChild(document.createElement('div'));
+ document.body.appendChild(li);
+
+ wrapper = shallowMount(TaskListItemActions, {
+ provide: { canUpdate: true, toggleClass: 'task-list-item-actions' },
+ attachTo: document.querySelector('div'),
+ });
+ };
+
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('renders dropdown', () => {
+ expect(findGlDropdown().props()).toMatchObject({
+ category: 'tertiary',
+ icon: 'ellipsis_v',
+ right: true,
+ text: TaskListItemActions.i18n.taskActions,
+ textSrOnly: true,
+ });
+ });
+
+ it('emits event when `Convert to task` dropdown item is clicked', () => {
+ jest.spyOn(eventHub, '$emit');
+
+ findConvertToTaskItem().vm.$emit('click');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('convert-task-list-item', '3:1-3:10');
+ });
+
+ it('emits event when `Delete` dropdown item is clicked', () => {
+ jest.spyOn(eventHub, '$emit');
+
+ findDeleteItem().vm.$emit('click');
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('delete-task-list-item', '3:1-3:10');
+ });
+});
diff --git a/spec/frontend/issues/show/issue_spec.js b/spec/frontend/issues/show/issue_spec.js
index 68c2e3768c7..2980a6c33ee 100644
--- a/spec/frontend/issues/show/issue_spec.js
+++ b/spec/frontend/issues/show/issue_spec.js
@@ -3,11 +3,12 @@ import waitForPromises from 'helpers/wait_for_promises';
import { initIssueApp } from '~/issues/show';
import * as parseData from '~/issues/show/utils/parse_data';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import createStore from '~/notes/stores';
import { appProps } from './mock_data/mock_data';
const mock = new MockAdapter(axios);
-mock.onGet().reply(200);
+mock.onGet().reply(HTTP_STATUS_OK);
jest.mock('~/lib/utils/poll');
@@ -18,7 +19,9 @@ const setupHTML = (initialData) => {
describe('Issue show index', () => {
describe('initIssueApp', () => {
- it('should initialize app with no potential XSS attack', async () => {
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/390368
+ // eslint-disable-next-line jest/no-disabled-tests
+ it.skip('should initialize app with no potential XSS attack', async () => {
const alertSpy = jest.spyOn(window, 'alert').mockImplementation(() => {});
const parseDataSpy = jest.spyOn(parseData, 'parseIssuableData');
diff --git a/spec/frontend/issues/show/mock_data/mock_data.js b/spec/frontend/issues/show/mock_data/mock_data.js
index 909789b7a0f..9f0b6fb1148 100644
--- a/spec/frontend/issues/show/mock_data/mock_data.js
+++ b/spec/frontend/issues/show/mock_data/mock_data.js
@@ -59,6 +59,14 @@ export const appProps = {
publishedIncidentUrl,
};
+export const descriptionHtmlWithList = `
+ <ul data-sourcepos="1:1-3:8" dir="auto">
+ <li data-sourcepos="1:1-1:8">todo 1</li>
+ <li data-sourcepos="2:1-2:8">todo 2</li>
+ <li data-sourcepos="3:1-3:8">todo 3</li>
+ </ul>
+`;
+
export const descriptionHtmlWithCheckboxes = `
<ul dir="auto" class="task-list" data-sourcepos"3:1-5:12">
<li class="task-list-item" data-sourcepos="3:1-3:11">
diff --git a/spec/frontend/issues/show/utils_spec.js b/spec/frontend/issues/show/utils_spec.js
index 603fb5cc2a6..e5041dd559b 100644
--- a/spec/frontend/issues/show/utils_spec.js
+++ b/spec/frontend/issues/show/utils_spec.js
@@ -1,4 +1,8 @@
-import { convertDescriptionWithNewSort } from '~/issues/show/utils';
+import {
+ deleteTaskListItem,
+ convertDescriptionWithNewSort,
+ extractTaskTitleAndDescription,
+} from '~/issues/show/utils';
describe('app/assets/javascripts/issues/show/utils.js', () => {
describe('convertDescriptionWithNewSort', () => {
@@ -137,4 +141,270 @@ describe('app/assets/javascripts/issues/show/utils.js', () => {
expect(convertDescriptionWithNewSort(description, list.firstChild)).toBe(expected);
});
});
+
+ describe('deleteTaskListItem', () => {
+ const description = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6
+
+ paragraph text
+
+ 1. [ ] item 7
+
+ paragraph text
+
+ 1. [ ] item 8
+
+ paragraph text
+
+ 1. [ ] item 9
+ 1. [ ] item 10`;
+
+ /* The equivalent HTML for the above markdown
+ <ol data-sourcepos="3:1-21:17">
+ <li data-sourcepos="3:1-21:17">item 1
+ <ol data-sourcepos="4:4-21:17">
+ <li data-sourcepos="4:4-4:16">
+ <p data-sourcepos="4:7-4:16">item 2</p>
+ </li>
+ <li data-sourcepos="5:4-7:19">
+ <p data-sourcepos="5:7-5:16">item 3</p>
+ <ol data-sourcepos="6:7-7:19">
+ <li data-sourcepos="6:7-6:19">item 4</li>
+ <li data-sourcepos="7:7-7:19">item 5</li>
+ </ol>
+ </li>
+ <li data-sourcepos="8:4-11:0">
+ <p data-sourcepos="8:7-8:16">item 6</p>
+ <p data-sourcepos="10:7-10:20">paragraph text</p>
+ </li>
+ <li data-sourcepos="12:4-20:19">
+ <p data-sourcepos="12:7-12:16">item 7</p>
+ <p data-sourcepos="14:7-14:20">paragraph text</p>
+ <ol data-sourcepos="16:7-20:19">
+ <li data-sourcepos="16:7-19:0">
+ <p data-sourcepos="16:10-16:19">item 8</p>
+ <p data-sourcepos="18:10-18:23">paragraph text</p>
+ </li>
+ <li data-sourcepos="20:7-20:19">
+ <p data-sourcepos="20:10-20:19">item 9</p>
+ </li>
+ </ol>
+ </li>
+ <li data-sourcepos="21:4-21:17">
+ <p data-sourcepos="21:7-21:17">item 10</p>
+ </li>
+ </ol>
+ </li>
+ </ol>
+ */
+
+ it('deletes item with no children', () => {
+ const sourcepos = '4:4-4:14';
+ const newDescription = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6
+
+ paragraph text
+
+ 1. [ ] item 7
+
+ paragraph text
+
+ 1. [ ] item 8
+
+ paragraph text
+
+ 1. [ ] item 9
+ 1. [ ] item 10`;
+
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
+ newDescription,
+ taskTitle: 'item 2',
+ });
+ });
+
+ it('deletes deeply nested item with no children', () => {
+ const sourcepos = '6:7-6:19';
+ const newDescription = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 3
+ 1. [ ] item 5
+ 1. [ ] item 6
+
+ paragraph text
+
+ 1. [ ] item 7
+
+ paragraph text
+
+ 1. [ ] item 8
+
+ paragraph text
+
+ 1. [ ] item 9
+ 1. [ ] item 10`;
+
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
+ newDescription,
+ taskTitle: 'item 4',
+ });
+ });
+
+ it('deletes item with children and moves sub-tasks up a level', () => {
+ const sourcepos = '5:4-7:19';
+ const newDescription = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6
+
+ paragraph text
+
+ 1. [ ] item 7
+
+ paragraph text
+
+ 1. [ ] item 8
+
+ paragraph text
+
+ 1. [ ] item 9
+ 1. [ ] item 10`;
+
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
+ newDescription,
+ taskTitle: 'item 3',
+ });
+ });
+
+ it('deletes item with associated paragraph text', () => {
+ const sourcepos = '8:4-11:0';
+ const newDescription = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 7
+
+ paragraph text
+
+ 1. [ ] item 8
+
+ paragraph text
+
+ 1. [ ] item 9
+ 1. [ ] item 10`;
+ const taskDescription = `
+paragraph text
+`;
+
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
+ newDescription,
+ taskDescription,
+ taskTitle: 'item 6',
+ });
+ });
+
+ it('deletes item with associated paragraph text and moves sub-tasks up a level', () => {
+ const sourcepos = '12:4-20:19';
+ const newDescription = `Tasks
+
+1. [ ] item 1
+ 1. [ ] item 2
+ 1. [ ] item 3
+ 1. [ ] item 4
+ 1. [ ] item 5
+ 1. [ ] item 6
+
+ paragraph text
+
+ 1. [ ] item 8
+
+ paragraph text
+
+ 1. [ ] item 9
+ 1. [ ] item 10`;
+ const taskDescription = `
+paragraph text
+`;
+
+ expect(deleteTaskListItem(description, sourcepos)).toEqual({
+ newDescription,
+ taskDescription,
+ taskTitle: 'item 7',
+ });
+ });
+ });
+
+ describe('extractTaskTitleAndDescription', () => {
+ const description = `A multi-line
+description`;
+
+ describe('when title is pure code block', () => {
+ const title = '`code block`';
+
+ it('moves the title to the description', () => {
+ expect(extractTaskTitleAndDescription(title)).toEqual({
+ title: 'Untitled',
+ description: title,
+ });
+ });
+
+ it('moves the title to the description and appends the description to it', () => {
+ expect(extractTaskTitleAndDescription(title, description)).toEqual({
+ title: 'Untitled',
+ description: `${title}\n\n${description}`,
+ });
+ });
+ });
+
+ describe('when title is too long', () => {
+ const title =
+ 'Deleniti id facere numquam cum consectetur sint ipsum consequatur. Odit nihil harum consequuntur est nemo adipisci. Incidunt suscipit voluptatem et culpa at voluptatem consequuntur. Rerum aliquam earum quia consequatur ipsam quae ut. Quod molestias ducimus quia ratione nostrum ut adipisci.';
+ const expectedTitle =
+ 'Deleniti id facere numquam cum consectetur sint ipsum consequatur. Odit nihil harum consequuntur est nemo adipisci. Incidunt suscipit voluptatem et culpa at voluptatem consequuntur. Rerum aliquam earum quia consequatur ipsam quae ut. Quod molestias ducimu';
+
+ it('moves the title beyond the character limit to the description', () => {
+ expect(extractTaskTitleAndDescription(title)).toEqual({
+ title: expectedTitle,
+ description: 's quia ratione nostrum ut adipisci.',
+ });
+ });
+
+ it('moves the title beyond the character limit to the description and appends the description to it', () => {
+ expect(extractTaskTitleAndDescription(title, description)).toEqual({
+ title: expectedTitle,
+ description: `s quia ratione nostrum ut adipisci.\n\n${description}`,
+ });
+ });
+ });
+
+ describe('when title is fine', () => {
+ const title = 'A fine title';
+
+ it('uses the title with no modifications', () => {
+ expect(extractTaskTitleAndDescription(title)).toEqual({ title });
+ });
+
+ it('uses the title and description with no modifications', () => {
+ expect(extractTaskTitleAndDescription(title, description)).toEqual({ title, description });
+ });
+ });
+ });
});
diff --git a/spec/frontend/jira_connect/subscriptions/api_spec.js b/spec/frontend/jira_connect/subscriptions/api_spec.js
index 21636017f10..e2a14a9102f 100644
--- a/spec/frontend/jira_connect/subscriptions/api_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/api_spec.js
@@ -104,9 +104,11 @@ describe('JiraConnect API', () => {
response = await makeRequest();
expect(axiosInstance.get).toHaveBeenCalledWith(mockGroupsPath, {
+ headers: {},
params: {
page: mockPage,
per_page: mockPerPage,
+ search: undefined,
},
});
expect(response.data).toEqual(mockResponse);
diff --git a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
index f1fc5e4d90b..97038a2a231 100644
--- a/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/components/add_namespace_modal/groups_list_spec.js
@@ -27,6 +27,7 @@ jest.mock('~/jira_connect/subscriptions/api', () => {
});
const mockGroupsPath = '/groups';
+const mockAccessToken = '123';
describe('GroupsList', () => {
let wrapper;
@@ -39,6 +40,9 @@ describe('GroupsList', () => {
provide: {
groupsPath: mockGroupsPath,
},
+ computed: {
+ accessToken: () => mockAccessToken,
+ },
...options,
}),
);
@@ -148,11 +152,15 @@ describe('GroupsList', () => {
});
it('calls `fetchGroups` with search term', () => {
- expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
- page: 1,
- perPage: DEFAULT_GROUPS_PER_PAGE,
- search: mockSearchTeam,
- });
+ expect(fetchGroups).toHaveBeenLastCalledWith(
+ mockGroupsPath,
+ {
+ page: 1,
+ perPage: DEFAULT_GROUPS_PER_PAGE,
+ search: mockSearchTeam,
+ },
+ mockAccessToken,
+ );
});
it('disables GroupListItems', () => {
@@ -222,11 +230,15 @@ describe('GroupsList', () => {
findSearchBox().vm.$emit('input', newSearch);
if (shouldSearch) {
- expect(fetchGroups).toHaveBeenCalledWith(mockGroupsPath, {
- page: 1,
- perPage: DEFAULT_GROUPS_PER_PAGE,
- search: expectedSearchValue,
- });
+ expect(fetchGroups).toHaveBeenCalledWith(
+ mockGroupsPath,
+ {
+ page: 1,
+ perPage: DEFAULT_GROUPS_PER_PAGE,
+ search: expectedSearchValue,
+ },
+ mockAccessToken,
+ );
} else {
expect(fetchGroups).not.toHaveBeenCalled();
}
@@ -257,11 +269,15 @@ describe('GroupsList', () => {
});
it('should load results for page 2', () => {
- expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
- page: 2,
- perPage: DEFAULT_GROUPS_PER_PAGE,
- search: '',
- });
+ expect(fetchGroups).toHaveBeenLastCalledWith(
+ mockGroupsPath,
+ {
+ page: 2,
+ perPage: DEFAULT_GROUPS_PER_PAGE,
+ search: '',
+ },
+ mockAccessToken,
+ );
});
it.each`
@@ -274,11 +290,15 @@ describe('GroupsList', () => {
const searchBox = findSearchBox();
searchBox.vm.$emit('input', searchTerm);
- expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
- page: expectedPage,
- perPage: DEFAULT_GROUPS_PER_PAGE,
- search: expectedSearchTerm,
- });
+ expect(fetchGroups).toHaveBeenLastCalledWith(
+ mockGroupsPath,
+ {
+ page: expectedPage,
+ perPage: DEFAULT_GROUPS_PER_PAGE,
+ search: expectedSearchTerm,
+ },
+ mockAccessToken,
+ );
},
);
});
@@ -324,11 +344,15 @@ describe('GroupsList', () => {
const paginationEl = findPagination();
paginationEl.vm.$emit('input', 2);
- expect(fetchGroups).toHaveBeenLastCalledWith(mockGroupsPath, {
- page: 2,
- perPage: DEFAULT_GROUPS_PER_PAGE,
- search: '',
- });
+ expect(fetchGroups).toHaveBeenLastCalledWith(
+ mockGroupsPath,
+ {
+ page: 2,
+ perPage: DEFAULT_GROUPS_PER_PAGE,
+ search: '',
+ },
+ mockAccessToken,
+ );
});
});
});
diff --git a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
index c12a45b2f41..b27eba6b040 100644
--- a/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
+++ b/spec/frontend/jira_connect/subscriptions/pages/sign_in/sign_in_page_spec.js
@@ -15,7 +15,7 @@ describe('SignInPage', () => {
const createComponent = ({
props = {},
jiraConnectOauthEnabled,
- jiraConnectOauthSelfManagedEnabled,
+ publicKeyStorageEnabled,
} = {}) => {
store = createStore();
@@ -24,10 +24,13 @@ describe('SignInPage', () => {
provide: {
glFeatures: {
jiraConnectOauth: jiraConnectOauthEnabled,
- jiraConnectOauthSelfManaged: jiraConnectOauthSelfManagedEnabled,
},
},
- propsData: { hasSubscriptions: false, ...props },
+ propsData: {
+ hasSubscriptions: false,
+ publicKeyStorageEnabled,
+ ...props,
+ },
});
};
@@ -36,47 +39,23 @@ describe('SignInPage', () => {
});
it.each`
- jiraConnectOauthEnabled | jiraConnectOauthSelfManagedEnabled | shouldRenderDotCom | shouldRenderMultiversion
- ${false} | ${false} | ${true} | ${false}
- ${false} | ${true} | ${true} | ${false}
- ${true} | ${false} | ${true} | ${false}
- ${true} | ${true} | ${false} | ${true}
+ jiraConnectOauthEnabled | publicKeyStorageEnabled | shouldRenderDotCom | shouldRenderMultiversion
+ ${false} | ${true} | ${true} | ${false}
+ ${false} | ${false} | ${true} | ${false}
+ ${true} | ${true} | ${false} | ${true}
+ ${true} | ${false} | ${true} | ${false}
`(
- 'renders correct component when jiraConnectOauth is $jiraConnectOauthEnabled and jiraConnectOauthSelfManaged is $jiraConnectOauthSelfManagedEnabled',
+ 'renders correct component when jiraConnectOauth is $jiraConnectOauthEnabled',
({
jiraConnectOauthEnabled,
- jiraConnectOauthSelfManagedEnabled,
+ publicKeyStorageEnabled,
shouldRenderDotCom,
shouldRenderMultiversion,
}) => {
- createComponent({ jiraConnectOauthEnabled, jiraConnectOauthSelfManagedEnabled });
+ createComponent({ jiraConnectOauthEnabled, publicKeyStorageEnabled });
expect(findSignInGitlabCom().exists()).toBe(shouldRenderDotCom);
expect(findSignInGitabMultiversion().exists()).toBe(shouldRenderMultiversion);
},
);
-
- describe('when jiraConnectOauthSelfManaged is false', () => {
- beforeEach(() => {
- createComponent({ jiraConnectOauthSelfManaged: false, props: { hasSubscriptions: true } });
- });
-
- it('renders SignInGitlabCom with correct props', () => {
- expect(findSignInGitlabCom().props()).toEqual({ hasSubscriptions: true });
- });
-
- describe('when error event is emitted', () => {
- it('emits another error event', () => {
- findSignInGitlabCom().vm.$emit('error');
- expect(wrapper.emitted('error')).toHaveLength(1);
- });
- });
-
- describe('when sign-in-oauth event is emitted', () => {
- it('emits another sign-in-oauth event', () => {
- findSignInGitlabCom().vm.$emit('sign-in-oauth');
- expect(wrapper.emitted('sign-in-oauth')[0]).toEqual([]);
- });
- });
- });
});
diff --git a/spec/frontend/jobs/components/job/job_app_spec.js b/spec/frontend/jobs/components/job/job_app_spec.js
index 98f1979db1b..cefedcd82fb 100644
--- a/spec/frontend/jobs/components/job/job_app_spec.js
+++ b/spec/frontend/jobs/components/job/job_app_spec.js
@@ -15,7 +15,10 @@ import StuckBlock from '~/jobs/components/job/stuck_block.vue';
import UnmetPrerequisitesBlock from '~/jobs/components/job/unmet_prerequisites_block.vue';
import createStore from '~/jobs/store';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import { MANUAL_STATUS } from '~/jobs/constants';
import job from '../../mock_data';
+import { mockPendingJobData } from './mock_data';
describe('Job App', () => {
Vue.use(Vuex);
@@ -48,8 +51,8 @@ describe('Job App', () => {
};
const setupAndMount = async ({ jobData = {}, jobLogData = {} } = {}) => {
- mock.onGet(initSettings.endpoint).replyOnce(200, { ...job, ...jobData });
- mock.onGet(`${initSettings.pagePath}/trace.json`).reply(200, jobLogData);
+ mock.onGet(initSettings.endpoint).replyOnce(HTTP_STATUS_OK, { ...job, ...jobData });
+ mock.onGet(`${initSettings.pagePath}/trace.json`).reply(HTTP_STATUS_OK, jobLogData);
const asyncInit = store.dispatch('init', initSettings);
@@ -310,4 +313,29 @@ describe('Job App', () => {
expect(findJobLog().exists()).toBe(true);
});
});
+
+ describe('job log polling', () => {
+ beforeEach(() => {
+ jest.spyOn(store, 'dispatch');
+ });
+
+ it('should poll job log by default', async () => {
+ await setupAndMount({
+ jobData: mockPendingJobData,
+ });
+
+ expect(store.dispatch).toHaveBeenCalledWith('fetchJobLog');
+ });
+
+ it('should NOT poll job log for manual variables form empty state', async () => {
+ const manualPendingJobData = mockPendingJobData;
+ manualPendingJobData.status.group = MANUAL_STATUS;
+
+ await setupAndMount({
+ jobData: manualPendingJobData,
+ });
+
+ expect(store.dispatch).not.toHaveBeenCalledWith('fetchJobLog');
+ });
+ });
});
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 3040570df19..a5b3b0e3b47 100644
--- a/spec/frontend/jobs/components/job/manual_variables_form_spec.js
+++ b/spec/frontend/jobs/components/job/manual_variables_form_spec.js
@@ -4,9 +4,10 @@ import VueApollo from 'vue-apollo';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { TYPENAME_CI_BUILD } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
-import { GRAPHQL_ID_TYPES } from '~/jobs/constants';
import waitForPromises from 'helpers/wait_for_promises';
+import { redirectTo } from '~/lib/utils/url_utility';
import ManualVariablesForm from '~/jobs/components/job/manual_variables_form.vue';
import getJobQuery from '~/jobs/components/job/graphql/queries/get_job.query.graphql';
import retryJobMutation from '~/jobs/components/job/graphql/mutations/job_retry_with_variables.mutation.graphql';
@@ -21,6 +22,11 @@ import {
const localVue = createLocalVue();
localVue.use(VueApollo);
+jest.mock('~/lib/utils/url_utility', () => ({
+ ...jest.requireActual('~/lib/utils/url_utility'),
+ redirectTo: jest.fn(),
+}));
+
const defaultProvide = {
projectPath: mockFullPath,
};
@@ -146,7 +152,7 @@ describe('Manual Variables Form', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: retryJobMutation,
variables: {
- id: convertToGraphQLId(GRAPHQL_ID_TYPES.ciBuild, mockId),
+ id: convertToGraphQLId(TYPENAME_CI_BUILD, mockId),
variables: [
{
key: 'new key',
@@ -156,6 +162,15 @@ describe('Manual Variables Form', () => {
},
});
});
+
+ // redirect to job after initial trigger assertion will be added in https://gitlab.com/gitlab-org/gitlab/-/issues/377268
+ it('redirects to job properly after rerun', async () => {
+ findRerunBtn().vm.$emit('click');
+ await waitForPromises();
+
+ expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledTimes(1);
+ expect(redirectTo).toHaveBeenCalledWith(mockJobMutationData.data.jobRetry.job.webPath);
+ });
});
describe('updating variables in UI', () => {
diff --git a/spec/frontend/jobs/components/job/mock_data.js b/spec/frontend/jobs/components/job/mock_data.js
index 9596e859475..8a838acca7a 100644
--- a/spec/frontend/jobs/components/job/mock_data.js
+++ b/spec/frontend/jobs/components/job/mock_data.js
@@ -74,3 +74,25 @@ export const mockJobMutationData = {
},
},
};
+
+export const mockPendingJobData = {
+ has_trace: false,
+ status: {
+ group: 'pending',
+ icon: 'status_pending',
+ label: 'pending',
+ text: 'pending',
+ details_path: 'path',
+ illustration: {
+ image: 'path',
+ size: '340',
+ title: '',
+ content: '',
+ },
+ action: {
+ button_title: 'Retry job',
+ method: 'post',
+ path: '/path',
+ },
+ },
+};
diff --git a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
index 7cc008f332d..55fe534aa3b 100644
--- a/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
+++ b/spec/frontend/jobs/components/table/cells/actions_cell_spec.js
@@ -37,6 +37,7 @@ describe('Job actions cell', () => {
const cancelableJob = findMockJob('cancelable');
const playableJob = findMockJob('playable');
const retryableJob = findMockJob('retryable');
+ const failedJob = findMockJob('failed');
const scheduledJob = findMockJob('scheduled');
const jobWithArtifact = findMockJob('with_artifact');
const cannotPlayJob = findMockJob('playable', mockJobsNodesAsGuest);
@@ -79,10 +80,6 @@ describe('Job actions cell', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays the artifacts download button with correct link', () => {
createComponent(jobWithArtifact);
@@ -191,6 +188,20 @@ describe('Job actions cell', () => {
expect(button().props('disabled')).toBe(true);
});
+ describe('Retry button title', () => {
+ it('displays retry title when job has failed and is retryable', () => {
+ createComponent(failedJob);
+
+ expect(findRetryButton().attributes('title')).toBe('Retry');
+ });
+
+ it('displays run again title when job has passed and is retryable', () => {
+ createComponent(retryableJob);
+
+ expect(findRetryButton().attributes('title')).toBe('Run again');
+ });
+ });
+
describe('Scheduled Jobs', () => {
const today = () => new Date('2021-08-31');
diff --git a/spec/frontend/jobs/store/actions_spec.js b/spec/frontend/jobs/store/actions_spec.js
index 0d11c4d56bf..73a158d52d8 100644
--- a/spec/frontend/jobs/store/actions_spec.js
+++ b/spec/frontend/jobs/store/actions_spec.js
@@ -30,6 +30,7 @@ import {
import * as types from '~/jobs/store/mutation_types';
import state from '~/jobs/store/state';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Job State actions', () => {
let mockedState;
@@ -112,7 +113,9 @@ describe('Job State actions', () => {
describe('success', () => {
it('dispatches requestJob and receiveJobSuccess', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, { id: 121212, name: 'karma' });
+ mock
+ .onGet(`${TEST_HOST}/endpoint.json`)
+ .replyOnce(HTTP_STATUS_OK, { id: 121212, name: 'karma' });
return testAction(
fetchJob,
@@ -134,7 +137,7 @@ describe('Job State actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches requestJob and receiveJobError', () => {
@@ -214,7 +217,7 @@ describe('Job State actions', () => {
describe('success', () => {
it('dispatches requestJobLog, receiveJobLogSuccess and stopPollingJobLog when job is complete', () => {
- mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, {
+ mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(HTTP_STATUS_OK, {
html: 'I, [2018-08-17T22:57:45.707325 #1841] INFO -- :',
complete: true,
});
@@ -252,7 +255,7 @@ describe('Job State actions', () => {
complete: false,
};
- mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(200, jobLogPayload);
+ mock.onGet(`${TEST_HOST}/endpoint/trace.json`).replyOnce(HTTP_STATUS_OK, jobLogPayload);
});
it('dispatches startPollingJobLog', () => {
@@ -288,7 +291,7 @@ describe('Job State actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(500);
+ mock.onGet(`${TEST_HOST}/endpoint/trace.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches requestJobLog and receiveJobLogError', () => {
@@ -424,9 +427,10 @@ describe('Job State actions', () => {
describe('success', () => {
it('dispatches requestJobsForStage and receiveJobsForStageSuccess', () => {
- mock
- .onGet(`${TEST_HOST}/jobs.json`)
- .replyOnce(200, { latest_statuses: [{ id: 121212, name: 'build' }], retried: [] });
+ mock.onGet(`${TEST_HOST}/jobs.json`).replyOnce(HTTP_STATUS_OK, {
+ latest_statuses: [{ id: 121212, name: 'build' }],
+ retried: [],
+ });
return testAction(
fetchJobsForStage,
@@ -449,7 +453,7 @@ describe('Job State actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/jobs.json`).reply(500);
+ mock.onGet(`${TEST_HOST}/jobs.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches requestJobsForStage and receiveJobsForStageError', () => {
diff --git a/spec/frontend/labels/components/promote_label_modal_spec.js b/spec/frontend/labels/components/promote_label_modal_spec.js
index 8953e3cbcd8..97913c20229 100644
--- a/spec/frontend/labels/components/promote_label_modal_spec.js
+++ b/spec/frontend/labels/components/promote_label_modal_spec.js
@@ -6,6 +6,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import { stubComponent } from 'helpers/stub_component';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PromoteLabelModal from '~/labels/components/promote_label_modal.vue';
import eventHub from '~/labels/event_hub';
@@ -66,7 +67,7 @@ describe('Promote label modal', () => {
it('redirects when a label is promoted', async () => {
const responseURL = `${TEST_HOST}/dummy/endpoint`;
- axiosMock.onPost(labelMockData.url).reply(200, { url: responseURL });
+ axiosMock.onPost(labelMockData.url).reply(HTTP_STATUS_OK, { url: responseURL });
wrapper.findComponent(GlModal).vm.$emit('primary');
@@ -85,8 +86,10 @@ describe('Promote label modal', () => {
it('displays an error if promoting a label failed', async () => {
const dummyError = new Error('promoting label failed');
- dummyError.response = { status: 500 };
- axiosMock.onPost(labelMockData.url).reply(500, { error: dummyError });
+ dummyError.response = { status: HTTP_STATUS_INTERNAL_SERVER_ERROR };
+ axiosMock
+ .onPost(labelMockData.url)
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, { error: dummyError });
wrapper.findComponent(GlModal).vm.$emit('primary');
diff --git a/spec/frontend/language_switcher/components/app_spec.js b/spec/frontend/language_switcher/components/app_spec.js
index effb71c2775..7f6fb138d89 100644
--- a/spec/frontend/language_switcher/components/app_spec.js
+++ b/spec/frontend/language_switcher/components/app_spec.js
@@ -28,7 +28,7 @@ describe('<LanguageSwitcher />', () => {
wrapper.destroy();
});
- const getPreferredLanguage = () => wrapper.find('.gl-dropdown-button-text').text();
+ const getPreferredLanguage = () => wrapper.find('.gl-new-dropdown-button-text').text();
const findLanguageDropdownItem = (code) => wrapper.findByTestId(`language_switcher_lang_${code}`);
const findFooter = () => wrapper.findByTestId('footer');
diff --git a/spec/frontend/lazy_loader_spec.js b/spec/frontend/lazy_loader_spec.js
index e0b6c7119f9..190114606ec 100644
--- a/spec/frontend/lazy_loader_spec.js
+++ b/spec/frontend/lazy_loader_spec.js
@@ -60,7 +60,6 @@ describe('LazyLoader', () => {
beforeEach(() => {
jest.spyOn(window, 'requestAnimationFrame').mockImplementation(execImmediately);
jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
- jest.spyOn(LazyLoader, 'loadImage');
mockLoadEvent();
});
@@ -106,7 +105,6 @@ describe('LazyLoader', () => {
trigger(img);
- expect(LazyLoader.loadImage).toHaveBeenCalledWith(img);
expect(img.getAttribute('src')).toBe(TEST_PATH);
expect(img.dataset.src).toBeUndefined();
expect(img).toHaveClass('js-lazy-loaded');
@@ -121,7 +119,6 @@ describe('LazyLoader', () => {
await waitForPromises();
- expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg.getAttribute('src')).toBe(TEST_PATH);
expect(newImg).toHaveClass('js-lazy-loaded');
});
@@ -131,7 +128,6 @@ describe('LazyLoader', () => {
lazyLoader.register();
- expect(LazyLoader.loadImage).not.toHaveBeenCalled();
expect(newImg).not.toHaveClass('js-lazy-loaded');
});
@@ -143,7 +139,6 @@ describe('LazyLoader', () => {
await waitForPromises();
- expect(LazyLoader.loadImage).not.toHaveBeenCalledWith(newImg);
expect(newImg).not.toHaveClass('js-lazy-loaded');
});
@@ -158,7 +153,6 @@ describe('LazyLoader', () => {
await waitForPromises();
- expect(LazyLoader.loadImage).toHaveBeenCalledWith(newImg);
expect(newImg.getAttribute('src')).toBe(TEST_PATH);
expect(newImg).toHaveClass('js-lazy-loaded');
});
diff --git a/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive.json b/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive.json
new file mode 100644
index 00000000000..a0d67885dad
--- /dev/null
+++ b/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive.json
@@ -0,0 +1,3089 @@
+{
+ "Project:gid://gitlab/Project/6": {
+ "__typename": "Project",
+ "id": "gid://gitlab/Project/6",
+ "issues({\"includeSubepics\":true,\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 1115
+ },
+ "issues({\"includeSubepics\":true,\"state\":\"closed\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 16
+ },
+ "issues({\"includeSubepics\":true,\"state\":\"all\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 1131
+ },
+ "issues({\"after\":null,\"before\":\"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4yMzI5NDUwMDAgKzAwMDAiLCJpZCI6IjE1NjYifQ\",\"includeSubepics\":true,\"last\":20,\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": false,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0xMCAxMjozNjo1NC41NDYxNzEwMDAgKzAwMDAiLCJpZCI6IjQ4MyJ9",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4zMDE3NDcwMDAgKzAwMDAiLCJpZCI6IjE1NjcifQ"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/483"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1585"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1584"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1583"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1582"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1581"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1580"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1579"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1578"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1577"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1576"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1575"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1574"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1573"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1572"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1571"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1570"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1569"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1568"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1567"
+ }
+ ]
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 44
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"closed\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 0
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"all\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 44
+ },
+ "issues({\"after\":null,\"before\":null,\"first\":20,\"includeSubepics\":true,\"search\":\"sint\",\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": false,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNTozMC4zMTM3NDMwMDAgKzAwMDAiLCJpZCI6IjE1ODQifQ",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1OC43NDI3NTkwMDAgKzAwMDAiLCJpZCI6IjExNDEifQ"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1584"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1540"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1532"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1515"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1514"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1463"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1461"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1439"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1403"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1399"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1375"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1349"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1333"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1321"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1318"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1299"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1268"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1262"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1254"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1141"
+ }
+ ]
+ },
+ "projectMembers({\"relations\":[\"DIRECT\",\"INHERITED\",\"INVITED_GROUPS\"],\"search\":\"\"})": {
+ "__typename": "MemberInterfaceConnection",
+ "nodes": [
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/54"
+ },
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/53"
+ },
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/52"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/26"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/25"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/11"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/10"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/9"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/1"
+ }
+ ]
+ },
+ "milestones({\"includeAncestors\":true,\"searchTitle\":\"\",\"sort\":\"EXPIRED_LAST_DUE_DATE_ASC\",\"state\":\"active\"})": {
+ "__typename": "MilestoneConnection",
+ "nodes": [
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/30"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/28"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/27"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/26"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/45"
+ }
+ ]
+ },
+ "labels({\"includeAncestorGroups\":true,\"searchTerm\":\"\"})": {
+ "__typename": "LabelConnection",
+ "nodes": [
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/99"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/41"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/48"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/46"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/50"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/44"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/96"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/45"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/95"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/49"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/98"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/97"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/47"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/42"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/43"
+ }
+ ]
+ },
+ "issues({\"after\":\"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1OC43NDI3NTkwMDAgKzAwMDAiLCJpZCI6IjExNDEifQ\",\"before\":null,\"first\":20,\"includeSubepics\":true,\"search\":\"sint\",\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": true,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1Ny42NTgwNTMwMDAgKzAwMDAiLCJpZCI6IjExMjMifQ",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowMjoyNy42OTg0MDEwMDAgKzAwMDAiLCJpZCI6IjU0MiJ9"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1123"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1100"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1084"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1052"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1017"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1007"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/988"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/949"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/908"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/852"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/842"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/782"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/779"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/769"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/718"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/634"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/614"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/564"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/553"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/542"
+ }
+ ]
+ }
+ },
+ "ROOT_QUERY": {
+ "__typename": "Query",
+ "project({\"fullPath\":\"flightjs/Flight\"}) @persist": {
+ "__ref": "Project:gid://gitlab/Project/6"
+ }
+ },
+ "UserCore:gid://gitlab/User/1": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/1",
+ "avatarUrl": "https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "name": "Administrator",
+ "username": "root",
+ "webUrl": "https://gdk.test:3443/root"
+ },
+ "Issue:gid://gitlab/Issue/483": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/483",
+ "iid": "31",
+ "confidential": false,
+ "createdAt": "2022-09-11T15:24:16Z",
+ "downvotes": 1,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 1,
+ "moved": false,
+ "state": "opened",
+ "title": "Instigate the Incident!",
+ "updatedAt": "2023-01-10T12:36:54Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 2,
+ "webPath": "/flightjs/Flight/-/issues/31",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/31",
+ "type": "INCIDENT",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1585": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1585",
+ "iid": "1131",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "proident aute commodo",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1131",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1131",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1584": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1584",
+ "iid": "1130",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint eiusmod eiusmod",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1130",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1130",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1583": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1583",
+ "iid": "1129",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "commodo mollit est",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1129",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1129",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1582": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1582",
+ "iid": "1128",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "non dolore laborum",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1128",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1128",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1581": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1581",
+ "iid": "1127",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "commodo do occaecat",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1127",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1127",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1580": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1580",
+ "iid": "1126",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ea nostrud ea",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1126",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1126",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1579": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1579",
+ "iid": "1125",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sed lorem fugiat",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1125",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1125",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1578": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1578",
+ "iid": "1124",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit anim sunt",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1124",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1124",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1577": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1577",
+ "iid": "1123",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "adipiscing fugiat ullamco",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1123",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1123",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1576": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1576",
+ "iid": "1122",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "pariatur et elit",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1122",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1122",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1575": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1575",
+ "iid": "1121",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ut ipsum occaecat",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1121",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1121",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1574": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1574",
+ "iid": "1120",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit ea elit",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1120",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1120",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1573": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1573",
+ "iid": "1119",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "nostrud voluptate do",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1119",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1119",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1572": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1572",
+ "iid": "1118",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ullamco consequat in",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1118",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1118",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1571": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1571",
+ "iid": "1117",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit Ut est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1117",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1117",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1570": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1570",
+ "iid": "1116",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "lorem commodo est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1116",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1116",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1569": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1569",
+ "iid": "1115",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "tempor irure laboris",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1115",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1115",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1568": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1568",
+ "iid": "1114",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "voluptate aliquip est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1114",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1114",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1567": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1567",
+ "iid": "1113",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "exercitation dolore labore",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1113",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1113",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1540": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1540",
+ "iid": "1086",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint nulla dolore",
+ "updatedAt": "2023-01-09T04:05:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1086",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1086",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1532": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1532",
+ "iid": "1078",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "amet culpa sint",
+ "updatedAt": "2023-01-09T04:05:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1078",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1078",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1515": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1515",
+ "iid": "1061",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:26Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint Duis incididunt",
+ "updatedAt": "2023-01-09T04:05:26Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1061",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1061",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1514": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1514",
+ "iid": "1060",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:25Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint velit ullamco",
+ "updatedAt": "2023-01-09T04:05:25Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1060",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1060",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1463": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1463",
+ "iid": "1009",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor occaecat sint",
+ "updatedAt": "2023-01-09T04:05:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1009",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1009",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1461": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1461",
+ "iid": "1007",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit sint irure",
+ "updatedAt": "2023-01-09T04:05:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1007",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1007",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1439": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1439",
+ "iid": "985",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:21Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint Ut amet",
+ "updatedAt": "2023-01-09T04:05:21Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/985",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/985",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1403": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1403",
+ "iid": "949",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:18Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "in consequat sint",
+ "updatedAt": "2023-01-09T04:05:18Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/949",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/949",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1399": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1399",
+ "iid": "945",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:18Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit nulla sint",
+ "updatedAt": "2023-01-09T04:05:18Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/945",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/945",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1375": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1375",
+ "iid": "921",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:16Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint sed ex",
+ "updatedAt": "2023-01-09T04:05:16Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/921",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/921",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1349": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1349",
+ "iid": "895",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:13Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "magna reprehenderit sint",
+ "updatedAt": "2023-01-09T04:05:13Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/895",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/895",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1333": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1333",
+ "iid": "879",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:11Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "tempor dolore sint",
+ "updatedAt": "2023-01-09T04:05:11Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/879",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/879",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1321": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1321",
+ "iid": "867",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:10Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "reprehenderit pariatur sint",
+ "updatedAt": "2023-01-09T04:05:10Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/867",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/867",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1318": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1318",
+ "iid": "864",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:10Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sit sint ad",
+ "updatedAt": "2023-01-09T04:05:10Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/864",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/864",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1299": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1299",
+ "iid": "845",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:08Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit sint fugiat",
+ "updatedAt": "2023-01-09T04:05:08Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/845",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/845",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1268": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1268",
+ "iid": "814",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:06Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor nostrud sint",
+ "updatedAt": "2023-01-09T04:05:06Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/814",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/814",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1262": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1262",
+ "iid": "808",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:06Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ut sint esse",
+ "updatedAt": "2023-01-09T04:05:06Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/808",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/808",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1254": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1254",
+ "iid": "800",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:05Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint ea est",
+ "updatedAt": "2023-01-09T04:05:05Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/800",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/800",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1141": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1141",
+ "iid": "687",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:58Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint quis laboris",
+ "updatedAt": "2023-01-09T04:04:58Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/687",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/687",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "UserCore:gid://gitlab/User/9": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/9",
+ "avatarUrl": "https://secure.gravatar.com/avatar/175e76e391370beeb21914ab74c2efd4?s=80&d=identicon",
+ "name": "Kiyoko Bahringer",
+ "username": "jamie"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/54": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/54",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/9"
+ }
+ },
+ "UserCore:gid://gitlab/User/19": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/19",
+ "avatarUrl": "https://secure.gravatar.com/avatar/3126153e3301ebf7cc8f7c99e57007f2?s=80&d=identicon",
+ "name": "Cecile Hermann",
+ "username": "jeannetta_breitenberg"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/53": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/53",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/19"
+ }
+ },
+ "UserCore:gid://gitlab/User/2": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/2",
+ "avatarUrl": "https://secure.gravatar.com/avatar/a138e401136c90561f949297387a3bb9?s=80&d=identicon",
+ "name": "Tish Treutel",
+ "username": "liana.larkin"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/52": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/52",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/2"
+ }
+ },
+ "UserCore:gid://gitlab/User/13": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/13",
+ "avatarUrl": "https://secure.gravatar.com/avatar/0ce8057f452296a13b5620bb2d9ede57?s=80&d=identicon",
+ "name": "Tammy Gusikowski",
+ "username": "xuan_oreilly"
+ },
+ "GroupMember:gid://gitlab/GroupMember/26": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/26",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/13"
+ }
+ },
+ "UserCore:gid://gitlab/User/21": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/21",
+ "avatarUrl": "https://secure.gravatar.com/avatar/415b09d256f26403384363d7948c4d77?s=80&d=identicon",
+ "name": "Twanna Hegmann",
+ "username": "jamaal"
+ },
+ "GroupMember:gid://gitlab/GroupMember/25": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/25",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/21"
+ }
+ },
+ "UserCore:gid://gitlab/User/14": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/14",
+ "avatarUrl": "https://secure.gravatar.com/avatar/e99697c6664381b0351b7617717dd49b?s=80&d=identicon",
+ "name": "Francie Cole",
+ "username": "greg.wisoky"
+ },
+ "GroupMember:gid://gitlab/GroupMember/11": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/11",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/14"
+ }
+ },
+ "UserCore:gid://gitlab/User/7": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/7",
+ "avatarUrl": "https://secure.gravatar.com/avatar/3a382857e362d6cce60d3806dd173444?s=80&d=identicon",
+ "name": "Ivan Carter",
+ "username": "ethyl"
+ },
+ "GroupMember:gid://gitlab/GroupMember/10": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/10",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/7"
+ }
+ },
+ "UserCore:gid://gitlab/User/15": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/15",
+ "avatarUrl": "https://secure.gravatar.com/avatar/79653006ff557e081db02deaa4ca281c?s=80&d=identicon",
+ "name": "Danuta Dare",
+ "username": "maddie_hintz"
+ },
+ "GroupMember:gid://gitlab/GroupMember/9": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/9",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/15"
+ }
+ },
+ "GroupMember:gid://gitlab/GroupMember/1": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/1",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ }
+ },
+ "Milestone:gid://gitlab/Milestone/30": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/30",
+ "title": "v4.0"
+ },
+ "Milestone:gid://gitlab/Milestone/28": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/28",
+ "title": "v2.0"
+ },
+ "Milestone:gid://gitlab/Milestone/27": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/27",
+ "title": "v1.0"
+ },
+ "Milestone:gid://gitlab/Milestone/26": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/26",
+ "title": "v0.0"
+ },
+ "Milestone:gid://gitlab/Milestone/45": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/45",
+ "title": "Sprint - Autem id maxime consequatur quam."
+ },
+ "Label:gid://gitlab/ProjectLabel/99": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/99",
+ "color": "#a5c6fb",
+ "textColor": "#333333",
+ "title": "Accent"
+ },
+ "Label:gid://gitlab/GroupLabel/41": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/41",
+ "color": "#0609ba",
+ "textColor": "#FFFFFF",
+ "title": "Breckwood"
+ },
+ "Label:gid://gitlab/GroupLabel/48": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/48",
+ "color": "#fa7620",
+ "textColor": "#FFFFFF",
+ "title": "Brieph"
+ },
+ "Label:gid://gitlab/GroupLabel/46": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/46",
+ "color": "#d97020",
+ "textColor": "#FFFFFF",
+ "title": "Bryntfunc"
+ },
+ "Label:gid://gitlab/GroupLabel/50": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/50",
+ "color": "#8a934f",
+ "textColor": "#FFFFFF",
+ "title": "CL"
+ },
+ "Label:gid://gitlab/GroupLabel/44": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/44",
+ "color": "#9e1d53",
+ "textColor": "#FFFFFF",
+ "title": "Cofunc"
+ },
+ "Label:gid://gitlab/ProjectLabel/96": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/96",
+ "color": "#0384f3",
+ "textColor": "#FFFFFF",
+ "title": "Corolla"
+ },
+ "Label:gid://gitlab/GroupLabel/45": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/45",
+ "color": "#f0b448",
+ "textColor": "#FFFFFF",
+ "title": "Cygcell"
+ },
+ "Label:gid://gitlab/ProjectLabel/95": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/95",
+ "color": "#d13231",
+ "textColor": "#FFFFFF",
+ "title": "Freestyle"
+ },
+ "Label:gid://gitlab/GroupLabel/49": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/49",
+ "color": "#f43983",
+ "textColor": "#FFFFFF",
+ "title": "Genbalt"
+ },
+ "Label:gid://gitlab/ProjectLabel/98": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/98",
+ "color": "#247441",
+ "textColor": "#FFFFFF",
+ "title": "LaSabre"
+ },
+ "Label:gid://gitlab/ProjectLabel/97": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/97",
+ "color": "#3bd51a",
+ "textColor": "#FFFFFF",
+ "title": "Probe"
+ },
+ "Label:gid://gitlab/GroupLabel/47": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/47",
+ "color": "#6bfb9d",
+ "textColor": "#333333",
+ "title": "Techbalt"
+ },
+ "Label:gid://gitlab/GroupLabel/42": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/42",
+ "color": "#996016",
+ "textColor": "#FFFFFF",
+ "title": "Troffe"
+ },
+ "Label:gid://gitlab/GroupLabel/43": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/43",
+ "color": "#a75c05",
+ "textColor": "#FFFFFF",
+ "title": "Tronceforge"
+ },
+ "Issue:gid://gitlab/Issue/1123": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1123",
+ "iid": "669",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:57Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "esse sint est",
+ "updatedAt": "2023-01-09T04:04:57Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/669",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/669",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1100": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1100",
+ "iid": "646",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:56Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "cupidatat sunt sint",
+ "updatedAt": "2023-01-09T04:04:56Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/646",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/646",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1084": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1084",
+ "iid": "630",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:54Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "culpa sint irure",
+ "updatedAt": "2023-01-09T04:04:54Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/630",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/630",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1052": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1052",
+ "iid": "598",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:52Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint in anim",
+ "updatedAt": "2023-01-09T04:04:52Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/598",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/598",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1017": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1017",
+ "iid": "563",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:50Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint lorem sint",
+ "updatedAt": "2023-01-09T04:04:50Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/563",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/563",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1007": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1007",
+ "iid": "553",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:49Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ea non sint",
+ "updatedAt": "2023-01-09T04:04:49Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/553",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/553",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/988": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/988",
+ "iid": "534",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:47Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "minim ea sint",
+ "updatedAt": "2023-01-09T04:04:47Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/534",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/534",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/949": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/949",
+ "iid": "495",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:42Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "adipiscing sint ullamco",
+ "updatedAt": "2023-01-09T04:04:42Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/495",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/495",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/908": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/908",
+ "iid": "454",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:38Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sit dolore sint",
+ "updatedAt": "2023-01-09T04:04:38Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/454",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/454",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/852": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/852",
+ "iid": "398",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:32Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor adipiscing sint",
+ "updatedAt": "2023-01-09T04:04:32Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/398",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/398",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/842": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/842",
+ "iid": "388",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:31Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "exercitation consequat sint",
+ "updatedAt": "2023-01-09T04:04:31Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/388",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/388",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/782": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/782",
+ "iid": "328",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:23Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "eiusmod mollit sint",
+ "updatedAt": "2023-01-09T04:04:23Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/328",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/328",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/779": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/779",
+ "iid": "325",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:23Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sunt sint aute",
+ "updatedAt": "2023-01-09T04:04:23Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/325",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/325",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/769": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/769",
+ "iid": "315",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "aute et sint",
+ "updatedAt": "2023-01-09T04:04:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/315",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/315",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/718": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/718",
+ "iid": "264",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:15Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "quis sint in",
+ "updatedAt": "2023-01-09T04:04:15Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/264",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/264",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/634": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/634",
+ "iid": "180",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:05Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint in Duis",
+ "updatedAt": "2023-01-09T04:04:05Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/180",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/180",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/614": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/614",
+ "iid": "160",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:02Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ex magna sint",
+ "updatedAt": "2023-01-09T04:04:02Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/160",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/160",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/564": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/564",
+ "iid": "110",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "pariatur dolore sint",
+ "updatedAt": "2023-01-09T04:02:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/110",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/110",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/553": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/553",
+ "iid": "99",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:28Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor sint anim",
+ "updatedAt": "2023-01-09T04:02:28Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/99",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/99",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/542": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/542",
+ "iid": "88",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint eiusmod anim",
+ "updatedAt": "2023-01-09T04:02:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/88",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/88",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ }
+}
diff --git a/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json b/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json
new file mode 100644
index 00000000000..c0651517986
--- /dev/null
+++ b/spec/frontend/lib/apollo/mock_data/cache_with_persist_directive_and_field.json
@@ -0,0 +1,3091 @@
+{
+ "Project:gid://gitlab/Project/6": {
+ "__typename": "Project",
+ "id": "gid://gitlab/Project/6",
+ "issues({\"includeSubepics\":true,\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 1115
+ },
+ "issues({\"includeSubepics\":true,\"state\":\"closed\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 16
+ },
+ "issues({\"includeSubepics\":true,\"state\":\"all\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 1131
+ },
+ "issues({\"after\":null,\"before\":\"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4yMzI5NDUwMDAgKzAwMDAiLCJpZCI6IjE1NjYifQ\",\"includeSubepics\":true,\"last\":20,\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "__persist": true,
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": false,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0xMCAxMjozNjo1NC41NDYxNzEwMDAgKzAwMDAiLCJpZCI6IjQ4MyJ9",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4zMDE3NDcwMDAgKzAwMDAiLCJpZCI6IjE1NjcifQ"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/483"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1585"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1584"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1583"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1582"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1581"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1580"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1579"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1578"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1577"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1576"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1575"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1574"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1573"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1572"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1571"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1570"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1569"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1568"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1567"
+ }
+ ]
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 44
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"closed\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 0
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"all\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 44
+ },
+ "issues({\"after\":null,\"before\":null,\"first\":20,\"includeSubepics\":true,\"search\":\"sint\",\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": false,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNTozMC4zMTM3NDMwMDAgKzAwMDAiLCJpZCI6IjE1ODQifQ",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1OC43NDI3NTkwMDAgKzAwMDAiLCJpZCI6IjExNDEifQ"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1584"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1540"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1532"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1515"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1514"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1463"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1461"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1439"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1403"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1399"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1375"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1349"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1333"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1321"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1318"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1299"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1268"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1262"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1254"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1141"
+ }
+ ]
+ },
+ "projectMembers({\"relations\":[\"DIRECT\",\"INHERITED\",\"INVITED_GROUPS\"],\"search\":\"\"})": {
+ "__typename": "MemberInterfaceConnection",
+ "nodes": [
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/54"
+ },
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/53"
+ },
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/52"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/26"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/25"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/11"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/10"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/9"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/1"
+ }
+ ]
+ },
+ "milestones({\"includeAncestors\":true,\"searchTitle\":\"\",\"sort\":\"EXPIRED_LAST_DUE_DATE_ASC\",\"state\":\"active\"})": {
+ "__typename": "MilestoneConnection",
+ "nodes": [
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/30"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/28"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/27"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/26"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/45"
+ }
+ ]
+ },
+ "labels({\"includeAncestorGroups\":true,\"searchTerm\":\"\"})": {
+ "__typename": "LabelConnection",
+ "nodes": [
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/99"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/41"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/48"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/46"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/50"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/44"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/96"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/45"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/95"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/49"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/98"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/97"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/47"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/42"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/43"
+ }
+ ]
+ },
+ "issues({\"after\":\"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1OC43NDI3NTkwMDAgKzAwMDAiLCJpZCI6IjExNDEifQ\",\"before\":null,\"first\":20,\"includeSubepics\":true,\"search\":\"sint\",\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": true,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1Ny42NTgwNTMwMDAgKzAwMDAiLCJpZCI6IjExMjMifQ",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowMjoyNy42OTg0MDEwMDAgKzAwMDAiLCJpZCI6IjU0MiJ9"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1123"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1100"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1084"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1052"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1017"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1007"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/988"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/949"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/908"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/852"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/842"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/782"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/779"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/769"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/718"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/634"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/614"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/564"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/553"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/542"
+ }
+ ]
+ }
+ },
+ "ROOT_QUERY": {
+ "__typename": "Query",
+ "project({\"fullPath\":\"flightjs/Flight\"}) @persist": {
+ "__ref": "Project:gid://gitlab/Project/6"
+ }
+ },
+ "UserCore:gid://gitlab/User/1": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/1",
+ "avatarUrl": "https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "name": "Administrator",
+ "username": "root",
+ "webUrl": "https://gdk.test:3443/root"
+ },
+ "Issue:gid://gitlab/Issue/483": {
+ "__typename": "Issue",
+ "__persist": true,
+ "id": "gid://gitlab/Issue/483",
+ "iid": "31",
+ "confidential": false,
+ "createdAt": "2022-09-11T15:24:16Z",
+ "downvotes": 1,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 1,
+ "moved": false,
+ "state": "opened",
+ "title": "Instigate the Incident!",
+ "updatedAt": "2023-01-10T12:36:54Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 2,
+ "webPath": "/flightjs/Flight/-/issues/31",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/31",
+ "type": "INCIDENT",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1585": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1585",
+ "iid": "1131",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "proident aute commodo",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1131",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1131",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1584": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1584",
+ "iid": "1130",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint eiusmod eiusmod",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1130",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1130",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1583": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1583",
+ "iid": "1129",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "commodo mollit est",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1129",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1129",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1582": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1582",
+ "iid": "1128",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "non dolore laborum",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1128",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1128",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1581": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1581",
+ "iid": "1127",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "commodo do occaecat",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1127",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1127",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1580": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1580",
+ "iid": "1126",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ea nostrud ea",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1126",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1126",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1579": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1579",
+ "iid": "1125",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sed lorem fugiat",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1125",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1125",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1578": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1578",
+ "iid": "1124",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit anim sunt",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1124",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1124",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1577": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1577",
+ "iid": "1123",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "adipiscing fugiat ullamco",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1123",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1123",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1576": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1576",
+ "iid": "1122",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "pariatur et elit",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1122",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1122",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1575": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1575",
+ "iid": "1121",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ut ipsum occaecat",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1121",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1121",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1574": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1574",
+ "iid": "1120",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit ea elit",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1120",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1120",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1573": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1573",
+ "iid": "1119",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "nostrud voluptate do",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1119",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1119",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1572": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1572",
+ "iid": "1118",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ullamco consequat in",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1118",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1118",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1571": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1571",
+ "iid": "1117",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit Ut est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1117",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1117",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1570": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1570",
+ "iid": "1116",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "lorem commodo est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1116",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1116",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1569": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1569",
+ "iid": "1115",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "tempor irure laboris",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1115",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1115",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1568": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1568",
+ "iid": "1114",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "voluptate aliquip est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1114",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1114",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1567": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1567",
+ "iid": "1113",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "exercitation dolore labore",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1113",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1113",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1540": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1540",
+ "iid": "1086",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint nulla dolore",
+ "updatedAt": "2023-01-09T04:05:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1086",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1086",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1532": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1532",
+ "iid": "1078",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "amet culpa sint",
+ "updatedAt": "2023-01-09T04:05:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1078",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1078",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1515": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1515",
+ "iid": "1061",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:26Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint Duis incididunt",
+ "updatedAt": "2023-01-09T04:05:26Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1061",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1061",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1514": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1514",
+ "iid": "1060",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:25Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint velit ullamco",
+ "updatedAt": "2023-01-09T04:05:25Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1060",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1060",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1463": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1463",
+ "iid": "1009",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor occaecat sint",
+ "updatedAt": "2023-01-09T04:05:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1009",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1009",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1461": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1461",
+ "iid": "1007",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit sint irure",
+ "updatedAt": "2023-01-09T04:05:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1007",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1007",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1439": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1439",
+ "iid": "985",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:21Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint Ut amet",
+ "updatedAt": "2023-01-09T04:05:21Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/985",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/985",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1403": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1403",
+ "iid": "949",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:18Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "in consequat sint",
+ "updatedAt": "2023-01-09T04:05:18Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/949",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/949",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1399": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1399",
+ "iid": "945",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:18Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit nulla sint",
+ "updatedAt": "2023-01-09T04:05:18Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/945",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/945",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1375": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1375",
+ "iid": "921",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:16Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint sed ex",
+ "updatedAt": "2023-01-09T04:05:16Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/921",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/921",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1349": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1349",
+ "iid": "895",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:13Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "magna reprehenderit sint",
+ "updatedAt": "2023-01-09T04:05:13Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/895",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/895",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1333": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1333",
+ "iid": "879",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:11Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "tempor dolore sint",
+ "updatedAt": "2023-01-09T04:05:11Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/879",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/879",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1321": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1321",
+ "iid": "867",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:10Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "reprehenderit pariatur sint",
+ "updatedAt": "2023-01-09T04:05:10Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/867",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/867",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1318": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1318",
+ "iid": "864",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:10Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sit sint ad",
+ "updatedAt": "2023-01-09T04:05:10Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/864",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/864",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1299": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1299",
+ "iid": "845",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:08Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit sint fugiat",
+ "updatedAt": "2023-01-09T04:05:08Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/845",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/845",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1268": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1268",
+ "iid": "814",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:06Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor nostrud sint",
+ "updatedAt": "2023-01-09T04:05:06Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/814",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/814",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1262": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1262",
+ "iid": "808",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:06Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ut sint esse",
+ "updatedAt": "2023-01-09T04:05:06Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/808",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/808",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1254": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1254",
+ "iid": "800",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:05Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint ea est",
+ "updatedAt": "2023-01-09T04:05:05Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/800",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/800",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1141": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1141",
+ "iid": "687",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:58Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint quis laboris",
+ "updatedAt": "2023-01-09T04:04:58Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/687",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/687",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "UserCore:gid://gitlab/User/9": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/9",
+ "avatarUrl": "https://secure.gravatar.com/avatar/175e76e391370beeb21914ab74c2efd4?s=80&d=identicon",
+ "name": "Kiyoko Bahringer",
+ "username": "jamie"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/54": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/54",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/9"
+ }
+ },
+ "UserCore:gid://gitlab/User/19": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/19",
+ "avatarUrl": "https://secure.gravatar.com/avatar/3126153e3301ebf7cc8f7c99e57007f2?s=80&d=identicon",
+ "name": "Cecile Hermann",
+ "username": "jeannetta_breitenberg"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/53": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/53",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/19"
+ }
+ },
+ "UserCore:gid://gitlab/User/2": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/2",
+ "avatarUrl": "https://secure.gravatar.com/avatar/a138e401136c90561f949297387a3bb9?s=80&d=identicon",
+ "name": "Tish Treutel",
+ "username": "liana.larkin"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/52": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/52",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/2"
+ }
+ },
+ "UserCore:gid://gitlab/User/13": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/13",
+ "avatarUrl": "https://secure.gravatar.com/avatar/0ce8057f452296a13b5620bb2d9ede57?s=80&d=identicon",
+ "name": "Tammy Gusikowski",
+ "username": "xuan_oreilly"
+ },
+ "GroupMember:gid://gitlab/GroupMember/26": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/26",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/13"
+ }
+ },
+ "UserCore:gid://gitlab/User/21": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/21",
+ "avatarUrl": "https://secure.gravatar.com/avatar/415b09d256f26403384363d7948c4d77?s=80&d=identicon",
+ "name": "Twanna Hegmann",
+ "username": "jamaal"
+ },
+ "GroupMember:gid://gitlab/GroupMember/25": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/25",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/21"
+ }
+ },
+ "UserCore:gid://gitlab/User/14": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/14",
+ "avatarUrl": "https://secure.gravatar.com/avatar/e99697c6664381b0351b7617717dd49b?s=80&d=identicon",
+ "name": "Francie Cole",
+ "username": "greg.wisoky"
+ },
+ "GroupMember:gid://gitlab/GroupMember/11": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/11",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/14"
+ }
+ },
+ "UserCore:gid://gitlab/User/7": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/7",
+ "avatarUrl": "https://secure.gravatar.com/avatar/3a382857e362d6cce60d3806dd173444?s=80&d=identicon",
+ "name": "Ivan Carter",
+ "username": "ethyl"
+ },
+ "GroupMember:gid://gitlab/GroupMember/10": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/10",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/7"
+ }
+ },
+ "UserCore:gid://gitlab/User/15": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/15",
+ "avatarUrl": "https://secure.gravatar.com/avatar/79653006ff557e081db02deaa4ca281c?s=80&d=identicon",
+ "name": "Danuta Dare",
+ "username": "maddie_hintz"
+ },
+ "GroupMember:gid://gitlab/GroupMember/9": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/9",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/15"
+ }
+ },
+ "GroupMember:gid://gitlab/GroupMember/1": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/1",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ }
+ },
+ "Milestone:gid://gitlab/Milestone/30": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/30",
+ "title": "v4.0"
+ },
+ "Milestone:gid://gitlab/Milestone/28": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/28",
+ "title": "v2.0"
+ },
+ "Milestone:gid://gitlab/Milestone/27": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/27",
+ "title": "v1.0"
+ },
+ "Milestone:gid://gitlab/Milestone/26": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/26",
+ "title": "v0.0"
+ },
+ "Milestone:gid://gitlab/Milestone/45": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/45",
+ "title": "Sprint - Autem id maxime consequatur quam."
+ },
+ "Label:gid://gitlab/ProjectLabel/99": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/99",
+ "color": "#a5c6fb",
+ "textColor": "#333333",
+ "title": "Accent"
+ },
+ "Label:gid://gitlab/GroupLabel/41": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/41",
+ "color": "#0609ba",
+ "textColor": "#FFFFFF",
+ "title": "Breckwood"
+ },
+ "Label:gid://gitlab/GroupLabel/48": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/48",
+ "color": "#fa7620",
+ "textColor": "#FFFFFF",
+ "title": "Brieph"
+ },
+ "Label:gid://gitlab/GroupLabel/46": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/46",
+ "color": "#d97020",
+ "textColor": "#FFFFFF",
+ "title": "Bryntfunc"
+ },
+ "Label:gid://gitlab/GroupLabel/50": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/50",
+ "color": "#8a934f",
+ "textColor": "#FFFFFF",
+ "title": "CL"
+ },
+ "Label:gid://gitlab/GroupLabel/44": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/44",
+ "color": "#9e1d53",
+ "textColor": "#FFFFFF",
+ "title": "Cofunc"
+ },
+ "Label:gid://gitlab/ProjectLabel/96": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/96",
+ "color": "#0384f3",
+ "textColor": "#FFFFFF",
+ "title": "Corolla"
+ },
+ "Label:gid://gitlab/GroupLabel/45": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/45",
+ "color": "#f0b448",
+ "textColor": "#FFFFFF",
+ "title": "Cygcell"
+ },
+ "Label:gid://gitlab/ProjectLabel/95": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/95",
+ "color": "#d13231",
+ "textColor": "#FFFFFF",
+ "title": "Freestyle"
+ },
+ "Label:gid://gitlab/GroupLabel/49": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/49",
+ "color": "#f43983",
+ "textColor": "#FFFFFF",
+ "title": "Genbalt"
+ },
+ "Label:gid://gitlab/ProjectLabel/98": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/98",
+ "color": "#247441",
+ "textColor": "#FFFFFF",
+ "title": "LaSabre"
+ },
+ "Label:gid://gitlab/ProjectLabel/97": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/97",
+ "color": "#3bd51a",
+ "textColor": "#FFFFFF",
+ "title": "Probe"
+ },
+ "Label:gid://gitlab/GroupLabel/47": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/47",
+ "color": "#6bfb9d",
+ "textColor": "#333333",
+ "title": "Techbalt"
+ },
+ "Label:gid://gitlab/GroupLabel/42": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/42",
+ "color": "#996016",
+ "textColor": "#FFFFFF",
+ "title": "Troffe"
+ },
+ "Label:gid://gitlab/GroupLabel/43": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/43",
+ "color": "#a75c05",
+ "textColor": "#FFFFFF",
+ "title": "Tronceforge"
+ },
+ "Issue:gid://gitlab/Issue/1123": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1123",
+ "iid": "669",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:57Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "esse sint est",
+ "updatedAt": "2023-01-09T04:04:57Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/669",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/669",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1100": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1100",
+ "iid": "646",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:56Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "cupidatat sunt sint",
+ "updatedAt": "2023-01-09T04:04:56Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/646",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/646",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1084": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1084",
+ "iid": "630",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:54Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "culpa sint irure",
+ "updatedAt": "2023-01-09T04:04:54Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/630",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/630",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1052": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1052",
+ "iid": "598",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:52Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint in anim",
+ "updatedAt": "2023-01-09T04:04:52Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/598",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/598",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1017": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1017",
+ "iid": "563",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:50Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint lorem sint",
+ "updatedAt": "2023-01-09T04:04:50Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/563",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/563",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1007": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1007",
+ "iid": "553",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:49Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ea non sint",
+ "updatedAt": "2023-01-09T04:04:49Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/553",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/553",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/988": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/988",
+ "iid": "534",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:47Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "minim ea sint",
+ "updatedAt": "2023-01-09T04:04:47Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/534",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/534",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/949": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/949",
+ "iid": "495",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:42Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "adipiscing sint ullamco",
+ "updatedAt": "2023-01-09T04:04:42Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/495",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/495",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/908": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/908",
+ "iid": "454",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:38Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sit dolore sint",
+ "updatedAt": "2023-01-09T04:04:38Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/454",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/454",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/852": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/852",
+ "iid": "398",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:32Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor adipiscing sint",
+ "updatedAt": "2023-01-09T04:04:32Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/398",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/398",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/842": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/842",
+ "iid": "388",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:31Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "exercitation consequat sint",
+ "updatedAt": "2023-01-09T04:04:31Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/388",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/388",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/782": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/782",
+ "iid": "328",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:23Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "eiusmod mollit sint",
+ "updatedAt": "2023-01-09T04:04:23Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/328",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/328",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/779": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/779",
+ "iid": "325",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:23Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sunt sint aute",
+ "updatedAt": "2023-01-09T04:04:23Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/325",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/325",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/769": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/769",
+ "iid": "315",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "aute et sint",
+ "updatedAt": "2023-01-09T04:04:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/315",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/315",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/718": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/718",
+ "iid": "264",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:15Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "quis sint in",
+ "updatedAt": "2023-01-09T04:04:15Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/264",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/264",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/634": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/634",
+ "iid": "180",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:05Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint in Duis",
+ "updatedAt": "2023-01-09T04:04:05Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/180",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/180",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/614": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/614",
+ "iid": "160",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:02Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ex magna sint",
+ "updatedAt": "2023-01-09T04:04:02Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/160",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/160",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/564": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/564",
+ "iid": "110",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "pariatur dolore sint",
+ "updatedAt": "2023-01-09T04:02:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/110",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/110",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/553": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/553",
+ "iid": "99",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:28Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor sint anim",
+ "updatedAt": "2023-01-09T04:02:28Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/99",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/99",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/542": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/542",
+ "iid": "88",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint eiusmod anim",
+ "updatedAt": "2023-01-09T04:02:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/88",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/88",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ }
+}
diff --git a/spec/frontend/lib/apollo/mock_data/non_persisted_cache.json b/spec/frontend/lib/apollo/mock_data/non_persisted_cache.json
new file mode 100644
index 00000000000..f30461f63db
--- /dev/null
+++ b/spec/frontend/lib/apollo/mock_data/non_persisted_cache.json
@@ -0,0 +1,3089 @@
+{
+ "Project:gid://gitlab/Project/6": {
+ "__typename": "Project",
+ "id": "gid://gitlab/Project/6",
+ "issues({\"includeSubepics\":true,\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 1115
+ },
+ "issues({\"includeSubepics\":true,\"state\":\"closed\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 16
+ },
+ "issues({\"includeSubepics\":true,\"state\":\"all\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 1131
+ },
+ "issues({\"after\":null,\"before\":\"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4yMzI5NDUwMDAgKzAwMDAiLCJpZCI6IjE1NjYifQ\",\"includeSubepics\":true,\"last\":20,\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": false,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0xMCAxMjozNjo1NC41NDYxNzEwMDAgKzAwMDAiLCJpZCI6IjQ4MyJ9",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4zMDE3NDcwMDAgKzAwMDAiLCJpZCI6IjE1NjcifQ"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/483"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1585"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1584"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1583"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1582"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1581"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1580"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1579"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1578"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1577"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1576"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1575"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1574"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1573"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1572"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1571"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1570"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1569"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1568"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1567"
+ }
+ ]
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 44
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"closed\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 0
+ },
+ "issues({\"includeSubepics\":true,\"search\":\"sint\",\"state\":\"all\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "count": 44
+ },
+ "issues({\"after\":null,\"before\":null,\"first\":20,\"includeSubepics\":true,\"search\":\"sint\",\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": false,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNTozMC4zMTM3NDMwMDAgKzAwMDAiLCJpZCI6IjE1ODQifQ",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1OC43NDI3NTkwMDAgKzAwMDAiLCJpZCI6IjExNDEifQ"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1584"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1540"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1532"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1515"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1514"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1463"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1461"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1439"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1403"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1399"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1375"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1349"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1333"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1321"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1318"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1299"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1268"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1262"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1254"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1141"
+ }
+ ]
+ },
+ "projectMembers({\"relations\":[\"DIRECT\",\"INHERITED\",\"INVITED_GROUPS\"],\"search\":\"\"})": {
+ "__typename": "MemberInterfaceConnection",
+ "nodes": [
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/54"
+ },
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/53"
+ },
+ {
+ "__ref": "ProjectMember:gid://gitlab/ProjectMember/52"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/26"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/25"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/11"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/10"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/9"
+ },
+ {
+ "__ref": "GroupMember:gid://gitlab/GroupMember/1"
+ }
+ ]
+ },
+ "milestones({\"includeAncestors\":true,\"searchTitle\":\"\",\"sort\":\"EXPIRED_LAST_DUE_DATE_ASC\",\"state\":\"active\"})": {
+ "__typename": "MilestoneConnection",
+ "nodes": [
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/30"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/28"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/27"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/26"
+ },
+ {
+ "__ref": "Milestone:gid://gitlab/Milestone/45"
+ }
+ ]
+ },
+ "labels({\"includeAncestorGroups\":true,\"searchTerm\":\"\"})": {
+ "__typename": "LabelConnection",
+ "nodes": [
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/99"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/41"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/48"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/46"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/50"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/44"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/96"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/45"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/95"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/49"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/98"
+ },
+ {
+ "__ref": "Label:gid://gitlab/ProjectLabel/97"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/47"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/42"
+ },
+ {
+ "__ref": "Label:gid://gitlab/GroupLabel/43"
+ }
+ ]
+ },
+ "issues({\"after\":\"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1OC43NDI3NTkwMDAgKzAwMDAiLCJpZCI6IjExNDEifQ\",\"before\":null,\"first\":20,\"includeSubepics\":true,\"search\":\"sint\",\"sort\":\"UPDATED_DESC\",\"state\":\"opened\",\"types\":[\"ISSUE\",\"INCIDENT\",\"TEST_CASE\",\"TASK\"]})": {
+ "__typename": "IssueConnection",
+ "pageInfo": {
+ "__typename": "PageInfo",
+ "hasNextPage": true,
+ "hasPreviousPage": true,
+ "startCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNDo1Ny42NTgwNTMwMDAgKzAwMDAiLCJpZCI6IjExMjMifQ",
+ "endCursor": "eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowMjoyNy42OTg0MDEwMDAgKzAwMDAiLCJpZCI6IjU0MiJ9"
+ },
+ "nodes": [
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1123"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1100"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1084"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1052"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1017"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/1007"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/988"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/949"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/908"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/852"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/842"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/782"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/779"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/769"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/718"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/634"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/614"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/564"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/553"
+ },
+ {
+ "__ref": "Issue:gid://gitlab/Issue/542"
+ }
+ ]
+ }
+ },
+ "ROOT_QUERY": {
+ "__typename": "Query",
+ "project({\"fullPath\":\"flightjs/Flight\"})": {
+ "__ref": "Project:gid://gitlab/Project/6"
+ }
+ },
+ "UserCore:gid://gitlab/User/1": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/1",
+ "avatarUrl": "https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon",
+ "name": "Administrator",
+ "username": "root",
+ "webUrl": "https://gdk.test:3443/root"
+ },
+ "Issue:gid://gitlab/Issue/483": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/483",
+ "iid": "31",
+ "confidential": false,
+ "createdAt": "2022-09-11T15:24:16Z",
+ "downvotes": 1,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 1,
+ "moved": false,
+ "state": "opened",
+ "title": "Instigate the Incident!",
+ "updatedAt": "2023-01-10T12:36:54Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 2,
+ "webPath": "/flightjs/Flight/-/issues/31",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/31",
+ "type": "INCIDENT",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1585": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1585",
+ "iid": "1131",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "proident aute commodo",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1131",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1131",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1584": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1584",
+ "iid": "1130",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint eiusmod eiusmod",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1130",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1130",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1583": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1583",
+ "iid": "1129",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "commodo mollit est",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1129",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1129",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1582": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1582",
+ "iid": "1128",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "non dolore laborum",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1128",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1128",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1581": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1581",
+ "iid": "1127",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "commodo do occaecat",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1127",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1127",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1580": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1580",
+ "iid": "1126",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ea nostrud ea",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1126",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1126",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1579": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1579",
+ "iid": "1125",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sed lorem fugiat",
+ "updatedAt": "2023-01-09T04:05:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1125",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1125",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1578": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1578",
+ "iid": "1124",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit anim sunt",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1124",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1124",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1577": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1577",
+ "iid": "1123",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "adipiscing fugiat ullamco",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1123",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1123",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1576": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1576",
+ "iid": "1122",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "pariatur et elit",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1122",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1122",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1575": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1575",
+ "iid": "1121",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ut ipsum occaecat",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1121",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1121",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1574": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1574",
+ "iid": "1120",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit ea elit",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1120",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1120",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1573": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1573",
+ "iid": "1119",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "nostrud voluptate do",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1119",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1119",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1572": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1572",
+ "iid": "1118",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ullamco consequat in",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1118",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1118",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1571": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1571",
+ "iid": "1117",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit Ut est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1117",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1117",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1570": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1570",
+ "iid": "1116",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "lorem commodo est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1116",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1116",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1569": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1569",
+ "iid": "1115",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "tempor irure laboris",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1115",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1115",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1568": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1568",
+ "iid": "1114",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "voluptate aliquip est",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1114",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1114",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1567": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1567",
+ "iid": "1113",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:29Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "exercitation dolore labore",
+ "updatedAt": "2023-01-09T04:05:29Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1113",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1113",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1540": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1540",
+ "iid": "1086",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint nulla dolore",
+ "updatedAt": "2023-01-09T04:05:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1086",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1086",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1532": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1532",
+ "iid": "1078",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "amet culpa sint",
+ "updatedAt": "2023-01-09T04:05:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1078",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1078",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1515": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1515",
+ "iid": "1061",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:26Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint Duis incididunt",
+ "updatedAt": "2023-01-09T04:05:26Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1061",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1061",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1514": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1514",
+ "iid": "1060",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:25Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint velit ullamco",
+ "updatedAt": "2023-01-09T04:05:25Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1060",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1060",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1463": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1463",
+ "iid": "1009",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor occaecat sint",
+ "updatedAt": "2023-01-09T04:05:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1009",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1009",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1461": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1461",
+ "iid": "1007",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "mollit sint irure",
+ "updatedAt": "2023-01-09T04:05:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/1007",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/1007",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1439": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1439",
+ "iid": "985",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:21Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint Ut amet",
+ "updatedAt": "2023-01-09T04:05:21Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/985",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/985",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1403": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1403",
+ "iid": "949",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:18Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "in consequat sint",
+ "updatedAt": "2023-01-09T04:05:18Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/949",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/949",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1399": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1399",
+ "iid": "945",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:18Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit nulla sint",
+ "updatedAt": "2023-01-09T04:05:18Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/945",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/945",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1375": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1375",
+ "iid": "921",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:16Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint sed ex",
+ "updatedAt": "2023-01-09T04:05:16Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/921",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/921",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1349": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1349",
+ "iid": "895",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:13Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "magna reprehenderit sint",
+ "updatedAt": "2023-01-09T04:05:13Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/895",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/895",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1333": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1333",
+ "iid": "879",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:11Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "tempor dolore sint",
+ "updatedAt": "2023-01-09T04:05:11Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/879",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/879",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1321": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1321",
+ "iid": "867",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:10Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "reprehenderit pariatur sint",
+ "updatedAt": "2023-01-09T04:05:10Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/867",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/867",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1318": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1318",
+ "iid": "864",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:10Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sit sint ad",
+ "updatedAt": "2023-01-09T04:05:10Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/864",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/864",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1299": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1299",
+ "iid": "845",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:08Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "velit sint fugiat",
+ "updatedAt": "2023-01-09T04:05:08Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/845",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/845",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1268": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1268",
+ "iid": "814",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:06Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor nostrud sint",
+ "updatedAt": "2023-01-09T04:05:06Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/814",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/814",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1262": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1262",
+ "iid": "808",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:06Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ut sint esse",
+ "updatedAt": "2023-01-09T04:05:06Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/808",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/808",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1254": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1254",
+ "iid": "800",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:05:05Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint ea est",
+ "updatedAt": "2023-01-09T04:05:05Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/800",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/800",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1141": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1141",
+ "iid": "687",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:58Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint quis laboris",
+ "updatedAt": "2023-01-09T04:04:58Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/687",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/687",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "UserCore:gid://gitlab/User/9": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/9",
+ "avatarUrl": "https://secure.gravatar.com/avatar/175e76e391370beeb21914ab74c2efd4?s=80&d=identicon",
+ "name": "Kiyoko Bahringer",
+ "username": "jamie"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/54": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/54",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/9"
+ }
+ },
+ "UserCore:gid://gitlab/User/19": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/19",
+ "avatarUrl": "https://secure.gravatar.com/avatar/3126153e3301ebf7cc8f7c99e57007f2?s=80&d=identicon",
+ "name": "Cecile Hermann",
+ "username": "jeannetta_breitenberg"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/53": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/53",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/19"
+ }
+ },
+ "UserCore:gid://gitlab/User/2": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/2",
+ "avatarUrl": "https://secure.gravatar.com/avatar/a138e401136c90561f949297387a3bb9?s=80&d=identicon",
+ "name": "Tish Treutel",
+ "username": "liana.larkin"
+ },
+ "ProjectMember:gid://gitlab/ProjectMember/52": {
+ "__typename": "ProjectMember",
+ "id": "gid://gitlab/ProjectMember/52",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/2"
+ }
+ },
+ "UserCore:gid://gitlab/User/13": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/13",
+ "avatarUrl": "https://secure.gravatar.com/avatar/0ce8057f452296a13b5620bb2d9ede57?s=80&d=identicon",
+ "name": "Tammy Gusikowski",
+ "username": "xuan_oreilly"
+ },
+ "GroupMember:gid://gitlab/GroupMember/26": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/26",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/13"
+ }
+ },
+ "UserCore:gid://gitlab/User/21": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/21",
+ "avatarUrl": "https://secure.gravatar.com/avatar/415b09d256f26403384363d7948c4d77?s=80&d=identicon",
+ "name": "Twanna Hegmann",
+ "username": "jamaal"
+ },
+ "GroupMember:gid://gitlab/GroupMember/25": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/25",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/21"
+ }
+ },
+ "UserCore:gid://gitlab/User/14": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/14",
+ "avatarUrl": "https://secure.gravatar.com/avatar/e99697c6664381b0351b7617717dd49b?s=80&d=identicon",
+ "name": "Francie Cole",
+ "username": "greg.wisoky"
+ },
+ "GroupMember:gid://gitlab/GroupMember/11": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/11",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/14"
+ }
+ },
+ "UserCore:gid://gitlab/User/7": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/7",
+ "avatarUrl": "https://secure.gravatar.com/avatar/3a382857e362d6cce60d3806dd173444?s=80&d=identicon",
+ "name": "Ivan Carter",
+ "username": "ethyl"
+ },
+ "GroupMember:gid://gitlab/GroupMember/10": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/10",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/7"
+ }
+ },
+ "UserCore:gid://gitlab/User/15": {
+ "__typename": "UserCore",
+ "id": "gid://gitlab/User/15",
+ "avatarUrl": "https://secure.gravatar.com/avatar/79653006ff557e081db02deaa4ca281c?s=80&d=identicon",
+ "name": "Danuta Dare",
+ "username": "maddie_hintz"
+ },
+ "GroupMember:gid://gitlab/GroupMember/9": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/9",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/15"
+ }
+ },
+ "GroupMember:gid://gitlab/GroupMember/1": {
+ "__typename": "GroupMember",
+ "id": "gid://gitlab/GroupMember/1",
+ "user": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ }
+ },
+ "Milestone:gid://gitlab/Milestone/30": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/30",
+ "title": "v4.0"
+ },
+ "Milestone:gid://gitlab/Milestone/28": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/28",
+ "title": "v2.0"
+ },
+ "Milestone:gid://gitlab/Milestone/27": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/27",
+ "title": "v1.0"
+ },
+ "Milestone:gid://gitlab/Milestone/26": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/26",
+ "title": "v0.0"
+ },
+ "Milestone:gid://gitlab/Milestone/45": {
+ "__typename": "Milestone",
+ "id": "gid://gitlab/Milestone/45",
+ "title": "Sprint - Autem id maxime consequatur quam."
+ },
+ "Label:gid://gitlab/ProjectLabel/99": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/99",
+ "color": "#a5c6fb",
+ "textColor": "#333333",
+ "title": "Accent"
+ },
+ "Label:gid://gitlab/GroupLabel/41": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/41",
+ "color": "#0609ba",
+ "textColor": "#FFFFFF",
+ "title": "Breckwood"
+ },
+ "Label:gid://gitlab/GroupLabel/48": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/48",
+ "color": "#fa7620",
+ "textColor": "#FFFFFF",
+ "title": "Brieph"
+ },
+ "Label:gid://gitlab/GroupLabel/46": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/46",
+ "color": "#d97020",
+ "textColor": "#FFFFFF",
+ "title": "Bryntfunc"
+ },
+ "Label:gid://gitlab/GroupLabel/50": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/50",
+ "color": "#8a934f",
+ "textColor": "#FFFFFF",
+ "title": "CL"
+ },
+ "Label:gid://gitlab/GroupLabel/44": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/44",
+ "color": "#9e1d53",
+ "textColor": "#FFFFFF",
+ "title": "Cofunc"
+ },
+ "Label:gid://gitlab/ProjectLabel/96": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/96",
+ "color": "#0384f3",
+ "textColor": "#FFFFFF",
+ "title": "Corolla"
+ },
+ "Label:gid://gitlab/GroupLabel/45": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/45",
+ "color": "#f0b448",
+ "textColor": "#FFFFFF",
+ "title": "Cygcell"
+ },
+ "Label:gid://gitlab/ProjectLabel/95": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/95",
+ "color": "#d13231",
+ "textColor": "#FFFFFF",
+ "title": "Freestyle"
+ },
+ "Label:gid://gitlab/GroupLabel/49": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/49",
+ "color": "#f43983",
+ "textColor": "#FFFFFF",
+ "title": "Genbalt"
+ },
+ "Label:gid://gitlab/ProjectLabel/98": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/98",
+ "color": "#247441",
+ "textColor": "#FFFFFF",
+ "title": "LaSabre"
+ },
+ "Label:gid://gitlab/ProjectLabel/97": {
+ "__typename": "Label",
+ "id": "gid://gitlab/ProjectLabel/97",
+ "color": "#3bd51a",
+ "textColor": "#FFFFFF",
+ "title": "Probe"
+ },
+ "Label:gid://gitlab/GroupLabel/47": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/47",
+ "color": "#6bfb9d",
+ "textColor": "#333333",
+ "title": "Techbalt"
+ },
+ "Label:gid://gitlab/GroupLabel/42": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/42",
+ "color": "#996016",
+ "textColor": "#FFFFFF",
+ "title": "Troffe"
+ },
+ "Label:gid://gitlab/GroupLabel/43": {
+ "__typename": "Label",
+ "id": "gid://gitlab/GroupLabel/43",
+ "color": "#a75c05",
+ "textColor": "#FFFFFF",
+ "title": "Tronceforge"
+ },
+ "Issue:gid://gitlab/Issue/1123": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1123",
+ "iid": "669",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:57Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "esse sint est",
+ "updatedAt": "2023-01-09T04:04:57Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/669",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/669",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1100": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1100",
+ "iid": "646",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:56Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "cupidatat sunt sint",
+ "updatedAt": "2023-01-09T04:04:56Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/646",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/646",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1084": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1084",
+ "iid": "630",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:54Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "culpa sint irure",
+ "updatedAt": "2023-01-09T04:04:54Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/630",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/630",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1052": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1052",
+ "iid": "598",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:52Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint in anim",
+ "updatedAt": "2023-01-09T04:04:52Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/598",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/598",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1017": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1017",
+ "iid": "563",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:50Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint lorem sint",
+ "updatedAt": "2023-01-09T04:04:50Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/563",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/563",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/1007": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/1007",
+ "iid": "553",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:49Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ea non sint",
+ "updatedAt": "2023-01-09T04:04:49Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/553",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/553",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/988": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/988",
+ "iid": "534",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:47Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "minim ea sint",
+ "updatedAt": "2023-01-09T04:04:47Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/534",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/534",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/949": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/949",
+ "iid": "495",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:42Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "adipiscing sint ullamco",
+ "updatedAt": "2023-01-09T04:04:42Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/495",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/495",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/908": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/908",
+ "iid": "454",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:38Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sit dolore sint",
+ "updatedAt": "2023-01-09T04:04:38Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/454",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/454",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/852": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/852",
+ "iid": "398",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:32Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor adipiscing sint",
+ "updatedAt": "2023-01-09T04:04:32Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/398",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/398",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/842": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/842",
+ "iid": "388",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:31Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "exercitation consequat sint",
+ "updatedAt": "2023-01-09T04:04:31Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/388",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/388",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/782": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/782",
+ "iid": "328",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:23Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "eiusmod mollit sint",
+ "updatedAt": "2023-01-09T04:04:23Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/328",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/328",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/779": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/779",
+ "iid": "325",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:23Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sunt sint aute",
+ "updatedAt": "2023-01-09T04:04:23Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/325",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/325",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/769": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/769",
+ "iid": "315",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:22Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "aute et sint",
+ "updatedAt": "2023-01-09T04:04:22Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/315",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/315",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/718": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/718",
+ "iid": "264",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:15Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "quis sint in",
+ "updatedAt": "2023-01-09T04:04:15Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/264",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/264",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/634": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/634",
+ "iid": "180",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:05Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint in Duis",
+ "updatedAt": "2023-01-09T04:04:05Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/180",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/180",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/614": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/614",
+ "iid": "160",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:04:02Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "ex magna sint",
+ "updatedAt": "2023-01-09T04:04:02Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/160",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/160",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/564": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/564",
+ "iid": "110",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:30Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "pariatur dolore sint",
+ "updatedAt": "2023-01-09T04:02:30Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/110",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/110",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/553": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/553",
+ "iid": "99",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:28Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "dolor sint anim",
+ "updatedAt": "2023-01-09T04:02:28Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/99",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/99",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ },
+ "Issue:gid://gitlab/Issue/542": {
+ "__typename": "Issue",
+ "id": "gid://gitlab/Issue/542",
+ "iid": "88",
+ "confidential": false,
+ "createdAt": "2023-01-09T04:02:27Z",
+ "downvotes": 0,
+ "dueDate": null,
+ "hidden": false,
+ "humanTimeEstimate": null,
+ "mergeRequestsCount": 0,
+ "moved": false,
+ "state": "opened",
+ "title": "sint eiusmod anim",
+ "updatedAt": "2023-01-09T04:02:27Z",
+ "closedAt": null,
+ "upvotes": 0,
+ "userDiscussionsCount": 0,
+ "webPath": "/flightjs/Flight/-/issues/88",
+ "webUrl": "https://gdk.test:3443/flightjs/Flight/-/issues/88",
+ "type": "ISSUE",
+ "assignees": {
+ "__typename": "UserCoreConnection",
+ "nodes": []
+ },
+ "author": {
+ "__ref": "UserCore:gid://gitlab/User/1"
+ },
+ "labels": {
+ "__typename": "LabelConnection",
+ "nodes": []
+ },
+ "milestone": null,
+ "taskCompletionStatus": {
+ "__typename": "TaskCompletionStatus",
+ "completedCount": 0,
+ "count": 0
+ },
+ "blockingCount": 0,
+ "healthStatus": null,
+ "weight": null
+ }
+}
diff --git a/spec/frontend/lib/apollo/persist_link_spec.js b/spec/frontend/lib/apollo/persist_link_spec.js
new file mode 100644
index 00000000000..ddb861bcee0
--- /dev/null
+++ b/spec/frontend/lib/apollo/persist_link_spec.js
@@ -0,0 +1,74 @@
+/* eslint-disable no-underscore-dangle */
+import { gql, execute, ApolloLink, Observable } from '@apollo/client/core';
+import { testApolloLink } from 'helpers/test_apollo_link';
+import { getPersistLink } from '~/lib/apollo/persist_link';
+
+const DEFAULT_QUERY = gql`
+ query {
+ foo {
+ bar
+ }
+ }
+`;
+
+const QUERY_WITH_DIRECTIVE = gql`
+ query {
+ foo @persist {
+ bar
+ }
+ }
+`;
+
+const QUERY_WITH_PERSIST_FIELD = gql`
+ query {
+ foo @persist {
+ bar
+ __persist
+ }
+ }
+`;
+
+const terminatingLink = new ApolloLink(() => Observable.of({ data: { foo: { bar: 1 } } }));
+
+describe('~/lib/apollo/persist_link', () => {
+ let subscription;
+
+ afterEach(() => {
+ if (subscription) {
+ subscription.unsubscribe();
+ }
+ });
+
+ it('removes `@persist` directive from the operation', async () => {
+ const operation = await testApolloLink(getPersistLink(), {}, QUERY_WITH_DIRECTIVE);
+ const { selections } = operation.query.definitions[0].selectionSet;
+
+ expect(selections[0].directives).toEqual([]);
+ });
+
+ it('removes `__persist` fields from the operation with `@persist` directive', async () => {
+ const operation = await testApolloLink(getPersistLink(), {}, QUERY_WITH_PERSIST_FIELD);
+
+ const { selections } = operation.query.definitions[0].selectionSet;
+ const childFields = selections[0].selectionSet.selections;
+
+ expect(childFields).toHaveLength(1);
+ expect(childFields.some((field) => field.name.value === '__persist')).toBe(false);
+ });
+
+ it('decorates the response with `__persist: true` is there is `__persist` field in the query', async () => {
+ const link = getPersistLink().concat(terminatingLink);
+
+ subscription = execute(link, { query: QUERY_WITH_PERSIST_FIELD }).subscribe(({ data }) => {
+ expect(data.foo.__persist).toBe(true);
+ });
+ });
+
+ it('does not decorate the response with `__persist: true` is there if query is not persistent', async () => {
+ const link = getPersistLink().concat(terminatingLink);
+
+ subscription = execute(link, { query: DEFAULT_QUERY }).subscribe(({ data }) => {
+ expect(data.foo.__persist).toBe(undefined);
+ });
+ });
+});
diff --git a/spec/frontend/lib/apollo/persistence_mapper_spec.js b/spec/frontend/lib/apollo/persistence_mapper_spec.js
new file mode 100644
index 00000000000..2efe28d2ca7
--- /dev/null
+++ b/spec/frontend/lib/apollo/persistence_mapper_spec.js
@@ -0,0 +1,163 @@
+import { persistenceMapper } from '~/lib/apollo/persistence_mapper';
+import NON_PERSISTED_CACHE from './mock_data/non_persisted_cache.json';
+import CACHE_WITH_PERSIST_DIRECTIVE from './mock_data/cache_with_persist_directive.json';
+import CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS from './mock_data/cache_with_persist_directive_and_field.json';
+
+describe('lib/apollo/persistence_mapper', () => {
+ it('returns only empty root query if `@persist` directive or `__persist` field is not present', async () => {
+ const persistedData = await persistenceMapper(JSON.stringify(NON_PERSISTED_CACHE));
+
+ expect(JSON.parse(persistedData)).toEqual({ ROOT_QUERY: { __typename: 'Query' } });
+ });
+
+ it('returns root query with one `project` field if only `@persist` directive is present', async () => {
+ const persistedData = await persistenceMapper(JSON.stringify(CACHE_WITH_PERSIST_DIRECTIVE));
+
+ expect(JSON.parse(persistedData)).toEqual({
+ ROOT_QUERY: {
+ __typename: 'Query',
+ 'project({"fullPath":"flightjs/Flight"}) @persist': {
+ __ref: 'Project:gid://gitlab/Project/6',
+ },
+ },
+ 'Project:gid://gitlab/Project/6': { __typename: 'Project', id: 'gid://gitlab/Project/6' },
+ });
+ });
+
+ it('returns root query nested fields that contain `__persist` field if `@persist` directive is present', async () => {
+ const persistedData = await persistenceMapper(
+ JSON.stringify(CACHE_WITH_PERSIST_DIRECTIVE_AND_FIELDS),
+ );
+
+ expect(JSON.parse(persistedData)).toEqual({
+ ROOT_QUERY: {
+ __typename: 'Query',
+ 'project({"fullPath":"flightjs/Flight"}) @persist': {
+ __ref: 'Project:gid://gitlab/Project/6',
+ },
+ },
+ 'Project:gid://gitlab/Project/6': {
+ __typename: 'Project',
+ id: 'gid://gitlab/Project/6',
+ 'issues({"after":null,"before":"eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4yMzI5NDUwMDAgKzAwMDAiLCJpZCI6IjE1NjYifQ","includeSubepics":true,"last":20,"sort":"UPDATED_DESC","state":"opened","types":["ISSUE","INCIDENT","TEST_CASE","TASK"]})': {
+ __typename: 'IssueConnection',
+ __persist: true,
+ pageInfo: {
+ __typename: 'PageInfo',
+ hasNextPage: true,
+ hasPreviousPage: false,
+ startCursor:
+ 'eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0xMCAxMjozNjo1NC41NDYxNzEwMDAgKzAwMDAiLCJpZCI6IjQ4MyJ9',
+ endCursor:
+ 'eyJ1cGRhdGVkX2F0IjoiMjAyMy0wMS0wOSAwNDowNToyOS4zMDE3NDcwMDAgKzAwMDAiLCJpZCI6IjE1NjcifQ',
+ },
+ nodes: [
+ {
+ __ref: 'Issue:gid://gitlab/Issue/483',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1585',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1584',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1583',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1582',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1581',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1580',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1579',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1578',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1577',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1576',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1575',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1574',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1573',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1572',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1571',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1570',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1569',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1568',
+ },
+ {
+ __ref: 'Issue:gid://gitlab/Issue/1567',
+ },
+ ],
+ },
+ },
+ 'Issue:gid://gitlab/Issue/483': {
+ __typename: 'Issue',
+ __persist: true,
+ id: 'gid://gitlab/Issue/483',
+ iid: '31',
+ confidential: false,
+ createdAt: '2022-09-11T15:24:16Z',
+ downvotes: 1,
+ dueDate: null,
+ hidden: false,
+ humanTimeEstimate: null,
+ mergeRequestsCount: 1,
+ moved: false,
+ state: 'opened',
+ title: 'Instigate the Incident!',
+ updatedAt: '2023-01-10T12:36:54Z',
+ closedAt: null,
+ upvotes: 0,
+ userDiscussionsCount: 2,
+ webPath: '/flightjs/Flight/-/issues/31',
+ webUrl: 'https://gdk.test:3443/flightjs/Flight/-/issues/31',
+ type: 'INCIDENT',
+ assignees: {
+ __typename: 'UserCoreConnection',
+ nodes: [],
+ },
+ author: {
+ __ref: 'UserCore:gid://gitlab/User/1',
+ },
+ labels: {
+ __typename: 'LabelConnection',
+ nodes: [],
+ },
+ milestone: null,
+ taskCompletionStatus: {
+ __typename: 'TaskCompletionStatus',
+ completedCount: 0,
+ count: 0,
+ },
+ blockingCount: 0,
+ healthStatus: null,
+ weight: null,
+ },
+ });
+ });
+});
diff --git a/spec/frontend/lib/utils/ajax_cache_spec.js b/spec/frontend/lib/utils/ajax_cache_spec.js
index d4b95172d18..338302642ff 100644
--- a/spec/frontend/lib/utils/ajax_cache_spec.js
+++ b/spec/frontend/lib/utils/ajax_cache_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import AjaxCache from '~/lib/utils/ajax_cache';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('AjaxCache', () => {
const dummyEndpoint = '/AjaxCache/dummyEndpoint';
@@ -102,7 +103,7 @@ describe('AjaxCache', () => {
});
it('stores and returns data from Ajax call if cache is empty', () => {
- mock.onGet(dummyEndpoint).reply(200, dummyResponse);
+ mock.onGet(dummyEndpoint).reply(HTTP_STATUS_OK, dummyResponse);
return AjaxCache.retrieve(dummyEndpoint).then((data) => {
expect(data).toEqual(dummyResponse);
@@ -111,7 +112,7 @@ describe('AjaxCache', () => {
});
it('makes no Ajax call if request is pending', () => {
- mock.onGet(dummyEndpoint).reply(200, dummyResponse);
+ mock.onGet(dummyEndpoint).reply(HTTP_STATUS_OK, dummyResponse);
return Promise.all([
AjaxCache.retrieve(dummyEndpoint),
@@ -148,7 +149,7 @@ describe('AjaxCache', () => {
AjaxCache.internalStorage[dummyEndpoint] = oldDummyResponse;
- mock.onGet(dummyEndpoint).reply(200, dummyResponse);
+ mock.onGet(dummyEndpoint).reply(HTTP_STATUS_OK, dummyResponse);
return Promise.all([
AjaxCache.retrieve(dummyEndpoint),
diff --git a/spec/frontend/lib/utils/apollo_startup_js_link_spec.js b/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
index b972f669ac4..78eef205b49 100644
--- a/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
+++ b/spec/frontend/lib/utils/apollo_startup_js_link_spec.js
@@ -1,5 +1,6 @@
import { ApolloLink, Observable } from '@apollo/client/core';
import { StartupJSLink } from '~/lib/utils/apollo_startup_js_link';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('StartupJSLink', () => {
const FORWARDED_RESPONSE = { data: 'FORWARDED_RESPONSE' };
@@ -37,7 +38,7 @@ describe('StartupJSLink', () => {
let startupLink;
let link;
- function mockFetchCall(status = 200, response = STARTUP_JS_RESPONSE) {
+ function mockFetchCall(status = HTTP_STATUS_OK, response = STARTUP_JS_RESPONSE) {
const p = {
ok: status >= 200 && status < 300,
status,
@@ -175,7 +176,7 @@ describe('StartupJSLink', () => {
window.gl = {
startup_graphql_calls: [
{
- fetchCall: mockFetchCall(404),
+ fetchCall: mockFetchCall(HTTP_STATUS_NOT_FOUND),
query: STARTUP_JS_QUERY,
variables: { id: 3 },
},
@@ -209,7 +210,7 @@ describe('StartupJSLink', () => {
window.gl = {
startup_graphql_calls: [
{
- fetchCall: mockFetchCall(200, ERROR_RESPONSE),
+ fetchCall: mockFetchCall(HTTP_STATUS_OK, ERROR_RESPONSE),
query: STARTUP_JS_QUERY,
variables: { id: 3 },
},
@@ -226,7 +227,7 @@ describe('StartupJSLink', () => {
window.gl = {
startup_graphql_calls: [
{
- fetchCall: mockFetchCall(200, { 'no-data': 'yay' }),
+ fetchCall: mockFetchCall(HTTP_STATUS_OK, { 'no-data': 'yay' }),
query: STARTUP_JS_QUERY,
variables: { id: 3 },
},
@@ -339,7 +340,7 @@ describe('StartupJSLink', () => {
variables: { id: 3 },
},
{
- fetchCall: mockFetchCall(200, STARTUP_JS_RESPONSE_TWO),
+ fetchCall: mockFetchCall(HTTP_STATUS_OK, STARTUP_JS_RESPONSE_TWO),
query: STARTUP_JS_QUERY_TWO,
variables: { id: 3 },
},
diff --git a/spec/frontend/lib/utils/axios_startup_calls_spec.js b/spec/frontend/lib/utils/axios_startup_calls_spec.js
index e12bf725560..4471b781446 100644
--- a/spec/frontend/lib/utils/axios_startup_calls_spec.js
+++ b/spec/frontend/lib/utils/axios_startup_calls_spec.js
@@ -1,6 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import setupAxiosStartupCalls from '~/lib/utils/axios_startup_calls';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('setupAxiosStartupCalls', () => {
const AXIOS_RESPONSE = { text: 'AXIOS_RESPONSE' };
@@ -31,9 +32,9 @@ describe('setupAxiosStartupCalls', () => {
beforeEach(() => {
window.gl = {};
mock = new MockAdapter(axios);
- mock.onGet('/non-startup').reply(200, AXIOS_RESPONSE);
- mock.onGet('/startup').reply(200, AXIOS_RESPONSE);
- mock.onGet('/startup-failing').reply(200, AXIOS_RESPONSE);
+ mock.onGet('/non-startup').reply(HTTP_STATUS_OK, AXIOS_RESPONSE);
+ mock.onGet('/startup').reply(HTTP_STATUS_OK, AXIOS_RESPONSE);
+ mock.onGet('/startup-failing').reply(HTTP_STATUS_OK, AXIOS_RESPONSE);
});
afterEach(() => {
@@ -52,10 +53,10 @@ describe('setupAxiosStartupCalls', () => {
beforeEach(() => {
window.gl.startup_calls = {
'/startup': {
- fetchCall: mockFetchCall(200),
+ fetchCall: mockFetchCall(HTTP_STATUS_OK),
},
'/startup-failing': {
- fetchCall: mockFetchCall(400),
+ fetchCall: mockFetchCall(HTTP_STATUS_BAD_REQUEST),
},
};
setupAxiosStartupCalls(axios);
@@ -80,7 +81,7 @@ describe('setupAxiosStartupCalls', () => {
const { headers, data, status, statusText } = await axios.get('/startup');
expect(headers).toEqual({ 'content-type': 'application/json' });
- expect(status).toBe(200);
+ expect(status).toBe(HTTP_STATUS_OK);
expect(statusText).toBe('MOCK-FETCH 200');
expect(data).toEqual(STARTUP_JS_RESPONSE);
expect(data).not.toEqual(AXIOS_RESPONSE);
@@ -126,7 +127,7 @@ describe('setupAxiosStartupCalls', () => {
it('removes GitLab Base URL from startup call', async () => {
window.gl.startup_calls = {
'/startup': {
- fetchCall: mockFetchCall(200),
+ fetchCall: mockFetchCall(HTTP_STATUS_OK),
},
};
setupAxiosStartupCalls(axios);
@@ -139,7 +140,7 @@ describe('setupAxiosStartupCalls', () => {
it('sorts the params in the requested API url', async () => {
window.gl.startup_calls = {
'/startup?alpha=true&bravo=true': {
- fetchCall: mockFetchCall(200),
+ fetchCall: mockFetchCall(HTTP_STATUS_OK),
},
};
setupAxiosStartupCalls(axios);
diff --git a/spec/frontend/lib/utils/axios_utils_spec.js b/spec/frontend/lib/utils/axios_utils_spec.js
index 1585a38ae86..2656fb1d648 100644
--- a/spec/frontend/lib/utils/axios_utils_spec.js
+++ b/spec/frontend/lib/utils/axios_utils_spec.js
@@ -3,14 +3,15 @@
import AxiosMockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('axios_utils', () => {
let mock;
beforeEach(() => {
mock = new AxiosMockAdapter(axios);
- mock.onAny('/ok').reply(200);
- mock.onAny('/err').reply(500);
+ mock.onAny('/ok').reply(HTTP_STATUS_OK);
+ mock.onAny('/err').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
// eslint-disable-next-line jest/no-standalone-expect
expect(axios.countActiveRequests()).toBe(0);
});
@@ -27,8 +28,8 @@ describe('axios_utils', () => {
return axios.waitForAll().finally(() => {
expect(handler).toHaveBeenCalledTimes(2);
- expect(handler.mock.calls[0][0].status).toBe(200);
- expect(handler.mock.calls[1][0].response.status).toBe(500);
+ expect(handler.mock.calls[0][0].status).toBe(HTTP_STATUS_OK);
+ expect(handler.mock.calls[1][0].response.status).toBe(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
});
});
diff --git a/spec/frontend/lib/utils/common_utils_spec.js b/spec/frontend/lib/utils/common_utils_spec.js
index 08ba78cddff..7b068f7d248 100644
--- a/spec/frontend/lib/utils/common_utils_spec.js
+++ b/spec/frontend/lib/utils/common_utils_spec.js
@@ -1,5 +1,4 @@
import * as commonUtils from '~/lib/utils/common_utils';
-import setWindowLocation from 'helpers/set_window_location_helper';
describe('common_utils', () => {
describe('getPagePath', () => {
@@ -623,6 +622,23 @@ describe('common_utils', () => {
milestones: ['12.3', '12.4'],
},
},
+ convertObjectPropsToLowerCase: {
+ obj: {
+ 'Project-Name': 'GitLab CE',
+ 'Group-Name': 'GitLab.org',
+ 'License-Type': 'MIT',
+ 'Mile-Stones': ['12.3', '12.4'],
+ },
+ objNested: {
+ 'Project-Name': 'GitLab CE',
+ 'Group-Name': 'GitLab.org',
+ 'License-Type': 'MIT',
+ 'Tech-Stack': {
+ 'Frontend-Framework': 'Vue',
+ },
+ 'Mile-Stones': ['12.3', '12.4'],
+ },
+ },
};
describe('convertObjectProps', () => {
@@ -638,6 +654,7 @@ describe('common_utils', () => {
${'convertObjectProps'} | ${mockObjects.convertObjectProps.obj} | ${mockObjects.convertObjectProps.objNested}
${'convertObjectPropsToCamelCase'} | ${mockObjects.convertObjectPropsToCamelCase.obj} | ${mockObjects.convertObjectPropsToCamelCase.objNested}
${'convertObjectPropsToSnakeCase'} | ${mockObjects.convertObjectPropsToSnakeCase.obj} | ${mockObjects.convertObjectPropsToSnakeCase.objNested}
+ ${'convertObjectPropsToLowerCase'} | ${mockObjects.convertObjectPropsToLowerCase.obj} | ${mockObjects.convertObjectPropsToLowerCase.objNested}
`('$functionName', ({ functionName, mockObj, mockObjNested }) => {
const testFunction =
functionName === 'convertObjectProps'
@@ -671,6 +688,12 @@ describe('common_utils', () => {
absolute_web_url: 'https://gitlab.com/gitlab-org/',
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'group-name': 'GitLab.org',
+ 'license-type': 'MIT',
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
expect(testFunction(mockObj)).toEqual(expected[functionName]);
@@ -711,6 +734,15 @@ describe('common_utils', () => {
},
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'group-name': 'GitLab.org',
+ 'license-type': 'MIT',
+ 'tech-stack': {
+ 'Frontend-Framework': 'Vue',
+ },
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
expect(testFunction(mockObjNested)).toEqual(expected[functionName]);
@@ -752,6 +784,15 @@ describe('common_utils', () => {
},
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'group-name': 'GitLab.org',
+ 'license-type': 'MIT',
+ 'tech-stack': {
+ 'frontend-framework': 'Vue',
+ },
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
it('converts nested objects', () => {
@@ -802,6 +843,15 @@ describe('common_utils', () => {
},
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'group-name': 'GitLab.org',
+ 'license-type': 'MIT',
+ 'tech-stack': {
+ 'Frontend-Framework': 'Vue',
+ },
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
const dropKeys = {
@@ -846,12 +896,20 @@ describe('common_utils', () => {
},
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'tech-stack': {
+ 'frontend-framework': 'Vue',
+ },
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
const dropKeys = {
convertObjectProps: ['group_name', 'database'],
convertObjectPropsToCamelCase: ['group_name', 'database'],
convertObjectPropsToSnakeCase: ['groupName', 'database'],
+ convertObjectPropsToLowerCase: ['Group-Name', 'License-Type'],
};
expect(
@@ -899,12 +957,22 @@ describe('common_utils', () => {
},
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'Group-Name': 'GitLab.org',
+ 'license-type': 'MIT',
+ 'tech-stack': {
+ 'Frontend-Framework': 'Vue',
+ },
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
const ignoreKeyNames = {
convertObjectProps: ['group_name'],
convertObjectPropsToCamelCase: ['group_name'],
convertObjectPropsToSnakeCase: ['groupName'],
+ convertObjectPropsToLowerCase: ['Group-Name'],
};
expect(
@@ -949,12 +1017,22 @@ describe('common_utils', () => {
},
milestones: ['12.3', '12.4'],
},
+ convertObjectPropsToLowerCase: {
+ 'project-name': 'GitLab CE',
+ 'group-name': 'GitLab.org',
+ 'license-type': 'MIT',
+ 'tech-stack': {
+ 'Frontend-Framework': 'Vue',
+ },
+ 'mile-stones': ['12.3', '12.4'],
+ },
};
const ignoreKeyNames = {
convertObjectProps: ['group_name', 'frontend_framework'],
convertObjectPropsToCamelCase: ['group_name', 'frontend_framework'],
convertObjectPropsToSnakeCase: ['groupName', 'frontendFramework'],
+ convertObjectPropsToLowerCase: ['Frontend-Framework'],
};
expect(
@@ -1070,35 +1148,4 @@ describe('common_utils', () => {
expect(result).toEqual([{ hello: '' }, { helloWorld: '' }]);
});
});
-
- describe('useNewFonts', () => {
- let beforeGon;
- const beforeLocation = window.location.href;
-
- beforeEach(() => {
- window.gon = window.gon || {};
- beforeGon = { ...window.gon };
- });
-
- describe.each`
- featureFlag | queryParameter | fontEnabled
- ${false} | ${false} | ${false}
- ${true} | ${false} | ${true}
- ${false} | ${true} | ${true}
- `('new font', ({ featureFlag, queryParameter, fontEnabled }) => {
- it(`will ${fontEnabled ? '' : 'NOT '}be applied when feature flag is ${
- featureFlag ? '' : 'NOT '
- }set and query parameter is ${queryParameter ? '' : 'NOT '}present`, () => {
- const search = queryParameter ? `?new_fonts` : '';
- setWindowLocation(search);
- window.gon = { features: { newFonts: featureFlag } };
- expect(commonUtils.useNewFonts()).toBe(fontEnabled);
- });
- });
-
- afterEach(() => {
- window.gon = beforeGon;
- setWindowLocation(beforeLocation);
- });
- });
});
diff --git a/spec/frontend/lib/utils/favicon_ci_spec.js b/spec/frontend/lib/utils/favicon_ci_spec.js
index e35b008b862..be647d98f1a 100644
--- a/spec/frontend/lib/utils/favicon_ci_spec.js
+++ b/spec/frontend/lib/utils/favicon_ci_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import { setFaviconOverlay, resetFavicon } from '~/lib/utils/favicon';
import { setCiStatusFavicon } from '~/lib/utils/favicon_ci';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/lib/utils/favicon');
@@ -28,7 +29,7 @@ describe('~/lib/utils/favicon_ci', () => {
`(
'with response=$response',
async ({ response, setFaviconOverlayCalls, resetFaviconCalls }) => {
- mock.onGet(TEST_URL).replyOnce(200, response);
+ mock.onGet(TEST_URL).replyOnce(HTTP_STATUS_OK, response);
expect(setFaviconOverlay).not.toHaveBeenCalled();
expect(resetFavicon).not.toHaveBeenCalled();
@@ -41,7 +42,7 @@ describe('~/lib/utils/favicon_ci', () => {
);
it('with error', async () => {
- mock.onGet(TEST_URL).replyOnce(500);
+ mock.onGet(TEST_URL).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await expect(setCiStatusFavicon(TEST_URL)).rejects.toEqual(expect.any(Error));
expect(resetFavicon).toHaveBeenCalled();
diff --git a/spec/frontend/lib/utils/icon_utils_spec.js b/spec/frontend/lib/utils/icon_utils_spec.js
index db1f174703b..59839862504 100644
--- a/spec/frontend/lib/utils/icon_utils_spec.js
+++ b/spec/frontend/lib/utils/icon_utils_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { clearSvgIconPathContentCache, getSvgIconPathContent } from '~/lib/utils/icon_utils';
describe('Icon utils', () => {
@@ -30,7 +31,7 @@ describe('Icon utils', () => {
describe('when the icons can be loaded', () => {
beforeEach(() => {
- axiosMock.onGet(gon.sprite_icons).reply(200, mockIcons);
+ axiosMock.onGet(gon.sprite_icons).reply(HTTP_STATUS_OK, mockIcons);
});
it('extracts svg icon path content from sprite icons', () => {
@@ -50,11 +51,11 @@ describe('Icon utils', () => {
beforeEach(() => {
axiosMock
.onGet(gon.sprite_icons)
- .replyOnce(500)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR)
.onGet(gon.sprite_icons)
- .replyOnce(500)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR)
.onGet(gon.sprite_icons)
- .reply(200, mockIcons);
+ .reply(HTTP_STATUS_OK, mockIcons);
});
it('returns null', () => {
diff --git a/spec/frontend/lib/utils/poll_spec.js b/spec/frontend/lib/utils/poll_spec.js
index 94a5f5385b7..63eeb54e850 100644
--- a/spec/frontend/lib/utils/poll_spec.js
+++ b/spec/frontend/lib/utils/poll_spec.js
@@ -1,5 +1,9 @@
import waitForPromises from 'helpers/wait_for_promises';
-import { successCodes } from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_OK,
+ successCodes,
+} from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
describe('Poll', () => {
@@ -51,7 +55,7 @@ describe('Poll', () => {
});
it('calls the success callback when no header for interval is provided', () => {
- mockServiceCall({ status: 200 });
+ mockServiceCall({ status: HTTP_STATUS_OK });
setup();
return waitForAllCallsToFinish(1, () => {
@@ -61,7 +65,7 @@ describe('Poll', () => {
});
it('calls the error callback when the http request returns an error', () => {
- mockServiceCall({ status: 500 }, true);
+ mockServiceCall({ status: HTTP_STATUS_INTERNAL_SERVER_ERROR }, true);
setup();
return waitForAllCallsToFinish(1, () => {
@@ -82,7 +86,7 @@ describe('Poll', () => {
});
it('should call the success callback when the interval header is -1', () => {
- mockServiceCall({ status: 200, headers: { 'poll-interval': -1 } });
+ mockServiceCall({ status: HTTP_STATUS_OK, headers: { 'poll-interval': -1 } });
return setup().then(() => {
expect(callbacks.success).toHaveBeenCalled();
expect(callbacks.error).not.toHaveBeenCalled();
@@ -118,7 +122,7 @@ describe('Poll', () => {
describe('with delayed initial request', () => {
it('delays the first request', async () => {
- mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
+ mockServiceCall({ status: HTTP_STATUS_OK, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
resource: service,
@@ -147,7 +151,7 @@ describe('Poll', () => {
describe('stop', () => {
it('stops polling when method is called', () => {
- mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
+ mockServiceCall({ status: HTTP_STATUS_OK, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
resource: service,
@@ -173,7 +177,7 @@ describe('Poll', () => {
describe('enable', () => {
it('should enable polling upon a response', () => {
- mockServiceCall({ status: 200 });
+ mockServiceCall({ status: HTTP_STATUS_OK });
const Polling = new Poll({
resource: service,
method: 'fetch',
@@ -183,7 +187,7 @@ describe('Poll', () => {
Polling.enable({
data: { page: 4 },
- response: { status: 200, headers: { 'poll-interval': 1 } },
+ response: { status: HTTP_STATUS_OK, headers: { 'poll-interval': 1 } },
});
return waitForAllCallsToFinish(1, () => {
@@ -198,7 +202,7 @@ describe('Poll', () => {
describe('restart', () => {
it('should restart polling when its called', () => {
- mockServiceCall({ status: 200, headers: { 'poll-interval': 1 } });
+ mockServiceCall({ status: HTTP_STATUS_OK, headers: { 'poll-interval': 1 } });
const Polling = new Poll({
resource: service,
diff --git a/spec/frontend/lib/utils/rails_ujs_spec.js b/spec/frontend/lib/utils/rails_ujs_spec.js
index da9cc5c6f3c..8ca4dfc9340 100644
--- a/spec/frontend/lib/utils/rails_ujs_spec.js
+++ b/spec/frontend/lib/utils/rails_ujs_spec.js
@@ -1,5 +1,6 @@
import { setHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
beforeAll(async () => {
// @rails/ujs expects jQuery.ajaxPrefilter to exist if jQuery exists at
@@ -20,7 +21,7 @@ function mockXHRResponse({ responseText, responseContentType } = {}) {
jest.spyOn(global.XMLHttpRequest.prototype, 'send').mockImplementation(function send() {
Object.defineProperties(this, {
readyState: { value: XMLHttpRequest.DONE },
- status: { value: 200 },
+ status: { value: HTTP_STATUS_OK },
response: { value: responseText },
});
this.onreadystatechange();
diff --git a/spec/frontend/lib/utils/scroll_utils_spec.js b/spec/frontend/lib/utils/scroll_utils_spec.js
new file mode 100644
index 00000000000..d42e25b929c
--- /dev/null
+++ b/spec/frontend/lib/utils/scroll_utils_spec.js
@@ -0,0 +1,21 @@
+import { isScrolledToBottom } from '~/lib/utils/scroll_utils';
+
+describe('isScrolledToBottom', () => {
+ const setScrollGetters = (getters) => {
+ Object.entries(getters).forEach(([name, value]) => {
+ jest.spyOn(Element.prototype, name, 'get').mockReturnValue(value);
+ });
+ };
+
+ it.each`
+ context | scrollTop | scrollHeight | result
+ ${'returns false when not scrolled to bottom'} | ${0} | ${2000} | ${false}
+ ${'returns true when scrolled to bottom'} | ${1000} | ${2000} | ${true}
+ ${'returns true when scrolled to bottom with subpixel precision'} | ${999.25} | ${2000} | ${true}
+ ${'returns true when cannot scroll'} | ${0} | ${500} | ${true}
+ `('$context', ({ scrollTop, scrollHeight, result }) => {
+ setScrollGetters({ scrollTop, clientHeight: 1000, scrollHeight });
+
+ expect(isScrolledToBottom()).toBe(result);
+ });
+});
diff --git a/spec/frontend/lib/utils/select2_utils_spec.js b/spec/frontend/lib/utils/select2_utils_spec.js
deleted file mode 100644
index 6d601dd5ad1..00000000000
--- a/spec/frontend/lib/utils/select2_utils_spec.js
+++ /dev/null
@@ -1,100 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import $ from 'jquery';
-import { setHTMLFixture } from 'helpers/fixtures';
-import waitForPromises from 'helpers/wait_for_promises';
-import axios from '~/lib/utils/axios_utils';
-import { select2AxiosTransport } from '~/lib/utils/select2_utils';
-
-import 'select2/select2';
-
-const TEST_URL = '/test/api/url';
-const TEST_SEARCH_DATA = { extraSearch: 'test' };
-const TEST_DATA = [{ id: 1 }];
-const TEST_SEARCH = 'FOO';
-
-describe('lib/utils/select2_utils', () => {
- let mock;
- let resultsSpy;
-
- beforeEach(() => {
- setHTMLFixture('<div><input id="root" /></div>');
-
- mock = new MockAdapter(axios);
-
- resultsSpy = jest.fn().mockReturnValue({ results: [] });
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- const setupSelect2 = (input) => {
- input.select2({
- ajax: {
- url: TEST_URL,
- quietMillis: 250,
- transport: select2AxiosTransport,
- data(search, page) {
- return {
- search,
- page,
- ...TEST_SEARCH_DATA,
- };
- },
- results: resultsSpy,
- },
- });
- };
-
- const setupSelect2AndSearch = async () => {
- const $input = $('#root');
-
- setupSelect2($input);
-
- $input.select2('search', TEST_SEARCH);
-
- jest.runOnlyPendingTimers();
- await waitForPromises();
- };
-
- describe('select2AxiosTransport', () => {
- it('uses axios to make request', async () => {
- // setup mock response
- const replySpy = jest.fn();
- mock.onGet(TEST_URL).reply((...args) => replySpy(...args));
-
- await setupSelect2AndSearch();
-
- expect(replySpy).toHaveBeenCalledWith(
- expect.objectContaining({
- url: TEST_URL,
- method: 'get',
- params: {
- page: 1,
- search: TEST_SEARCH,
- ...TEST_SEARCH_DATA,
- },
- }),
- );
- });
-
- it.each`
- headers | pagination
- ${{}} | ${{ more: false }}
- ${{ 'X-PAGE': '1', 'x-next-page': 2 }} | ${{ more: true }}
- `(
- 'passes results and pagination to results callback, with headers=$headers',
- async ({ headers, pagination }) => {
- mock.onGet(TEST_URL).reply(200, TEST_DATA, headers);
-
- await setupSelect2AndSearch();
-
- expect(resultsSpy).toHaveBeenCalledWith(
- { results: TEST_DATA, pagination },
- 1,
- expect.anything(),
- );
- },
- );
- });
-});
diff --git a/spec/frontend/lib/utils/text_markdown_spec.js b/spec/frontend/lib/utils/text_markdown_spec.js
index 9fbb3d0a660..7aab1013fc0 100644
--- a/spec/frontend/lib/utils/text_markdown_spec.js
+++ b/spec/frontend/lib/utils/text_markdown_spec.js
@@ -192,9 +192,10 @@ describe('init markdown', () => {
});
describe('Continuing markdown lists', () => {
- const enterEvent = new KeyboardEvent('keydown', { key: 'Enter' });
+ let enterEvent;
beforeEach(() => {
+ enterEvent = new KeyboardEvent('keydown', { key: 'Enter', cancelable: true });
textArea.addEventListener('keydown', keypressNoteText);
textArea.addEventListener('compositionstart', compositionStartNoteText);
textArea.addEventListener('compositionend', compositionEndNoteText);
@@ -256,7 +257,7 @@ describe('init markdown', () => {
${'108. item\n109. '} | ${'108. item\n'}
${'108. item\n - second\n - '} | ${'108. item\n - second\n'}
${'108. item\n 1. second\n 1. '} | ${'108. item\n 1. second\n'}
- `('adds correct list continuation characters', ({ text, expected }) => {
+ `('remove list continuation characters', ({ text, expected }) => {
textArea.value = text;
textArea.setSelectionRange(text.length, text.length);
@@ -300,6 +301,37 @@ describe('init markdown', () => {
},
);
+ // test that when pressing Enter in the prefix area of a list item,
+ // such as between `2.`, we simply propagate the Enter,
+ // adding a newline. Since the event doesn't actually get propagated
+ // in the test, check that `defaultPrevented` is false
+ it.each`
+ text | add_at | prevented
+ ${'- one\n- two\n- three'} | ${6} | ${false}
+ ${'- one\n- two\n- three'} | ${7} | ${false}
+ ${'- one\n- two\n- three'} | ${8} | ${true}
+ ${'- [ ] one\n- [ ] two\n- [ ] three'} | ${10} | ${false}
+ ${'- [ ] one\n- [ ] two\n- [ ] three'} | ${15} | ${false}
+ ${'- [ ] one\n- [ ] two\n- [ ] three'} | ${16} | ${true}
+ ${'- [ ] one\n - [ ] two\n- [ ] three'} | ${10} | ${false}
+ ${'- [ ] one\n - [ ] two\n- [ ] three'} | ${11} | ${false}
+ ${'- [ ] one\n - [ ] two\n- [ ] three'} | ${17} | ${false}
+ ${'- [ ] one\n - [ ] two\n- [ ] three'} | ${18} | ${true}
+ ${'1. one\n2. two\n3. three'} | ${7} | ${false}
+ ${'1. one\n2. two\n3. three'} | ${9} | ${false}
+ ${'1. one\n2. two\n3. three'} | ${10} | ${true}
+ `(
+ 'allows a newline to be added if cursor is inside the list marker prefix area',
+ ({ text, add_at, prevented }) => {
+ textArea.value = text;
+ textArea.setSelectionRange(add_at, add_at);
+
+ textArea.dispatchEvent(enterEvent);
+
+ expect(enterEvent.defaultPrevented).toBe(prevented);
+ },
+ );
+
it('does not duplicate a line item for IME characters', () => {
const text = '- 日本語';
const expected = '- 日本語\n- ';
diff --git a/spec/frontend/listbox/index_spec.js b/spec/frontend/listbox/index_spec.js
index 0816152f4e3..39e0332631b 100644
--- a/spec/frontend/listbox/index_spec.js
+++ b/spec/frontend/listbox/index_spec.js
@@ -96,8 +96,8 @@ describe('initListbox', () => {
});
});
- it('passes the "right" prop through to the underlying component', () => {
- expect(listbox().props('right')).toBe(parsedAttributes.right);
+ it('passes the "placement" prop through to the underlying component', () => {
+ expect(listbox().props('placement')).toBe(parsedAttributes.placement);
});
});
});
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 402a5e9db27..95db30a3683 100644
--- a/spec/frontend/members/components/table/member_action_buttons_spec.js
+++ b/spec/frontend/members/components/table/member_action_buttons_spec.js
@@ -3,15 +3,15 @@ import AccessRequestActionButtons from '~/members/components/action_buttons/acce
import GroupActionButtons from '~/members/components/action_buttons/group_action_buttons.vue';
import InviteActionButtons from '~/members/components/action_buttons/invite_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 MemberActions from '~/members/components/table/member_actions.vue';
import { MEMBER_TYPES } from '~/members/constants';
import { member as memberMock, group, invite, accessRequest } from '../../mock_data';
-describe('MemberActionButtons', () => {
+describe('MemberActions', () => {
let wrapper;
const createComponent = (propsData = {}) => {
- wrapper = shallowMount(MemberActionButtons, {
+ wrapper = shallowMount(MemberActions, {
propsData: {
isCurrentUser: false,
isInvitedUser: false,
diff --git a/spec/frontend/members/components/table/members_table_spec.js b/spec/frontend/members/components/table/members_table_spec.js
index 1d18026a410..b8e0d73d8f6 100644
--- a/spec/frontend/members/components/table/members_table_spec.js
+++ b/spec/frontend/members/components/table/members_table_spec.js
@@ -5,7 +5,7 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { mountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import CreatedAt from '~/members/components/table/created_at.vue';
import ExpirationDatepicker from '~/members/components/table/expiration_datepicker.vue';
-import MemberActionButtons from '~/members/components/table/member_action_buttons.vue';
+import MemberActions from '~/members/components/table/member_actions.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';
@@ -71,7 +71,7 @@ describe('MembersTable', () => {
'member-avatar',
'member-source',
'created-at',
- 'member-action-buttons',
+ 'member-actions',
'role-dropdown',
'remove-group-link-modal',
'remove-member-modal',
@@ -181,10 +181,7 @@ describe('MembersTable', () => {
expect(actionField.exists()).toBe(true);
expect(actionField.classes('gl-sr-only')).toBe(true);
expect(
- wrapper
- .find(`[data-label="Actions"][role="cell"]`)
- .findComponent(MemberActionButtons)
- .exists(),
+ wrapper.find(`[data-label="Actions"][role="cell"]`).findComponent(MemberActions).exists(),
).toBe(true);
});
diff --git a/spec/frontend/members/utils_spec.js b/spec/frontend/members/utils_spec.js
index 9f200324c02..4f276e8c9df 100644
--- a/spec/frontend/members/utils_spec.js
+++ b/spec/frontend/members/utils_spec.js
@@ -166,9 +166,9 @@ describe('Members Utils', () => {
describe('canDisableTwoFactor', () => {
it.each`
- member | expected
- ${{ ...memberMock, canGetTwoFactorDisabled: true }} | ${false}
- ${{ ...memberMock, canGetTwoFactorDisabled: false }} | ${false}
+ member | expected
+ ${{ ...memberMock, canDisableTwoFactor: true }} | ${false}
+ ${{ ...memberMock, canDisableTwoFactor: false }} | ${false}
`(
'returns $expected for members whose two factor authentication can be disabled',
({ member, expected }) => {
diff --git a/spec/frontend/merge_conflicts/store/actions_spec.js b/spec/frontend/merge_conflicts/store/actions_spec.js
index 50eac982e20..19ef4b7db25 100644
--- a/spec/frontend/merge_conflicts/store/actions_spec.js
+++ b/spec/frontend/merge_conflicts/store/actions_spec.js
@@ -1,6 +1,7 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Cookies from '~/lib/utils/cookies';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
@@ -35,7 +36,7 @@ describe('merge conflicts actions', () => {
const conflictsPath = 'conflicts/path/mock';
it('on success dispatches setConflictsData', () => {
- mock.onGet(conflictsPath).reply(200, {});
+ mock.onGet(conflictsPath).reply(HTTP_STATUS_OK, {});
return testAction(
actions.fetchConflictsData,
conflictsPath,
@@ -49,7 +50,7 @@ describe('merge conflicts actions', () => {
});
it('when data has type equal to error', () => {
- mock.onGet(conflictsPath).reply(200, { type: 'error', message: 'error message' });
+ mock.onGet(conflictsPath).reply(HTTP_STATUS_OK, { type: 'error', message: 'error message' });
return testAction(
actions.fetchConflictsData,
conflictsPath,
@@ -64,7 +65,7 @@ describe('merge conflicts actions', () => {
});
it('when request fails', () => {
- mock.onGet(conflictsPath).reply(400);
+ mock.onGet(conflictsPath).reply(HTTP_STATUS_BAD_REQUEST);
return testAction(
actions.fetchConflictsData,
conflictsPath,
@@ -102,7 +103,7 @@ describe('merge conflicts actions', () => {
const resolveConflictsPath = 'resolve/conflicts/path/mock';
it('on success reloads the page', async () => {
- mock.onPost(resolveConflictsPath).reply(200, { redirect_to: 'hrefPath' });
+ mock.onPost(resolveConflictsPath).reply(HTTP_STATUS_OK, { redirect_to: 'hrefPath' });
await testAction(
actions.submitResolvedConflicts,
resolveConflictsPath,
@@ -114,7 +115,7 @@ describe('merge conflicts actions', () => {
});
it('on errors shows flash', async () => {
- mock.onPost(resolveConflictsPath).reply(400);
+ mock.onPost(resolveConflictsPath).reply(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.submitResolvedConflicts,
resolveConflictsPath,
diff --git a/spec/frontend/merge_request_spec.js b/spec/frontend/merge_request_spec.js
index 16e3e49a297..579cee8c022 100644
--- a/spec/frontend/merge_request_spec.js
+++ b/spec/frontend/merge_request_spec.js
@@ -5,6 +5,7 @@ import { TEST_HOST } from 'spec/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_CONFLICT, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MergeRequest from '~/merge_request';
jest.mock('~/flash');
@@ -22,7 +23,7 @@ describe('MergeRequest', () => {
mock
.onPatch(`${TEST_HOST}/frontend-fixtures/merge-requests-project/-/merge_requests/1.json`)
- .reply(200, {});
+ .reply(HTTP_STATUS_OK, {});
test.merge = new MergeRequest();
return test.merge;
@@ -89,7 +90,7 @@ describe('MergeRequest', () => {
it('shows an error notification when tasklist update failed', async () => {
mock
.onPatch(`${TEST_HOST}/frontend-fixtures/merge-requests-project/-/merge_requests/1.json`)
- .reply(409, {});
+ .reply(HTTP_STATUS_CONFLICT, {});
$('.js-task-list-field').trigger({
type: 'tasklist:changed',
diff --git a/spec/frontend/merge_requests/components/compare_app_spec.js b/spec/frontend/merge_requests/components/compare_app_spec.js
new file mode 100644
index 00000000000..8f84341b653
--- /dev/null
+++ b/spec/frontend/merge_requests/components/compare_app_spec.js
@@ -0,0 +1,50 @@
+import { shallowMount } from '@vue/test-utils';
+import CompareApp from '~/merge_requests/components/compare_app.vue';
+
+let wrapper;
+
+function factory(provideData = {}) {
+ wrapper = shallowMount(CompareApp, {
+ provide: {
+ inputs: {
+ project: {
+ id: 'project',
+ name: 'project',
+ },
+ branch: {
+ id: 'branch',
+ name: 'branch',
+ },
+ },
+ toggleClass: {
+ project: 'project',
+ branch: 'branch',
+ },
+ i18n: {
+ projectHeaderText: 'Project',
+ branchHeaderText: 'Branch',
+ },
+ ...provideData,
+ },
+ });
+}
+
+describe('Merge requests compare app component', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('shows commit box when selected branch is empty', () => {
+ factory({
+ currentBranch: {
+ text: '',
+ value: '',
+ },
+ });
+
+ const commitBox = wrapper.find('[data-testid="commit-box"]');
+
+ expect(commitBox.exists()).toBe(true);
+ expect(commitBox.text()).toBe('Select a branch to compare');
+ });
+});
diff --git a/spec/frontend/merge_requests/components/target_project_dropdown_spec.js b/spec/frontend/merge_requests/components/compare_dropdown_spec.js
index 3fddbe7ae21..ab5c315816c 100644
--- a/spec/frontend/merge_requests/components/target_project_dropdown_spec.js
+++ b/spec/frontend/merge_requests/components/compare_dropdown_spec.js
@@ -3,26 +3,32 @@ import { GlCollapsibleListbox } from '@gitlab/ui';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
-import TargetProjectDropdown from '~/merge_requests/components/target_project_dropdown.vue';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import CompareDropdown from '~/merge_requests/components/compare_dropdown.vue';
let wrapper;
let mock;
-function factory() {
- wrapper = mount(TargetProjectDropdown, {
- provide: {
- targetProjectsPath: '/gitlab-org/gitlab/target_projects',
- currentProject: { value: 1, text: 'gitlab-org/gitlab' },
+function factory(propsData = {}) {
+ wrapper = mount(CompareDropdown, {
+ propsData: {
+ endpoint: '/gitlab-org/gitlab/target_projects',
+ default: { value: 1, text: 'gitlab-org/gitlab' },
+ dropdownHeader: 'Select',
+ inputId: 'input_id',
+ inputName: 'input_name',
+ isProject: true,
+ ...propsData,
},
});
}
const findDropdown = () => wrapper.findComponent(GlCollapsibleListbox);
-describe('Merge requests target project dropdown component', () => {
+describe('Merge requests compare dropdown component', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet('/gitlab-org/gitlab/target_projects').reply(200, [
+ mock.onGet('/gitlab-org/gitlab/target_projects').reply(HTTP_STATUS_OK, [
{
id: 10,
name: 'Gitlab Test',
@@ -77,4 +83,22 @@ describe('Merge requests target project dropdown component', () => {
expect(mock.history.get[1].params).toEqual({ search: 'test' });
});
+
+ it('renders static data', async () => {
+ factory({
+ endpoint: undefined,
+ staticData: [
+ {
+ value: '10',
+ text: 'GitLab Org',
+ },
+ ],
+ });
+
+ wrapper.find('[data-testid="base-dropdown-toggle"]').trigger('click');
+
+ await waitForPromises();
+
+ expect(wrapper.findAll('li').length).toBe(1);
+ });
});
diff --git a/spec/frontend/milestones/components/delete_milestone_modal_spec.js b/spec/frontend/milestones/components/delete_milestone_modal_spec.js
index 6692a3b9347..87235fa843a 100644
--- a/spec/frontend/milestones/components/delete_milestone_modal_spec.js
+++ b/spec/frontend/milestones/components/delete_milestone_modal_spec.js
@@ -4,6 +4,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import DeleteMilestoneModal from '~/milestones/components/delete_milestone_modal.vue';
import eventHub from '~/milestones/event_hub';
+import { HTTP_STATUS_IM_A_TEAPOT, HTTP_STATUS_NOT_FOUND } from '~/lib/utils/http_status';
import { redirectTo } from '~/lib/utils/url_utility';
import { createAlert } from '~/flash';
@@ -71,9 +72,9 @@ describe('Delete milestone modal', () => {
});
it.each`
- statusCode | alertMessage
- ${418} | ${`Failed to delete milestone ${mockProps.milestoneTitle}`}
- ${404} | ${`Milestone ${mockProps.milestoneTitle} was not found`}
+ statusCode | alertMessage
+ ${HTTP_STATUS_IM_A_TEAPOT} | ${`Failed to delete milestone ${mockProps.milestoneTitle}`}
+ ${HTTP_STATUS_NOT_FOUND} | ${`Milestone ${mockProps.milestoneTitle} was not found`}
`(
'displays error if deleting milestone failed with code $statusCode',
async ({ statusCode, alertMessage }) => {
diff --git a/spec/frontend/milestones/components/milestone_combobox_spec.js b/spec/frontend/milestones/components/milestone_combobox_spec.js
index c20c51db75e..f8ddca1a2ad 100644
--- a/spec/frontend/milestones/components/milestone_combobox_spec.js
+++ b/spec/frontend/milestones/components/milestone_combobox_spec.js
@@ -4,6 +4,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { ENTER_KEY } from '~/lib/utils/keys';
import MilestoneCombobox from '~/milestones/components/milestone_combobox.vue';
import createStore from '~/milestones/stores/';
@@ -63,15 +64,15 @@ describe('Milestone combobox component', () => {
projectMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
+ .mockReturnValue([HTTP_STATUS_OK, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
groupMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, groupMilestones, { [X_TOTAL_HEADER]: '6' }]);
+ .mockReturnValue([HTTP_STATUS_OK, groupMilestones, { [X_TOTAL_HEADER]: '6' }]);
searchApiCallSpy = jest
.fn()
- .mockReturnValue([200, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
+ .mockReturnValue([HTTP_STATUS_OK, projectMilestones, { [X_TOTAL_HEADER]: '6' }]);
mock
.onGet(`/api/v4/projects/${projectId}/milestones`)
@@ -247,9 +248,11 @@ describe('Milestone combobox component', () => {
beforeEach(() => {
projectMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
- groupMilestonesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+ groupMilestonesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@@ -300,7 +303,7 @@ describe('Milestone combobox component', () => {
beforeEach(() => {
projectMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@@ -314,8 +317,10 @@ describe('Milestone combobox component', () => {
describe('when the project milestones search returns an error', () => {
beforeEach(() => {
- projectMilestonesApiCallSpy = jest.fn().mockReturnValue([500]);
- searchApiCallSpy = jest.fn().mockReturnValue([500]);
+ projectMilestonesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
+ searchApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
createComponent({ value: [] });
@@ -363,7 +368,7 @@ describe('Milestone combobox component', () => {
createComponent();
projectMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, [{ title: 'v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
+ .mockReturnValue([HTTP_STATUS_OK, [{ title: 'v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
return waitForRequests();
});
@@ -427,7 +432,7 @@ describe('Milestone combobox component', () => {
beforeEach(() => {
groupMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@@ -441,8 +446,10 @@ describe('Milestone combobox component', () => {
describe('when the group milestones search returns an error', () => {
beforeEach(() => {
- groupMilestonesApiCallSpy = jest.fn().mockReturnValue([500]);
- searchApiCallSpy = jest.fn().mockReturnValue([500]);
+ groupMilestonesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
+ searchApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
createComponent({ value: [] });
@@ -490,7 +497,11 @@ describe('Milestone combobox component', () => {
createComponent();
groupMilestonesApiCallSpy = jest
.fn()
- .mockReturnValue([200, [{ title: 'group-v1.0' }], { [X_TOTAL_HEADER]: '1' }]);
+ .mockReturnValue([
+ HTTP_STATUS_OK,
+ [{ title: 'group-v1.0' }],
+ { [X_TOTAL_HEADER]: '1' },
+ ]);
return waitForRequests();
});
diff --git a/spec/frontend/milestones/components/promote_milestone_modal_spec.js b/spec/frontend/milestones/components/promote_milestone_modal_spec.js
index 60657fbc9b8..d7ad3d29d0a 100644
--- a/spec/frontend/milestones/components/promote_milestone_modal_spec.js
+++ b/spec/frontend/milestones/components/promote_milestone_modal_spec.js
@@ -5,6 +5,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import * as urlUtils from '~/lib/utils/url_utility';
import PromoteMilestoneModal from '~/milestones/components/promote_milestone_modal.vue';
@@ -94,7 +95,7 @@ describe('Promote milestone modal', () => {
it('displays an error if promoting a milestone failed', async () => {
const dummyError = new Error('promoting milestone failed');
- dummyError.response = { status: 500 };
+ dummyError.response = { status: HTTP_STATUS_INTERNAL_SERVER_ERROR };
jest.spyOn(axios, 'post').mockImplementation((url) => {
expect(url).toBe(milestoneMockData.url);
return Promise.reject(dummyError);
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 0c3d3e78038..7d7eee2bc2c 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
@@ -24,14 +24,14 @@ exports[`MlCandidate renders correctly 1`] = `
<h2
class="gl-alert-title"
>
- Machine Learning Experiment Tracking is in Incubating Phase
+ Machine learning experiment tracking is in incubating phase
</h2>
<div
class="gl-alert-body"
>
- GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited
+ GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited.
<a
class="gl-link"
@@ -39,7 +39,7 @@ exports[`MlCandidate renders correctly 1`] = `
rel="noopener noreferrer"
target="_blank"
>
- Learn more
+ Learn more about incubating features
</a>
</div>
@@ -58,7 +58,7 @@ exports[`MlCandidate renders correctly 1`] = `
class="gl-button-text"
>
- Feedback
+ Give feedback on this feature
</span>
</a>
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
deleted file mode 100644
index 3ee2c1cc075..00000000000
--- a/spec/frontend/ml/experiment_tracking/components/__snapshots__/ml_experiment_spec.js.snap
+++ /dev/null
@@ -1,761 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`MlExperiment with candidates renders correctly 1`] = `
-<div>
- <div
- class="gl-alert gl-alert-warning"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16 gl-alert-icon"
- data-testid="warning-icon"
- role="img"
- >
- <use
- href="#warning"
- />
- </svg>
-
- <div
- aria-live="assertive"
- class="gl-alert-content"
- role="alert"
- >
- <h2
- class="gl-alert-title"
- >
- Machine Learning Experiment Tracking is in Incubating Phase
- </h2>
-
- <div
- class="gl-alert-body"
- >
-
- GitLab incubates features to explore new use cases. These features are updated regularly, and support is limited
-
- <a
- class="gl-link"
- href="https://about.gitlab.com/handbook/engineering/incubation/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Learn more
- </a>
- </div>
-
- <div
- class="gl-alert-actions"
- >
- <a
- class="btn gl-alert-action btn-confirm btn-md gl-button"
- href="https://gitlab.com/gitlab-org/gitlab/-/issues/381660"
- >
- <!---->
-
- <!---->
-
- <span
- class="gl-button-text"
- >
-
- Feedback
-
- </span>
- </a>
- </div>
- </div>
-
- <button
- aria-label="Dismiss"
- class="btn gl-dismiss-btn btn-default btn-sm gl-button btn-default-tertiary btn-icon"
- type="button"
- >
- <!---->
-
- <svg
- aria-hidden="true"
- class="gl-button-icon gl-icon s16"
- data-testid="close-icon"
- role="img"
- >
- <use
- href="#close"
- />
- </svg>
-
- <!---->
- </button>
- </div>
-
- <h3>
-
- Experiment candidates
-
- </h3>
-
- <table
- aria-busy="false"
- aria-colcount="9"
- class="table b-table gl-table gl-mt-0! ml-candidate-table table-sm"
- role="table"
- >
- <!---->
- <!---->
- <thead
- class=""
- role="rowgroup"
- >
- <!---->
- <tr
- class=""
- role="row"
- >
- <th
- aria-colindex="1"
- class=""
- role="columnheader"
- scope="col"
- >
- <div>
- Name
- </div>
- </th>
- <th
- aria-colindex="2"
- class=""
- role="columnheader"
- scope="col"
- >
- <div>
- Created at
- </div>
- </th>
- <th
- aria-colindex="3"
- class=""
- role="columnheader"
- scope="col"
- >
- <div>
- User
- </div>
- </th>
- <th
- aria-colindex="4"
- class=""
- role="columnheader"
- scope="col"
- >
- <div>
- 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"
- scope="col"
- >
- <div />
- </th>
- <th
- aria-colindex="9"
- aria-label="Artifact"
- class=""
- role="columnheader"
- scope="col"
- >
- <div />
- </th>
- </tr>
- </thead>
- <tbody
- role="rowgroup"
- >
- <!---->
- <tr
- class=""
- role="row"
- >
- <td
- aria-colindex="1"
- class=""
- role="cell"
- >
- <div
- title="aCandidate"
- >
- aCandidate
- </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"
- >
- <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="9"
- class=""
- role="cell"
- >
- <a
- class="gl-link"
- href="link_to_artifact"
- rel="noopener"
- target="_blank"
- title="Artifacts"
- >
- Artifacts
- </a>
- </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_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 fb45c4b07a4..483e454d7d7 100644
--- a/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
+++ b/spec/frontend/ml/experiment_tracking/components/ml_candidate_spec.js
@@ -28,7 +28,7 @@ describe('MlCandidate', () => {
},
};
- return mountExtended(MlCandidate, { provide: { candidate } });
+ return mountExtended(MlCandidate, { propsData: { candidate } });
};
const findAlert = () => wrapper.findComponent(GlAlert);
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 abcaf17303f..f307d2c5a58 100644
--- a/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
+++ b/spec/frontend/ml/experiment_tracking/components/ml_experiment_spec.js
@@ -1,140 +1,315 @@
-import { GlAlert, GlPagination } from '@gitlab/ui';
+import { GlAlert, GlTable, GlLink } from '@gitlab/ui';
+import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import MlExperiment from '~/ml/experiment_tracking/components/ml_experiment.vue';
+import RegistrySearch from '~/vue_shared/components/registry/registry_search.vue';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+import setWindowLocation from 'helpers/set_window_location_helper';
+import * as urlHelpers from '~/lib/utils/url_utility';
describe('MlExperiment', () => {
let wrapper;
+ const startCursor = 'eyJpZCI6IjE2In0';
+ const defaultPageInfo = {
+ startCursor,
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ };
+
const createWrapper = (
candidates = [],
metricNames = [],
paramNames = [],
- pagination = { page: 1, isLastPage: false, per_page: 2, totalItems: 0 },
+ pageInfo = defaultPageInfo,
) => {
- return mountExtended(MlExperiment, {
- provide: { candidates, metricNames, paramNames, pagination },
+ wrapper = mountExtended(MlExperiment, {
+ provide: { candidates, metricNames, paramNames, pageInfo },
});
};
- const findAlert = () => wrapper.findComponent(GlAlert);
+ const candidates = [
+ {
+ 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,
+ },
+ ];
+
+ const createWrapperWithCandidates = (pageInfo = defaultPageInfo) => {
+ createWrapper(candidates, ['rmse', 'auc', 'mae'], ['l1_ratio'], pageInfo);
+ };
- const findEmptyState = () => wrapper.findByText('This experiment has no logged candidates');
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findPagination = () => wrapper.findComponent(Pagination);
+ const findEmptyState = () => wrapper.findByText('No candidates to display');
+ const findRegistrySearch = () => wrapper.findComponent(RegistrySearch);
+ const findTable = () => wrapper.findComponent(GlTable);
+ const findTableHeaders = () => findTable().findAll('th');
+ const findTableRows = () => findTable().findAll('tbody > tr');
+ const findNthTableRow = (idx) => findTableRows().at(idx);
+ const findColumnInRow = (row, col) => findNthTableRow(row).findAll('td').at(col);
+ const hrefInRowAndColumn = (row, col) =>
+ findColumnInRow(row, col).findComponent(GlLink).attributes().href;
it('shows incubation warning', () => {
- wrapper = createWrapper();
+ createWrapper();
expect(findAlert().exists()).toBe(true);
});
- describe('no candidates', () => {
- it('shows empty state', () => {
- wrapper = createWrapper();
+ describe('default inputs', () => {
+ beforeEach(async () => {
+ createWrapper();
+ await nextTick();
+ });
+
+ it('shows empty state', () => {
expect(findEmptyState().exists()).toBe(true);
});
it('does not show pagination', () => {
- wrapper = createWrapper();
+ expect(findPagination().exists()).toBe(false);
+ });
- expect(wrapper.findComponent(GlPagination).exists()).toBe(false);
+ it('there are no columns', () => {
+ expect(findTable().findAll('th')).toHaveLength(0);
+ });
+
+ it('initializes sorting correctly', () => {
+ expect(findRegistrySearch().props('sorting')).toMatchObject({
+ orderBy: 'created_at',
+ sort: 'desc',
+ });
+ });
+
+ it('initializes filters correctly', () => {
+ expect(findRegistrySearch().props('filters')).toMatchObject([{ value: { data: '' } }]);
});
});
- describe('with candidates', () => {
- 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',
- 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);
+ describe('Search', () => {
+ it('shows search box', () => {
+ createWrapper();
+
+ expect(findRegistrySearch().exists()).toBe(true);
+ });
+
+ it('metrics are added as options for sorting', () => {
+ createWrapper([], ['bar']);
+
+ const labels = findRegistrySearch()
+ .props('sortableFields')
+ .map((e) => e.orderBy);
+ expect(labels).toContain('metric.bar');
+ });
+
+ it('sets the component filters based on the querystring', () => {
+ setWindowLocation('https://blah?name=A&orderBy=B&sort=C');
+
+ createWrapper();
+
+ expect(findRegistrySearch().props('filters')).toMatchObject([{ value: { data: 'A' } }]);
+ });
+
+ it('sets the component sort based on the querystring', () => {
+ setWindowLocation('https://blah?name=A&orderBy=B&sort=C');
+
+ createWrapper();
+
+ expect(findRegistrySearch().props('sorting')).toMatchObject({ orderBy: 'B', sort: 'c' });
+ });
+
+ it('sets the component sort based on the querystring, when order by is a metric', () => {
+ setWindowLocation('https://blah?name=A&orderBy=B&sort=C&orderByType=metric');
+
+ createWrapper();
+
+ expect(findRegistrySearch().props('sorting')).toMatchObject({
+ orderBy: 'metric.B',
+ sort: 'c',
+ });
+ });
+
+ describe('Search submit', () => {
+ beforeEach(() => {
+ setWindowLocation('https://blah.com/?name=query&orderBy=name&orderByType=column&sort=asc');
+ jest.spyOn(urlHelpers, 'visitUrl').mockImplementation(() => {});
+
+ createWrapper();
+ });
+
+ it('On submit, resets the cursor and reloads to correct page', () => {
+ findRegistrySearch().vm.$emit('filter:submit');
+
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
+ 'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=asc',
+ );
});
- it('should get the page number from the URL', () => {
- wrapper = createWrapperWithCandidates({ ...defaultPagination, page: 2 });
+ it('On sorting changed, resets cursor and reloads to correct page', () => {
+ findRegistrySearch().vm.$emit('sorting:changed', { orderBy: 'created_at' });
- expect(wrapper.findComponent(GlPagination).props().value).toBe(2);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
+ 'https://blah.com/?name=query&orderBy=created_at&orderByType=column&sort=asc',
+ );
});
- it('should not have a prevPage if the page is 1', () => {
- wrapper = createWrapperWithCandidates();
+ it('On sorting changed and is metric, resets cursor and reloads to correct page', () => {
+ findRegistrySearch().vm.$emit('sorting:changed', { orderBy: 'metric.auc' });
- expect(wrapper.findComponent(GlPagination).props().prevPage).toBe(null);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
+ 'https://blah.com/?name=query&orderBy=auc&orderByType=metric&sort=asc',
+ );
});
- it('should set the prevPage to 1 if the page is 2', () => {
- wrapper = createWrapperWithCandidates({ ...defaultPagination, page: 2 });
+ it('On direction changed, reloads to correct page', () => {
+ findRegistrySearch().vm.$emit('sorting:changed', { sort: 'desc' });
- expect(wrapper.findComponent(GlPagination).props().prevPage).toBe(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledTimes(1);
+ expect(urlHelpers.visitUrl).toHaveBeenCalledWith(
+ 'https://blah.com/?name=query&orderBy=name&orderByType=column&sort=desc',
+ );
});
+ });
+ });
+
+ describe('Pagination behaviour', () => {
+ beforeEach(() => {
+ createWrapperWithCandidates();
+ });
+
+ it('should show', () => {
+ expect(findPagination().exists()).toBe(true);
+ });
- it('should not have a nextPage if isLastPage is true', async () => {
- wrapper = createWrapperWithCandidates({ ...defaultPagination, isLastPage: true });
+ it('Passes pagination to pagination component', () => {
+ createWrapperWithCandidates();
+
+ expect(findPagination().props('startCursor')).toBe(startCursor);
+ });
+ });
+
+ describe('Candidate table', () => {
+ const firstCandidateIndex = 0;
+ const secondCandidateIndex = 1;
+ const firstCandidate = candidates[firstCandidateIndex];
+
+ beforeEach(() => {
+ createWrapperWithCandidates();
+ });
+
+ it('renders all rows', () => {
+ expect(findTableRows()).toHaveLength(candidates.length);
+ });
+
+ it('sets the correct columns in the table', () => {
+ const expectedColumnNames = [
+ 'Name',
+ 'Created at',
+ 'User',
+ 'L1 Ratio',
+ 'Rmse',
+ 'Auc',
+ 'Mae',
+ '',
+ '',
+ ];
+
+ expect(findTableHeaders().wrappers.map((h) => h.text())).toEqual(expectedColumnNames);
+ });
- expect(wrapper.findComponent(GlPagination).props().nextPage).toBe(null);
+ describe('Artifact column', () => {
+ const artifactColumnIndex = -1;
+
+ it('shows the a link to the artifact', () => {
+ expect(hrefInRowAndColumn(firstCandidateIndex, artifactColumnIndex)).toBe(
+ firstCandidate.artifact,
+ );
+ });
+
+ it('shows empty state when no artifact', () => {
+ expect(findColumnInRow(secondCandidateIndex, artifactColumnIndex).text()).toBe('-');
+ });
+ });
+
+ describe('User column', () => {
+ const userColumn = 2;
+
+ it('creates a link to the user', () => {
+ const column = findColumnInRow(firstCandidateIndex, userColumn).findComponent(GlLink);
+
+ expect(column.attributes().href).toBe(firstCandidate.user.path);
+ expect(column.text()).toBe(`@${firstCandidate.user.username}`);
+ });
+
+ it('when there is no user shows empty state', () => {
+ createWrapperWithCandidates();
+
+ expect(findColumnInRow(secondCandidateIndex, userColumn).text()).toBe('-');
});
+ });
+
+ describe('Candidate name column', () => {
+ const nameColumnIndex = 0;
+
+ it('Sets the name', () => {
+ expect(findColumnInRow(firstCandidateIndex, nameColumnIndex).text()).toBe(
+ firstCandidate.name,
+ );
+ });
+
+ it('when there is no user shows nothing', () => {
+ expect(findColumnInRow(secondCandidateIndex, nameColumnIndex).text()).toBe('');
+ });
+ });
- it('should set the nextPage to 2 if the page is 1', () => {
- wrapper = createWrapperWithCandidates();
+ describe('Detail column', () => {
+ const detailColumn = -2;
- expect(wrapper.findComponent(GlPagination).props().nextPage).toBe(2);
+ it('is a link to details', () => {
+ expect(hrefInRowAndColumn(firstCandidateIndex, detailColumn)).toBe(firstCandidate.details);
});
});
});
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js b/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js
new file mode 100644
index 00000000000..017db647ac6
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/ml_experiments_index_spec.js
@@ -0,0 +1,110 @@
+import { GlEmptyState, GlLink, GlTableLite } from '@gitlab/ui';
+import MlExperimentsIndexApp from '~/ml/experiment_tracking/routes/experiments/index';
+import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ startCursor,
+ firstExperiment,
+ secondExperiment,
+ experiments,
+ defaultPageInfo,
+} from './mock_data';
+
+let wrapper;
+const createWrapper = (defaultExperiments = [], pageInfo = defaultPageInfo) => {
+ wrapper = mountExtended(MlExperimentsIndexApp, {
+ propsData: { experiments: defaultExperiments, pageInfo },
+ });
+};
+
+const findAlert = () => wrapper.findComponent(IncubationAlert);
+const findPagination = () => wrapper.findComponent(Pagination);
+const findEmptyState = () => wrapper.findComponent(GlEmptyState);
+const findTable = () => wrapper.findComponent(GlTableLite);
+const findTableHeaders = () => findTable().findAll('th');
+const findTableRows = () => findTable().findAll('tbody > tr');
+const findNthTableRow = (idx) => findTableRows().at(idx);
+const findColumnInRow = (row, col) => findNthTableRow(row).findAll('td').at(col);
+const hrefInRowAndColumn = (row, col) =>
+ findColumnInRow(row, col).findComponent(GlLink).attributes().href;
+
+describe('MlExperimentsIndex', () => {
+ describe('empty state', () => {
+ beforeEach(() => createWrapper());
+
+ it('displays empty state when no experiment', () => {
+ expect(findEmptyState().exists()).toBe(true);
+ });
+
+ it('does not show table', () => {
+ expect(findTable().exists()).toBe(false);
+ });
+
+ it('does not show pagination', () => {
+ expect(findPagination().exists()).toBe(false);
+ });
+ });
+
+ it('displays IncubationAlert', () => {
+ createWrapper(experiments);
+
+ expect(findAlert().exists()).toBe(true);
+ });
+
+ describe('experiments table', () => {
+ const firstRow = 0;
+ const secondRow = 1;
+ const nameColumn = 0;
+ const candidateCountColumn = 1;
+
+ beforeEach(() => createWrapper(experiments));
+
+ it('displays the table', () => {
+ expect(findTable().exists()).toBe(true);
+ });
+
+ it('sets headers correctly', () => {
+ const expectedColumnNames = ['Experiment', 'Logged candidates for experiment'];
+
+ expect(findTableHeaders().wrappers.map((h) => h.text())).toEqual(expectedColumnNames);
+ });
+
+ describe('experiment name column', () => {
+ it('displays the experiment name', () => {
+ expect(findColumnInRow(firstRow, nameColumn).text()).toBe(firstExperiment.name);
+ expect(findColumnInRow(secondRow, nameColumn).text()).toBe(secondExperiment.name);
+ });
+
+ it('is a link to the experiment', () => {
+ expect(hrefInRowAndColumn(firstRow, nameColumn)).toBe(firstExperiment.path);
+ expect(hrefInRowAndColumn(secondRow, nameColumn)).toBe(secondExperiment.path);
+ });
+ });
+
+ describe('candidate count column', () => {
+ it('shows the candidate count', () => {
+ expect(findColumnInRow(firstRow, candidateCountColumn).text()).toBe(
+ `${firstExperiment.candidate_count}`,
+ );
+ expect(findColumnInRow(secondRow, candidateCountColumn).text()).toBe(
+ `${secondExperiment.candidate_count}`,
+ );
+ });
+ });
+ });
+
+ describe('pagination', () => {
+ describe('Pagination behaviour', () => {
+ beforeEach(() => createWrapper(experiments));
+
+ it('should show', () => {
+ expect(findPagination().exists()).toBe(true);
+ });
+
+ it('Passes pagination to pagination component', () => {
+ expect(findPagination().props('startCursor')).toBe(startCursor);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/mock_data.js b/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/mock_data.js
new file mode 100644
index 00000000000..ea02584a7cc
--- /dev/null
+++ b/spec/frontend/ml/experiment_tracking/routes/experiments/index/components/mock_data.js
@@ -0,0 +1,21 @@
+export const startCursor = 'eyJpZCI6IjE2In0';
+export const defaultPageInfo = Object.freeze({
+ startCursor,
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+});
+
+export const firstExperiment = Object.freeze({
+ name: 'Experiment 1',
+ path: 'path/to/experiment/1',
+ candidate_count: 2,
+});
+
+export const secondExperiment = Object.freeze({
+ name: 'Experiment 2',
+ path: 'path/to/experiment/2',
+ candidate_count: 3,
+});
+
+export const experiments = [firstExperiment, secondExperiment];
diff --git a/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
index 08487a7a796..4483c9fd39f 100644
--- a/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/empty_state_spec.js.snap
@@ -5,6 +5,7 @@ exports[`EmptyState shows gettingStarted state 1`] = `
<!---->
<gl-empty-state-stub
+ contentclass=""
description="Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments."
invertindarkmode="true"
primarybuttonlink="/clustersPath"
@@ -22,6 +23,7 @@ exports[`EmptyState shows noData state 1`] = `
<!---->
<gl-empty-state-stub
+ contentclass=""
description="You are connected to the Prometheus server, but there is currently no data to display."
invertindarkmode="true"
primarybuttonlink="/settingsPath"
@@ -39,6 +41,7 @@ exports[`EmptyState shows unableToConnect state 1`] = `
<!---->
<gl-empty-state-stub
+ contentclass=""
description="Ensure connectivity is available from the GitLab server to the Prometheus server"
invertindarkmode="true"
primarybuttonlink="/documentationPath"
diff --git a/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap b/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
index 1d7ff420a17..42a16a39dfd 100644
--- a/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
+++ b/spec/frontend/monitoring/components/__snapshots__/group_empty_state_spec.js.snap
@@ -3,6 +3,7 @@
exports[`GroupEmptyState given state BAD_QUERY passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": null,
"invertInDarkMode": true,
"primaryButtonLink": "/path/to/settings",
@@ -31,6 +32,7 @@ exports[`GroupEmptyState given state BAD_QUERY renders the slotted content 1`] =
exports[`GroupEmptyState given state CONNECTION_FAILED passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": "We couldn't reach the Prometheus server. Either the server no longer exists or the configuration details need updating.",
"invertInDarkMode": true,
"primaryButtonLink": "/path/to/settings",
@@ -48,6 +50,7 @@ exports[`GroupEmptyState given state CONNECTION_FAILED renders the slotted conte
exports[`GroupEmptyState given state FOO STATE passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": "An error occurred while loading the data. Please try again.",
"invertInDarkMode": true,
"primaryButtonLink": null,
@@ -65,6 +68,7 @@ exports[`GroupEmptyState given state FOO STATE renders the slotted content 1`] =
exports[`GroupEmptyState given state LOADING passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": "Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.",
"invertInDarkMode": true,
"primaryButtonLink": null,
@@ -82,6 +86,7 @@ exports[`GroupEmptyState given state LOADING renders the slotted content 1`] = `
exports[`GroupEmptyState given state NO_DATA passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": null,
"invertInDarkMode": true,
"primaryButtonLink": null,
@@ -110,6 +115,7 @@ exports[`GroupEmptyState given state NO_DATA renders the slotted content 1`] = `
exports[`GroupEmptyState given state TIMEOUT passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": null,
"invertInDarkMode": true,
"primaryButtonLink": null,
@@ -138,6 +144,7 @@ exports[`GroupEmptyState given state TIMEOUT renders the slotted content 1`] = `
exports[`GroupEmptyState given state UNKNOWN_ERROR passes the expected props to GlEmptyState 1`] = `
Object {
"compact": true,
+ "contentClass": Array [],
"description": "An error occurred while loading the data. Please try again.",
"invertInDarkMode": true,
"primaryButtonLink": null,
diff --git a/spec/frontend/monitoring/requests/index_spec.js b/spec/frontend/monitoring/requests/index_spec.js
index cf7df3dd9d5..308895768a4 100644
--- a/spec/frontend/monitoring/requests/index_spec.js
+++ b/spec/frontend/monitoring/requests/index_spec.js
@@ -4,6 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import * as commonUtils from '~/lib/utils/common_utils';
import {
HTTP_STATUS_BAD_REQUEST,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_NO_CONTENT,
HTTP_STATUS_OK,
HTTP_STATUS_SERVICE_UNAVAILABLE,
@@ -55,7 +56,7 @@ describe('monitoring metrics_requests', () => {
});
it('rejects after getting an error', () => {
- mock.onGet(dashboardEndpoint).reply(500);
+ mock.onGet(dashboardEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return getDashboard(dashboardEndpoint, params).catch((error) => {
expect(error).toEqual(expect.any(Error));
@@ -99,7 +100,7 @@ describe('monitoring metrics_requests', () => {
});
it('rejects after getting an HTTP 500 error', () => {
- mock.onGet(prometheusEndpoint).reply(500, {
+ mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {
status: 'error',
error: 'An error occurred',
});
@@ -125,7 +126,7 @@ describe('monitoring metrics_requests', () => {
// Mock multiple attempts while the cache is filling up and fails
mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
mock.onGet(prometheusEndpoint).replyOnce(HTTP_STATUS_NO_CONTENT);
- mock.onGet(prometheusEndpoint).reply(500, {
+ mock.onGet(prometheusEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {
status: 'error',
error: 'An error occurred',
}); // 3rd attempt
diff --git a/spec/frontend/monitoring/store/actions_spec.js b/spec/frontend/monitoring/store/actions_spec.js
index fbe030b1a7d..8eda46a2ff1 100644
--- a/spec/frontend/monitoring/store/actions_spec.js
+++ b/spec/frontend/monitoring/store/actions_spec.js
@@ -7,6 +7,7 @@ import * as commonUtils from '~/lib/utils/common_utils';
import {
HTTP_STATUS_BAD_REQUEST,
HTTP_STATUS_CREATED,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
HTTP_STATUS_OK,
HTTP_STATUS_UNPROCESSABLE_ENTITY,
} from '~/lib/utils/http_status';
@@ -205,7 +206,7 @@ describe('Monitoring store actions', () => {
it('on success, dispatches receive and success actions, then fetches dashboard warnings', () => {
document.body.dataset.page = 'projects:environments:metrics';
- mock.onGet(state.dashboardEndpoint).reply(200, response);
+ mock.onGet(state.dashboardEndpoint).reply(HTTP_STATUS_OK, response);
return testAction(
fetchDashboard,
@@ -231,7 +232,9 @@ describe('Monitoring store actions', () => {
fullDashboardPath: store.getters['monitoringDashboard/fullDashboardPath'],
};
result = () => {
- mock.onGet(state.dashboardEndpoint).replyOnce(500, mockDashboardsErrorResponse);
+ mock
+ .onGet(state.dashboardEndpoint)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, mockDashboardsErrorResponse);
return fetchDashboard({ state, commit, dispatch, getters: localGetters }, params);
};
});
@@ -417,7 +420,7 @@ describe('Monitoring store actions', () => {
});
it('commits result', () => {
- mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt
+ mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_OK, { data }); // One attempt
return testAction(
fetchPrometheusMetric,
@@ -450,7 +453,7 @@ describe('Monitoring store actions', () => {
};
it('uses calculated step', async () => {
- mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt
+ mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_OK, { data }); // One attempt
await testAction(
fetchPrometheusMetric,
@@ -489,7 +492,7 @@ describe('Monitoring store actions', () => {
};
it('uses metric step', async () => {
- mock.onGet(prometheusEndpointPath).reply(200, { data }); // One attempt
+ mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_OK, { data }); // One attempt
await testAction(
fetchPrometheusMetric,
@@ -517,7 +520,7 @@ describe('Monitoring store actions', () => {
});
it('commits failure, when waiting for results and getting a server error', async () => {
- mock.onGet(prometheusEndpointPath).reply(500);
+ mock.onGet(prometheusEndpointPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
const error = new Error('Request failed with status code 500');
@@ -552,7 +555,7 @@ describe('Monitoring store actions', () => {
describe('fetchDeploymentsData', () => {
it('dispatches receiveDeploymentsDataSuccess on success', () => {
state.deploymentsEndpoint = '/success';
- mock.onGet(state.deploymentsEndpoint).reply(200, {
+ mock.onGet(state.deploymentsEndpoint).reply(HTTP_STATUS_OK, {
deployments: deploymentData,
});
@@ -566,7 +569,7 @@ describe('Monitoring store actions', () => {
});
it('dispatches receiveDeploymentsDataFailure on error', () => {
state.deploymentsEndpoint = '/error';
- mock.onGet(state.deploymentsEndpoint).reply(500);
+ mock.onGet(state.deploymentsEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(
fetchDeploymentsData,
@@ -918,7 +921,7 @@ describe('Monitoring store actions', () => {
it('stars dashboard if it is not starred', () => {
state.selectedDashboard = unstarredDashboard;
- mock.onPost(unstarredDashboard.user_starred_path).reply(200);
+ mock.onPost(unstarredDashboard.user_starred_path).reply(HTTP_STATUS_OK);
return testAction(toggleStarredValue, null, state, [
{ type: types.REQUEST_DASHBOARD_STARRING },
@@ -934,7 +937,7 @@ describe('Monitoring store actions', () => {
it('unstars dashboard if it is starred', () => {
state.selectedDashboard = starredDashboard;
- mock.onPost(starredDashboard.user_starred_path).reply(200);
+ mock.onPost(starredDashboard.user_starred_path).reply(HTTP_STATUS_OK);
return testAction(toggleStarredValue, null, state, [
{ type: types.REQUEST_DASHBOARD_STARRING },
@@ -1065,7 +1068,7 @@ describe('Monitoring store actions', () => {
},
];
- mock.onGet('/series?match[]=metric_name').reply(200, {
+ mock.onGet('/series?match[]=metric_name').reply(HTTP_STATUS_OK, {
status: 'success',
data,
});
@@ -1085,7 +1088,7 @@ describe('Monitoring store actions', () => {
});
it('should notify the user that dynamic options were not loaded', () => {
- mock.onGet('/series?match[]=metric_name').reply(500);
+ mock.onGet('/series?match[]=metric_name').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
return testAction(fetchVariableMetricLabelValues, { defaultQueryParams }, state, [], []).then(
() => {
@@ -1150,7 +1153,9 @@ describe('Monitoring store actions', () => {
});
it('should display a generic error when the backend fails', () => {
- mock.onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent }).reply(500);
+ mock
+ .onPost(panelPreviewEndpoint, { panel_yaml: mockYmlContent })
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
testAction(fetchPanelPreview, mockYmlContent, state, [
{ type: types.SET_PANEL_PREVIEW_IS_SHOWN, payload: true },
diff --git a/spec/frontend/mr_notes/stores/actions_spec.js b/spec/frontend/mr_notes/stores/actions_spec.js
index 568c1b930c9..ae30ed1f0b3 100644
--- a/spec/frontend/mr_notes/stores/actions_spec.js
+++ b/spec/frontend/mr_notes/stores/actions_spec.js
@@ -1,6 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { createStore } from '~/mr_notes/stores';
describe('MR Notes Mutator Actions', () => {
@@ -31,7 +31,7 @@ describe('MR Notes Mutator Actions', () => {
mock = new MockAdapter(axios);
- mock.onGet(metadata).reply(200, mrMetadata);
+ mock.onGet(metadata).reply(HTTP_STATUS_OK, mrMetadata);
});
afterEach(() => {
@@ -54,7 +54,7 @@ describe('MR Notes Mutator Actions', () => {
});
it('should set failedToLoadMetadata flag when request fails', async () => {
- mock.onGet(metadata).reply(500);
+ mock.onGet(metadata).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
await store.dispatch('fetchMrMetadata');
diff --git a/spec/frontend/nav/components/new_nav_toggle_spec.js b/spec/frontend/nav/components/new_nav_toggle_spec.js
index ee75dfb70e4..bad24345f9d 100644
--- a/spec/frontend/nav/components/new_nav_toggle_spec.js
+++ b/spec/frontend/nav/components/new_nav_toggle_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import { getByText as getByTextHelper } from '@testing-library/dom';
import { GlToggle } from '@gitlab/ui';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import NewNavToggle from '~/nav/components/new_nav_toggle.vue';
import waitForPromises from 'helpers/wait_for_promises';
@@ -74,7 +75,7 @@ describe('NewNavToggle', () => {
});
it('reloads the page on success', async () => {
- mock.onPut(TEST_ENDPONT).reply(200);
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_OK);
actFn();
await waitForPromises();
@@ -83,7 +84,7 @@ describe('NewNavToggle', () => {
});
it('shows an alert on error', async () => {
- mock.onPut(TEST_ENDPONT).reply(500);
+ mock.onPut(TEST_ENDPONT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
actFn();
await waitForPromises();
diff --git a/spec/frontend/nav/components/top_nav_app_spec.js b/spec/frontend/nav/components/top_nav_app_spec.js
index b32ab5ebe09..e70f70afc97 100644
--- a/spec/frontend/nav/components/top_nav_app_spec.js
+++ b/spec/frontend/nav/components/top_nav_app_spec.js
@@ -65,7 +65,7 @@ describe('~/nav/components/top_nav_app.vue', () => {
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_nav', {
label: 'hamburger_menu',
- property: 'top_navigation',
+ property: 'navigation_top',
});
});
});
diff --git a/spec/frontend/nav/components/top_nav_container_view_spec.js b/spec/frontend/nav/components/top_nav_container_view_spec.js
index 0218f09af0a..293fe361fa9 100644
--- a/spec/frontend/nav/components/top_nav_container_view_spec.js
+++ b/spec/frontend/nav/components/top_nav_container_view_spec.js
@@ -103,6 +103,7 @@ describe('~/nav/components/top_nav_container_view.vue', () => {
expect(findMenuSections().props()).toEqual({
sections,
withTopBorder: true,
+ isPrimarySection: false,
});
});
});
diff --git a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
index 048fca846ad..8a0340087ec 100644
--- a/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
+++ b/spec/frontend/nav/components/top_nav_dropdown_menu_spec.js
@@ -56,6 +56,7 @@ describe('~/nav/components/top_nav_dropdown_menu.vue', () => {
{ id: 'secondary', menuItems: TEST_NAV_DATA.secondary },
],
withTopBorder: false,
+ isPrimarySection: true,
});
});
diff --git a/spec/frontend/nav/components/top_nav_menu_sections_spec.js b/spec/frontend/nav/components/top_nav_menu_sections_spec.js
index 0ed5cffd93f..7a5a8475ab7 100644
--- a/spec/frontend/nav/components/top_nav_menu_sections_spec.js
+++ b/spec/frontend/nav/components/top_nav_menu_sections_spec.js
@@ -80,7 +80,11 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
}),
},
{
- classes: [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-mt-3'],
+ classes: [
+ ...TopNavMenuSections.BORDER_CLASSES.split(' '),
+ 'gl-border-gray-50',
+ 'gl-mt-3',
+ ],
menuItems: TEST_SECTIONS[1].menuItems.map((menuItem, index) => {
const classes = menuItem.type === 'header' ? [...headerClasses] : [...itemClasses];
if (index > 0) classes.push(menuItem.type === 'header' ? 'gl-pt-3!' : 'gl-mt-1');
@@ -117,8 +121,21 @@ describe('~/nav/components/top_nav_menu_sections.vue', () => {
it('renders border classes for top section', () => {
expect(findSectionModels().map((x) => x.classes)).toEqual([
- [...TopNavMenuSections.BORDER_CLASSES.split(' ')],
- [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-mt-3'],
+ [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-border-gray-50'],
+ [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-border-gray-50', 'gl-mt-3'],
+ ]);
+ });
+ });
+
+ describe('with isPrimarySection=true', () => {
+ beforeEach(() => {
+ createComponent({ isPrimarySection: true });
+ });
+
+ it('renders border classes for top section', () => {
+ expect(findSectionModels().map((x) => x.classes)).toEqual([
+ [],
+ [...TopNavMenuSections.BORDER_CLASSES.split(' '), 'gl-border-gray-100', 'gl-mt-3'],
]);
});
});
diff --git a/spec/frontend/notes/components/attachments_warning_spec.js b/spec/frontend/notes/components/attachments_warning_spec.js
new file mode 100644
index 00000000000..0e99c26ed2b
--- /dev/null
+++ b/spec/frontend/notes/components/attachments_warning_spec.js
@@ -0,0 +1,16 @@
+import { mount } from '@vue/test-utils';
+import AttachmentsWarning from '~/notes/components/attachments_warning.vue';
+
+describe('Attachments Warning Component', () => {
+ let wrapper;
+
+ beforeEach(() => {
+ wrapper = mount(AttachmentsWarning);
+ });
+
+ it('shows warning', () => {
+ const expected =
+ 'Attachments are sent by email. Attachments over 10 MB are sent as links to your GitLab instance, and only accessible to project members.';
+ expect(wrapper.text()).toBe(expected);
+ });
+});
diff --git a/spec/frontend/notes/components/comment_field_layout_spec.js b/spec/frontend/notes/components/comment_field_layout_spec.js
index 6662492fd81..93b54f95021 100644
--- a/spec/frontend/notes/components/comment_field_layout_spec.js
+++ b/spec/frontend/notes/components/comment_field_layout_spec.js
@@ -1,17 +1,13 @@
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import CommentFieldLayout from '~/notes/components/comment_field_layout.vue';
+import AttachmentsWarning from '~/notes/components/attachments_warning.vue';
import EmailParticipantsWarning from '~/notes/components/email_participants_warning.vue';
import NoteableWarning from '~/vue_shared/components/notes/noteable_warning.vue';
describe('Comment Field Layout Component', () => {
let wrapper;
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
const LOCKED_DISCUSSION_DOCS_PATH = 'docs/locked/path';
const CONFIDENTIAL_ISSUES_DOCS_PATH = 'docs/confidential/path';
@@ -22,18 +18,32 @@ describe('Comment Field Layout Component', () => {
confidential_issues_docs_path: CONFIDENTIAL_ISSUES_DOCS_PATH,
};
+ const commentFieldWithAttachmentData = {
+ noteableData: {
+ ...noteableDataMock,
+ issue_email_participants: [{ email: 'someone@gitlab.com' }, { email: 'another@gitlab.com' }],
+ },
+ containsLink: true,
+ };
+
const findIssuableNoteWarning = () => wrapper.findComponent(NoteableWarning);
const findEmailParticipantsWarning = () => wrapper.findComponent(EmailParticipantsWarning);
+ const findAttachmentsWarning = () => wrapper.findComponent(AttachmentsWarning);
const findErrorAlert = () => wrapper.findByTestId('comment-field-alert-container');
- const createWrapper = (props = {}, slots = {}) => {
+ const createWrapper = (props = {}, provide = {}) => {
wrapper = extendedWrapper(
shallowMount(CommentFieldLayout, {
propsData: {
noteableData: noteableDataMock,
...props,
},
- slots,
+ provide: {
+ glFeatures: {
+ serviceDeskNewNoteEmailNativeAttachments: true,
+ },
+ ...provide,
+ },
}),
);
};
@@ -108,23 +118,25 @@ describe('Comment Field Layout Component', () => {
expect(findEmailParticipantsWarning().exists()).toBe(false);
});
+
+ it('does not show AttachmentWarning', () => {
+ createWrapper();
+
+ expect(findAttachmentsWarning().exists()).toBe(false);
+ });
});
describe('issue has email participants', () => {
beforeEach(() => {
- createWrapper({
- noteableData: {
- ...noteableDataMock,
- issue_email_participants: [
- { email: 'someone@gitlab.com' },
- { email: 'another@gitlab.com' },
- ],
- },
- });
+ createWrapper(commentFieldWithAttachmentData);
});
it('shows EmailParticipantsWarning', () => {
- expect(findEmailParticipantsWarning().isVisible()).toBe(true);
+ expect(findEmailParticipantsWarning().exists()).toBe(true);
+ });
+
+ it('shows AttachmentsWarning', () => {
+ expect(findAttachmentsWarning().isVisible()).toBe(true);
});
it('sets EmailParticipantsWarning props', () => {
@@ -148,4 +160,22 @@ describe('Comment Field Layout Component', () => {
expect(findEmailParticipantsWarning().exists()).toBe(false);
});
});
+
+ describe('serviceDeskNewNoteEmailNativeAttachments flag', () => {
+ it('shows warning message when flag is enabled', () => {
+ createWrapper(commentFieldWithAttachmentData, {
+ glFeatures: { serviceDeskNewNoteEmailNativeAttachments: true },
+ });
+
+ expect(findAttachmentsWarning().exists()).toBe(true);
+ });
+
+ it('shows warning message when flag is disables', () => {
+ createWrapper(commentFieldWithAttachmentData, {
+ glFeatures: { serviceDeskNewNoteEmailNativeAttachments: false },
+ });
+
+ expect(findAttachmentsWarning().exists()).toBe(false);
+ });
+ });
});
diff --git a/spec/frontend/notes/components/comment_form_spec.js b/spec/frontend/notes/components/comment_form_spec.js
index e13985ef469..dfb05c85fc8 100644
--- a/spec/frontend/notes/components/comment_form_spec.js
+++ b/spec/frontend/notes/components/comment_form_spec.js
@@ -10,6 +10,7 @@ import batchComments from '~/batch_comments/stores/modules/batch_comments';
import { refreshUserMergeRequestCounts } from '~/commons/nav/user_merge_requests';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_UNPROCESSABLE_ENTITY } from '~/lib/utils/http_status';
import CommentForm from '~/notes/components/comment_form.vue';
import CommentTypeDropdown from '~/notes/components/comment_type_dropdown.vue';
import * as constants from '~/notes/constants';
@@ -162,11 +163,11 @@ describe('issue_comment_form component', () => {
});
it.each`
- httpStatus | errors
- ${400} | ${[COMMENT_FORM.GENERIC_UNSUBMITTABLE_NETWORK]}
- ${422} | ${['error 1']}
- ${422} | ${['error 1', 'error 2']}
- ${422} | ${['error 1', 'error 2', 'error 3']}
+ httpStatus | errors
+ ${400} | ${[COMMENT_FORM.GENERIC_UNSUBMITTABLE_NETWORK]}
+ ${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${['error 1']}
+ ${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${['error 1', 'error 2']}
+ ${HTTP_STATUS_UNPROCESSABLE_ENTITY} | ${['error 1', 'error 2', 'error 3']}
`(
'displays the correct errors ($errors) for a $httpStatus network response',
async ({ errors, httpStatus }) => {
@@ -198,7 +199,10 @@ describe('issue_comment_form component', () => {
store = createStore({
actions: {
saveNote: jest.fn().mockRejectedValue({
- response: { status: 422, data: { errors: { commands_only: [...commandErrors] } } },
+ response: {
+ status: HTTP_STATUS_UNPROCESSABLE_ENTITY,
+ data: { errors: { commands_only: [...commandErrors] } },
+ },
}),
},
});
diff --git a/spec/frontend/notes/components/discussion_filter_spec.js b/spec/frontend/notes/components/discussion_filter_spec.js
index ed9fc47540d..ed1ced1b3d1 100644
--- a/spec/frontend/notes/components/discussion_filter_spec.js
+++ b/spec/frontend/notes/components/discussion_filter_spec.js
@@ -7,6 +7,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import createEventHub from '~/helpers/event_hub_factory';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import DiscussionFilter from '~/notes/components/discussion_filter.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import Tracking from '~/tracking';
@@ -74,7 +75,7 @@ describe('DiscussionFilter component', () => {
// We are mocking the discussions retrieval,
// as it doesn't matter for our tests here
- mock.onGet(DISCUSSION_PATH).reply(200, '');
+ mock.onGet(DISCUSSION_PATH).reply(HTTP_STATUS_OK, '');
window.mrTabs = undefined;
wrapper = mountComponent();
jest.spyOn(Tracking, 'event');
diff --git a/spec/frontend/notes/components/note_actions_spec.js b/spec/frontend/notes/components/note_actions_spec.js
index c7420ca9c48..8630b7b7d07 100644
--- a/spec/frontend/notes/components/note_actions_spec.js
+++ b/spec/frontend/notes/components/note_actions_spec.js
@@ -7,6 +7,7 @@ import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import noteActions from '~/notes/components/note_actions.vue';
import { NOTEABLE_TYPE_MAPPING } from '~/notes/constants';
import TimelineEventButton from '~/notes/components/note_actions/timeline_event_button.vue';
+import AbuseCategorySelector from '~/abuse_reports/components/abuse_category_selector.vue';
import createStore from '~/notes/stores';
import UserAccessRoleBadge from '~/vue_shared/components/user_access_role_badge.vue';
import { userDataMock } from '../mock_data';
@@ -21,6 +22,7 @@ describe('noteActions', () => {
const findUserAccessRoleBadge = (idx) => wrapper.findAllComponents(UserAccessRoleBadge).at(idx);
const findUserAccessRoleBadgeText = (idx) => findUserAccessRoleBadge(idx).text().trim();
const findTimelineButton = () => wrapper.findComponent(TimelineEventButton);
+ const findReportAbuseButton = () => wrapper.find(`[data-testid="report-abuse-button"]`);
const setupStoreForIncidentTimelineEvents = ({
userCanAdd,
@@ -63,7 +65,6 @@ describe('noteActions', () => {
noteId: '539',
noteUrl: `${TEST_HOST}/group/project/-/merge_requests/1#note_1`,
projectName: 'project',
- reportAbusePath: `${TEST_HOST}/abuse_reports/new?ref_url=http%3A%2F%2Flocalhost%3A3000%2Fgitlab-org%2Fgitlab-ce%2Fissues%2F7%23note_539&user_id=26`,
showReply: false,
awardPath: `${TEST_HOST}/award_emoji`,
};
@@ -115,7 +116,7 @@ describe('noteActions', () => {
});
it('should be possible to report abuse to admin', () => {
- expect(wrapper.find(`a[href="${props.reportAbusePath}"]`).exists()).toBe(true);
+ expect(findReportAbuseButton().exists()).toBe(true);
});
it('should be possible to copy link to a note', () => {
@@ -373,4 +374,53 @@ describe('noteActions', () => {
});
});
});
+
+ describe('report abuse button', () => {
+ const findAbuseCategorySelector = () => wrapper.findComponent(AbuseCategorySelector);
+
+ describe('when user is not allowed to report abuse', () => {
+ beforeEach(() => {
+ store.dispatch('setUserData', userDataMock);
+ wrapper = mountNoteActions({ ...props, canReportAsAbuse: false });
+ });
+
+ it('does not render the report abuse', () => {
+ expect(findReportAbuseButton().exists()).toBe(false);
+ });
+
+ it('does not render the abuse category drawer', () => {
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
+ });
+
+ describe('when user is allowed to report abuse', () => {
+ beforeEach(() => {
+ store.dispatch('setUserData', userDataMock);
+ wrapper = mountNoteActions({ ...props, canReportAsAbuse: true });
+ });
+
+ it('renders report abuse button', () => {
+ expect(findReportAbuseButton().exists()).toBe(true);
+ });
+
+ it('does not render the abuse category drawer immediately', () => {
+ expect(findAbuseCategorySelector().exists()).toBe(false);
+ });
+
+ it('opens the drawer when report abuse button is clicked', async () => {
+ await findReportAbuseButton().trigger('click');
+
+ expect(findAbuseCategorySelector().props('showDrawer')).toEqual(true);
+ });
+
+ it('closes the drawer', async () => {
+ await findReportAbuseButton().trigger('click');
+ findAbuseCategorySelector().vm.$emit('close-drawer');
+
+ await nextTick();
+
+ expect(findAbuseCategorySelector().exists()).toEqual(false);
+ });
+ });
+ });
});
diff --git a/spec/frontend/notes/components/note_awards_list_spec.js b/spec/frontend/notes/components/note_awards_list_spec.js
index 9fc89ffa473..89ac0216f41 100644
--- a/spec/frontend/notes/components/note_awards_list_spec.js
+++ b/spec/frontend/notes/components/note_awards_list_spec.js
@@ -2,6 +2,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import Vue from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import awardsNote from '~/notes/components/note_awards_list.vue';
import createStore from '~/notes/stores';
import { noteableDataMock, notesDataMock } from '../mock_data';
@@ -17,7 +18,7 @@ describe('note_awards_list component', () => {
beforeEach(() => {
mock = new AxiosMockAdapter(axios);
- mock.onPost(toggleAwardPath).reply(200, '');
+ mock.onPost(toggleAwardPath).reply(HTTP_STATUS_OK, '');
const Component = Vue.extend(awardsNote);
diff --git a/spec/frontend/notes/components/noteable_note_spec.js b/spec/frontend/notes/components/noteable_note_spec.js
index 3d7195752d3..af1b4f64037 100644
--- a/spec/frontend/notes/components/noteable_note_spec.js
+++ b/spec/frontend/notes/components/noteable_note_spec.js
@@ -34,6 +34,9 @@ const singleLineNotePosition = {
describe('issue_note', () => {
let store;
let wrapper;
+
+ const REPORT_ABUSE_PATH = '/abuse_reports/add_category';
+
const findMultilineComment = () => wrapper.find('[data-testid="multiline-comment"]');
const createWrapper = (props = {}, storeUpdater = (s) => s) => {
@@ -62,6 +65,9 @@ describe('issue_note', () => {
'note-body',
'multiline-comment-form',
],
+ provide: {
+ reportAbusePath: REPORT_ABUSE_PATH,
+ },
});
};
@@ -241,7 +247,6 @@ describe('issue_note', () => {
expect(noteActionsProps.canDelete).toBe(note.current_user.can_edit);
expect(noteActionsProps.canReportAsAbuse).toBe(true);
expect(noteActionsProps.canResolve).toBe(false);
- expect(noteActionsProps.reportAbusePath).toBe(note.report_abuse_path);
expect(noteActionsProps.resolvable).toBe(false);
expect(noteActionsProps.isResolved).toBe(false);
expect(noteActionsProps.isResolving).toBe(false);
diff --git a/spec/frontend/notes/components/notes_app_spec.js b/spec/frontend/notes/components/notes_app_spec.js
index 0c3d0da4f0f..b08a22f8674 100644
--- a/spec/frontend/notes/components/notes_app_spec.js
+++ b/spec/frontend/notes/components/notes_app_spec.js
@@ -7,6 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import DraftNote from '~/batch_comments/components/draft_note.vue';
import batchComments from '~/batch_comments/stores/modules/batch_comments';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { getLocationHash } from '~/lib/utils/url_utility';
import * as urlUtility from '~/lib/utils/url_utility';
import CommentForm from '~/notes/components/comment_form.vue';
@@ -286,7 +287,7 @@ describe('note_app', () => {
describe('emoji awards', () => {
beforeEach(() => {
- axiosMock.onAny().reply(200, []);
+ axiosMock.onAny().reply(HTTP_STATUS_OK, []);
wrapper = mountComponent();
return waitForPromises();
});
diff --git a/spec/frontend/notes/deprecated_notes_spec.js b/spec/frontend/notes/deprecated_notes_spec.js
index f52c3e28691..6d3bc19bd45 100644
--- a/spec/frontend/notes/deprecated_notes_spec.js
+++ b/spec/frontend/notes/deprecated_notes_spec.js
@@ -7,6 +7,7 @@ import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as urlUtility from '~/lib/utils/url_utility';
// These must be imported synchronously because they pull dependencies
@@ -27,6 +28,10 @@ window.gl = window.gl || {};
gl.utils = gl.utils || {};
gl.utils.disableButtonIfEmptyField = () => {};
+function wrappedDiscussionNote(note) {
+ return `<table><tbody>${note}</tbody></table>`;
+}
+
// the following test is unreliable and failing in main 2-3 times a day
// see https://gitlab.com/gitlab-org/gitlab/issues/206906#note_290602581
// eslint-disable-next-line jest/no-disabled-tests
@@ -75,7 +80,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
describe('task lists', () => {
beforeEach(() => {
- mockAxios.onAny().reply(200, {});
+ mockAxios.onAny().reply(HTTP_STATUS_OK, {});
new Notes('', []);
});
@@ -181,7 +186,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
const $form = $('form.js-main-target-form');
$form.find('textarea.js-note-text').val(sampleComment);
- mockAxios.onPost(NOTES_POST_PATH).reply(200, noteEntity);
+ mockAxios.onPost(NOTES_POST_PATH).reply(HTTP_STATUS_OK, noteEntity);
});
it('updates note and resets edit form', () => {
@@ -435,22 +440,40 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
);
});
- it('should append to row selected with line_code', () => {
- $form.length = 0;
- note.discussion_line_code = 'line_code';
- note.diff_discussion_html = '<tr></tr>';
+ describe('HTML output', () => {
+ let line;
- const line = document.createElement('div');
- line.id = note.discussion_line_code;
- document.body.appendChild(line);
+ beforeEach(() => {
+ $form.length = 0;
+ note.discussion_line_code = 'line_code';
+ note.diff_discussion_html = '<tr></tr>';
- // Override mocks for this single test
- $form.closest.mockReset();
- $form.closest.mockReturnValue($form);
+ line = document.createElement('div');
+ line.id = note.discussion_line_code;
+ document.body.appendChild(line);
- Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+ // Override mocks for these tests
+ $form.closest.mockReset();
+ $form.closest.mockReturnValue($form);
+ });
- expect(line.nextSibling.outerHTML).toEqual(note.diff_discussion_html);
+ it('should append to row selected with line_code', () => {
+ Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+
+ expect(line.nextSibling.outerHTML).toEqual(
+ wrappedDiscussionNote(note.diff_discussion_html),
+ );
+ });
+
+ it('sanitizes the output html without stripping leading <tr> or <td> elements', () => {
+ const sanitizedDiscussion = '<tr><td><a>I am a dolphin!</a></td></tr>';
+ note.diff_discussion_html =
+ '<tr><td><a href="javascript:alert(1)">I am a dolphin!</a></td></tr>';
+
+ Notes.prototype.renderDiscussionNote.call(notes, note, $form);
+
+ expect(line.nextSibling.outerHTML).toEqual(wrappedDiscussionNote(sanitizedDiscussion));
+ });
});
});
@@ -546,7 +569,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
let $notesContainer;
function mockNotesPost() {
- mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(HTTP_STATUS_OK, note);
}
function mockNotesPostError() {
@@ -591,7 +614,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
};
mockAxios.onPost(NOTES_POST_PATH).replyOnce(() => {
expect($submitButton).toBeDisabled();
- return [200, note];
+ return [HTTP_STATUS_OK, note];
});
await notes.postComment(dummyEvent);
@@ -650,7 +673,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
beforeEach(() => {
loadHTMLFixture('commit/show.html');
- mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(HTTP_STATUS_OK, note);
new Notes('', []);
window.gon.current_username = 'root';
@@ -695,7 +718,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
note: sampleComment,
valid: true,
};
- mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(HTTP_STATUS_OK, note);
new Notes('', []);
$form = $('form.js-main-target-form');
@@ -730,7 +753,7 @@ describe.skip('Old Notes (~/deprecated_notes.js)', () => {
let $notesContainer;
beforeEach(() => {
- mockAxios.onPost(NOTES_POST_PATH).reply(200, note);
+ mockAxios.onPost(NOTES_POST_PATH).reply(HTTP_STATUS_OK, note);
new Notes('', []);
window.gon.current_username = 'root';
diff --git a/spec/frontend/notes/mock_data.js b/spec/frontend/notes/mock_data.js
index 286f2adc1d8..d5b7ad73177 100644
--- a/spec/frontend/notes/mock_data.js
+++ b/spec/frontend/notes/mock_data.js
@@ -1,4 +1,5 @@
// Copied to ee/spec/frontend/notes/mock_data.js
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { __ } from '~/locale';
export const notesDataMock = {
@@ -655,11 +656,11 @@ export const DISCUSSION_NOTE_RESPONSE_MAP = {
};
export function getIndividualNoteResponse(config) {
- return [200, INDIVIDUAL_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
+ return [HTTP_STATUS_OK, INDIVIDUAL_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
}
export function getDiscussionNoteResponse(config) {
- return [200, DISCUSSION_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
+ return [HTTP_STATUS_OK, DISCUSSION_NOTE_RESPONSE_MAP[config.method.toUpperCase()][config.url]];
}
export const notesWithDescriptionChanges = [
diff --git a/spec/frontend/notes/stores/actions_spec.js b/spec/frontend/notes/stores/actions_spec.js
index 0b2623f3d77..c4c0dc58b0d 100644
--- a/spec/frontend/notes/stores/actions_spec.js
+++ b/spec/frontend/notes/stores/actions_spec.js
@@ -7,6 +7,11 @@ import { createAlert } from '~/flash';
import toast from '~/vue_shared/plugins/global_toast';
import { EVENT_ISSUABLE_VUE_APP_CHANGE } from '~/issuable/constants';
import axios from '~/lib/utils/axios_utils';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_OK,
+ HTTP_STATUS_SERVICE_UNAVAILABLE,
+} from '~/lib/utils/http_status';
import * as notesConstants from '~/notes/constants';
import createStore from '~/notes/stores';
import * as actions from '~/notes/stores/actions';
@@ -175,7 +180,7 @@ describe('Actions Notes Store', () => {
describe('async methods', () => {
beforeEach(() => {
- axiosMock.onAny().reply(200, {});
+ axiosMock.onAny().reply(HTTP_STATUS_OK, {});
});
describe('closeMergeRequest', () => {
@@ -249,8 +254,9 @@ describe('Actions Notes Store', () => {
const pollResponse = { notes: [], last_fetched_at: '123456' };
const pollHeaders = { 'poll-interval': `${pollInterval}` };
const successMock = () =>
- axiosMock.onGet(notesDataMock.notesPath).reply(200, pollResponse, pollHeaders);
- const failureMock = () => axiosMock.onGet(notesDataMock.notesPath).reply(500);
+ axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_OK, pollResponse, pollHeaders);
+ const failureMock = () =>
+ axiosMock.onGet(notesDataMock.notesPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
const advanceAndRAF = async (time) => {
if (time) {
jest.advanceTimersByTime(time);
@@ -343,11 +349,11 @@ describe('Actions Notes Store', () => {
axiosMock
.onGet(notesDataMock.notesPath)
- .replyOnce(500) // cause one error
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR) // cause one error
.onGet(notesDataMock.notesPath)
- .replyOnce(200, pollResponse, pollHeaders) // then a success
+ .replyOnce(HTTP_STATUS_OK, pollResponse, pollHeaders) // then a success
.onGet(notesDataMock.notesPath)
- .reply(500); // and then more errors
+ .reply(HTTP_STATUS_INTERNAL_SERVER_ERROR); // and then more errors
await startPolling(); // Failure #1
await advanceXMoreIntervals(1); // Success #1
@@ -398,7 +404,7 @@ describe('Actions Notes Store', () => {
const endpoint = `${TEST_HOST}/note`;
beforeEach(() => {
- axiosMock.onDelete(endpoint).replyOnce(200, {});
+ axiosMock.onDelete(endpoint).replyOnce(HTTP_STATUS_OK, {});
document.body.dataset.page = '';
});
@@ -467,7 +473,7 @@ describe('Actions Notes Store', () => {
const endpoint = `${TEST_HOST}/note`;
beforeEach(() => {
- axiosMock.onDelete(endpoint).replyOnce(200, {});
+ axiosMock.onDelete(endpoint).replyOnce(HTTP_STATUS_OK, {});
document.body.dataset.page = '';
});
@@ -507,7 +513,7 @@ describe('Actions Notes Store', () => {
};
beforeEach(() => {
- axiosMock.onAny().reply(200, res);
+ axiosMock.onAny().reply(HTTP_STATUS_OK, res);
});
it('commits ADD_NEW_NOTE and dispatches updateMergeRequestWidget', () => {
@@ -542,7 +548,7 @@ describe('Actions Notes Store', () => {
};
beforeEach(() => {
- axiosMock.onAny().replyOnce(200, res);
+ axiosMock.onAny().replyOnce(HTTP_STATUS_OK, res);
});
it('does not commit ADD_NEW_NOTE or dispatch updateMergeRequestWidget', () => {
@@ -563,7 +569,7 @@ describe('Actions Notes Store', () => {
};
beforeEach(() => {
- axiosMock.onAny().reply(200, res);
+ axiosMock.onAny().reply(HTTP_STATUS_OK, res);
});
describe('as note', () => {
@@ -754,7 +760,7 @@ describe('Actions Notes Store', () => {
it('updates discussion if response contains disussion', () => {
const discussion = { notes: [] };
- axiosMock.onAny().reply(200, { discussion });
+ axiosMock.onAny().reply(HTTP_STATUS_OK, { discussion });
return testAction(
actions.replyToDiscussion,
@@ -773,7 +779,7 @@ describe('Actions Notes Store', () => {
it('adds a reply to a discussion', () => {
const res = {};
- axiosMock.onAny().reply(200, res);
+ axiosMock.onAny().reply(HTTP_STATUS_OK, res);
return testAction(
actions.replyToDiscussion,
@@ -1186,7 +1192,7 @@ describe('Actions Notes Store', () => {
describe('if response contains no errors', () => {
it('dispatches requestDeleteDescriptionVersion', () => {
- axiosMock.onDelete(endpoint).replyOnce(200);
+ axiosMock.onDelete(endpoint).replyOnce(HTTP_STATUS_OK);
return testAction(
actions.softDeleteDescriptionVersion,
payload,
@@ -1208,7 +1214,7 @@ describe('Actions Notes Store', () => {
describe('if response contains errors', () => {
const errorMessage = 'Request failed with status code 503';
it('dispatches receiveDeleteDescriptionVersionError and throws an error', async () => {
- axiosMock.onDelete(endpoint).replyOnce(503);
+ axiosMock.onDelete(endpoint).replyOnce(HTTP_STATUS_SERVICE_UNAVAILABLE);
await expect(
testAction(
actions.softDeleteDescriptionVersion,
@@ -1438,7 +1444,7 @@ describe('Actions Notes Store', () => {
});
it('updates the discussions and dispatches `updateResolvableDiscussionsCounts`', () => {
- axiosMock.onAny().reply(200, { discussion });
+ axiosMock.onAny().reply(HTTP_STATUS_OK, { discussion });
return testAction(
actions.fetchDiscussions,
{},
@@ -1504,7 +1510,7 @@ describe('Actions Notes Store', () => {
const actionPayload = { config, path: 'test-path', perPage: 20 };
it('updates the discussions and dispatches `updateResolvableDiscussionsCounts if there are no headers', () => {
- axiosMock.onAny().reply(200, { discussion }, {});
+ axiosMock.onAny().reply(HTTP_STATUS_OK, { discussion }, {});
return testAction(
actions.fetchDiscussionsBatch,
actionPayload,
@@ -1519,7 +1525,7 @@ describe('Actions Notes Store', () => {
});
it('dispatches itself if there is `x-next-page-cursor` header', () => {
- axiosMock.onAny().reply(200, { discussion }, { 'x-next-page-cursor': 1 });
+ axiosMock.onAny().reply(HTTP_STATUS_OK, { discussion }, { 'x-next-page-cursor': 1 });
return testAction(
actions.fetchDiscussionsBatch,
actionPayload,
diff --git a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
index 329cc15df97..601f8abd34d 100644
--- a/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
+++ b/spec/frontend/packages_and_registries/dependency_proxy/app_spec.js
@@ -17,6 +17,7 @@ import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { GRAPHQL_PAGE_SIZE } from '~/packages_and_registries/dependency_proxy/constants';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_ACCEPTED } from '~/lib/utils/http_status';
import DependencyProxyApp from '~/packages_and_registries/dependency_proxy/app.vue';
import TitleArea from '~/vue_shared/components/registry/title_area.vue';
@@ -92,7 +93,7 @@ describe('DependencyProxyApp', () => {
window.gon = { ...dummyGon };
mock = new MockAdapter(axios);
- mock.onDelete(expectedUrl).reply(202, {});
+ mock.onDelete(expectedUrl).reply(HTTP_STATUS_ACCEPTED, {});
});
afterEach(() => {
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
index a33528d2d91..801cde8582e 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/components/__snapshots__/packages_list_app_spec.js.snap
@@ -30,6 +30,7 @@ exports[`packages_list_app renders 1`] = `
<div
class="gl-max-w-full gl-m-auto"
+ data-testid="gl-empty-state-content"
>
<div
class="gl-mx-auto gl-my-0 gl-p-5"
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
index 36417eaf793..2c185e040f4 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/list/stores/actions_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import Api from '~/api';
import { createAlert } from '~/flash';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { MISSING_DELETE_PATH_ERROR } from '~/packages_and_registries/infrastructure_registry/list/constants';
import * as actions from '~/packages_and_registries/infrastructure_registry/list/stores/actions';
import * as types from '~/packages_and_registries/infrastructure_registry/list/stores/mutation_types';
@@ -182,7 +183,7 @@ describe('Actions Package list store', () => {
},
};
it('should perform a delete operation on _links.delete_api_path', () => {
- mock.onDelete(payload._links.delete_api_path).replyOnce(200);
+ mock.onDelete(payload._links.delete_api_path).replyOnce(HTTP_STATUS_OK);
Api.projectPackages = jest.fn().mockResolvedValue({ data: 'foo' });
return testAction(
@@ -198,7 +199,7 @@ describe('Actions Package list store', () => {
});
it('should stop the loading and call create flash on api error', async () => {
- mock.onDelete(payload._links.delete_api_path).replyOnce(400);
+ mock.onDelete(payload._links.delete_api_path).replyOnce(HTTP_STATUS_BAD_REQUEST);
await testAction(
actions.requestDeletePackage,
payload,
diff --git a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap
index 91824dee5b0..08e2de6c18f 100644
--- a/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/infrastructure_registry/components/shared/__snapshots__/package_list_row_spec.js.snap
@@ -101,18 +101,6 @@ exports[`packages_list_row renders 1`] = `
</div>
</div>
- <div
- class="gl-display-flex"
- >
- <div
- class="gl-w-7"
- />
-
- <!---->
-
- <div
- class="gl-w-9"
- />
- </div>
+ <!---->
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap
deleted file mode 100644
index bdd0fe3ad9e..00000000000
--- a/spec/frontend/packages_and_registries/package_registry/components/details/__snapshots__/version_row_spec.js.snap
+++ /dev/null
@@ -1,104 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`VersionRow renders 1`] = `
-<div
- class="gl-display-flex gl-flex-direction-column gl-border-b-solid gl-border-t-solid gl-border-t-1 gl-border-b-1 gl-border-t-transparent gl-border-b-gray-100"
->
- <div
- class="gl-display-flex gl-align-items-center gl-py-3"
- >
- <!---->
-
- <div
- class="gl-display-flex gl-xs-flex-direction-column gl-justify-content-space-between gl-align-items-stretch gl-flex-grow-1"
- >
- <div
- class="gl-display-flex gl-flex-direction-column gl-xs-mb-3 gl-min-w-0 gl-flex-grow-1"
- >
- <div
- class="gl-display-flex gl-align-items-center gl-text-body gl-font-weight-bold gl-min-h-6 gl-min-w-0"
- >
- <div
- class="gl-display-flex gl-align-items-center gl-mr-3 gl-min-w-0"
- >
- <gl-link-stub
- class="gl-text-body gl-min-w-0"
- href="243"
- >
- <span
- class="gl-truncate"
- data-testid="truncate-end-container"
- title="@gitlab-org/package-15"
- >
- <span
- class="gl-truncate-end"
- >
- @gitlab-org/package-15
- </span>
- </span>
- </gl-link-stub>
-
- <package-tags-stub
- class="gl-ml-3"
- hidelabel="true"
- tagdisplaylimit="1"
- tags="[object Object],[object Object],[object Object]"
- />
- </div>
-
- <!---->
- </div>
-
- <div
- class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1"
- >
-
- 1.0.1
-
- </div>
- </div>
-
- <div
- class="gl-display-flex gl-flex-direction-column gl-sm-align-items-flex-end gl-justify-content-space-between gl-text-gray-500 gl-flex-shrink-0"
- >
- <div
- class="gl-display-flex gl-align-items-center gl-sm-text-body gl-sm-font-weight-bold gl-min-h-6"
- >
- <publish-method-stub
- packageentity="[object Object]"
- />
- </div>
-
- <div
- class="gl-display-flex gl-align-items-center gl-min-h-6"
- >
- <span>
- Created
- <time-ago-tooltip-stub
- cssclass=""
- time="2021-08-10T09:33:54Z"
- tooltipplacement="top"
- />
- </span>
- </div>
- </div>
- </div>
-
- <!---->
- </div>
-
- <div
- class="gl-display-flex"
- >
- <div
- class="gl-w-7"
- />
-
- <!---->
-
- <div
- class="gl-w-9"
- />
- </div>
-</div>
-`;
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 20a459e2c1a..27c0ab96cfc 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,8 +1,16 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { stubComponent } from 'helpers/stub_component';
+import DeleteModal from '~/packages_and_registries/package_registry/components/delete_modal.vue';
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 Tracking from '~/tracking';
+import {
+ CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+} from '~/packages_and_registries/package_registry/constants';
import { packageData } from '../../mock_data';
describe('PackageVersionsList', () => {
@@ -24,6 +32,7 @@ describe('PackageVersionsList', () => {
findRegistryList: () => wrapper.findComponent(RegistryList),
findEmptySlot: () => wrapper.findComponent(EmptySlotStub),
findListRow: () => wrapper.findAllComponents(VersionRow),
+ findDeletePackagesModal: () => wrapper.findComponent(DeleteModal),
};
const mountComponent = (props) => {
wrapper = shallowMountExtended(PackageVersionsList, {
@@ -35,6 +44,11 @@ describe('PackageVersionsList', () => {
},
stubs: {
RegistryList,
+ DeleteModal: stubComponent(DeleteModal, {
+ methods: {
+ show: jest.fn(),
+ },
+ }),
},
slots: {
'empty-state': EmptySlotStub,
@@ -144,4 +158,80 @@ describe('PackageVersionsList', () => {
expect(wrapper.emitted('next-page')).toHaveLength(1);
});
});
+
+ describe('when the user can bulk destroy versions', () => {
+ let eventSpy;
+ const { findDeletePackagesModal, findRegistryList } = uiElements;
+
+ beforeEach(() => {
+ eventSpy = jest.spyOn(Tracking, 'event');
+ mountComponent({ canDestroy: true });
+ });
+
+ it('binds the right props', () => {
+ expect(uiElements.findRegistryList().props()).toMatchObject({
+ items: packageList,
+ pagination: {},
+ isLoading: false,
+ hiddenDelete: false,
+ title: '2 versions',
+ });
+ });
+
+ describe('upon deletion', () => {
+ beforeEach(() => {
+ findRegistryList().vm.$emit('delete', packageList);
+ });
+
+ it('passes itemsToBeDeleted to the modal', () => {
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toStrictEqual(packageList);
+ expect(wrapper.emitted('delete')).toBeUndefined();
+ });
+
+ it('requesting delete tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ undefined,
+ REQUEST_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+
+ describe('when modal confirms', () => {
+ beforeEach(() => {
+ findDeletePackagesModal().vm.$emit('confirm');
+ });
+
+ it('emits delete event', () => {
+ expect(wrapper.emitted('delete')[0]).toEqual([packageList]);
+ });
+
+ it('tracks the right action', () => {
+ expect(eventSpy).toHaveBeenCalledWith(
+ undefined,
+ DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+ });
+
+ it.each(['confirm', 'cancel'])(
+ 'resets itemsToBeDeleted when modal emits %s',
+ async (event) => {
+ await findDeletePackagesModal().vm.$emit(event);
+
+ expect(findDeletePackagesModal().props('itemsToBeDeleted')).toHaveLength(0);
+ },
+ );
+
+ it('canceling delete tracks the right action', () => {
+ findDeletePackagesModal().vm.$emit('cancel');
+
+ expect(eventSpy).toHaveBeenCalledWith(
+ undefined,
+ CANCEL_DELETE_PACKAGE_VERSIONS_TRACKING_ACTION,
+ expect.any(Object),
+ );
+ });
+ });
+ });
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
index faeca76d746..67340822fa5 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/details/version_row_spec.js
@@ -1,11 +1,13 @@
-import { GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
+import { GlFormCheckbox, GlIcon, GlLink, GlSprintf, GlTruncate } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
+import ListItem from '~/vue_shared/components/registry/list_item.vue';
import PackageTags from '~/packages_and_registries/shared/components/package_tags.vue';
import PublishMethod from '~/packages_and_registries/shared/components/publish_method.vue';
import VersionRow from '~/packages_and_registries/package_registry/components/details/version_row.vue';
-import ListItem from '~/vue_shared/components/registry/list_item.vue';
import TimeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
+import { PACKAGE_ERROR_STATUS } from '~/packages_and_registries/package_registry/constants';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
import { packageVersions } from '../../mock_data';
@@ -19,17 +21,23 @@ describe('VersionRow', () => {
const findPackageTags = () => wrapper.findComponent(PackageTags);
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
const findTimeAgoTooltip = () => wrapper.findComponent(TimeAgoTooltip);
+ const findPackageName = () => wrapper.findComponent(GlTruncate);
+ const findWarningIcon = () => wrapper.findComponent(GlIcon);
+ const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
- function createComponent(packageEntity = packageVersion) {
+ function createComponent({ packageEntity = packageVersion, selected = false } = {}) {
wrapper = shallowMountExtended(VersionRow, {
propsData: {
packageEntity,
+ selected,
},
stubs: {
- ListItem,
GlSprintf,
GlTruncate,
},
+ directives: {
+ GlTooltip: createMockDirective(),
+ },
});
}
@@ -37,16 +45,15 @@ describe('VersionRow', () => {
wrapper.destroy();
});
- it('renders', () => {
+ it('has a link to the version detail', () => {
createComponent();
- expect(wrapper.element).toMatchSnapshot();
+ expect(findLink().attributes('href')).toBe(`${getIdFromGraphQLId(packageVersion.id)}`);
});
- it('has a link to the version detail', () => {
+ it('lists the package name', () => {
createComponent();
- expect(findLink().attributes('href')).toBe(`${getIdFromGraphQLId(packageVersion.id)}`);
expect(findLink().text()).toBe(packageVersion.name);
});
@@ -73,17 +80,89 @@ describe('VersionRow', () => {
expect(findTimeAgoTooltip().props('time')).toBe(packageVersion.createdAt);
});
- describe('disabled status', () => {
- it('disables the list item', () => {
- createComponent({ ...packageVersion, status: 'something' });
+ describe('left action template', () => {
+ it('does not render checkbox if not permitted', () => {
+ createComponent({ packageEntity: { ...packageVersion, canDestroy: false } });
+
+ expect(findBulkDeleteAction().exists()).toBe(false);
+ });
+
+ it('renders checkbox', () => {
+ createComponent();
+
+ expect(findBulkDeleteAction().exists()).toBe(true);
+ expect(findBulkDeleteAction().attributes('checked')).toBeUndefined();
+ });
+
+ it('emits select when checked', () => {
+ createComponent();
+
+ findBulkDeleteAction().vm.$emit('change');
+
+ expect(wrapper.emitted('select')).toHaveLength(1);
+ });
+
+ it('renders checkbox in selected state if selected', () => {
+ createComponent({
+ selected: true,
+ });
+
+ expect(findBulkDeleteAction().attributes('checked')).toBe('true');
+ expect(findListItem().props('selected')).toBe(true);
+ });
+ });
+
+ describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => {
+ beforeEach(() => {
+ createComponent({
+ packageEntity: {
+ ...packageVersion,
+ status: PACKAGE_ERROR_STATUS,
+ _links: {
+ webPath: null,
+ },
+ },
+ });
+ });
+
+ it('lists the package name', () => {
+ expect(findPackageName().props('text')).toBe('@gitlab-org/package-15');
+ });
+
+ it('does not have a link to navigate to the details page', () => {
+ expect(findLink().exists()).toBe(false);
+ });
+
+ it('has a warning icon', () => {
+ const icon = findWarningIcon();
+ const tooltip = getBinding(icon.element, 'gl-tooltip');
+ expect(icon.props('name')).toBe('warning');
+ expect(icon.props('ariaLabel')).toBe('Warning');
+ expect(tooltip.value).toMatchObject({
+ title: 'Invalid Package: failed metadata extraction',
+ });
+ });
+ });
- expect(findListItem().props('disabled')).toBe(true);
+ describe('disabled status', () => {
+ beforeEach(() => {
+ createComponent({
+ packageEntity: {
+ ...packageVersion,
+ status: 'something',
+ _links: {
+ webPath: null,
+ },
+ },
+ });
});
- it('disables the link', () => {
- createComponent({ ...packageVersion, status: 'something' });
+ it('lists the package name', () => {
+ expect(findPackageName().props('text')).toBe('@gitlab-org/package-15');
+ });
- expect(findLink().attributes('disabled')).toBe('true');
+ it('does not have a link to navigate to the details page', () => {
+ expect(findLink().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js
index 93c2196b210..689b53fa2a4 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/functional/delete_package_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/functional/delete_packages_spec.js
@@ -4,36 +4,38 @@ import waitForPromises from 'helpers/wait_for_promises';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import { createAlert, VARIANT_SUCCESS, VARIANT_WARNING } from '~/flash';
-import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
+import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue';
-import destroyPackageMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_package.mutation.graphql';
+import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
import {
- packageDestroyMutation,
- packageDestroyMutationError,
+ packagesDestroyMutation,
+ packagesDestroyMutationError,
packagesListQuery,
} from '../../mock_data';
jest.mock('~/flash');
-describe('DeletePackage', () => {
+describe('DeletePackages', () => {
let wrapper;
let apolloProvider;
let resolver;
let mutationResolver;
- const eventPayload = { id: '1' };
+ const eventPayload = [{ id: '1' }];
+ const eventPayloadMultiple = [{ id: '1' }, { id: '2' }];
+ const mutationPayload = { ids: ['1'] };
function createComponent(propsData = {}) {
Vue.use(VueApollo);
const requestHandlers = [
[getPackagesQuery, resolver],
- [destroyPackageMutation, mutationResolver],
+ [destroyPackagesMutation, mutationResolver],
];
apolloProvider = createMockApollo(requestHandlers);
- wrapper = shallowMountExtended(DeletePackage, {
+ wrapper = shallowMountExtended(DeletePackages, {
propsData,
apolloProvider,
scopedSlots: {
@@ -43,7 +45,9 @@ describe('DeletePackage', () => {
'data-testid': 'trigger-button',
},
on: {
- click: props.deletePackage,
+ click: (payload) => {
+ return props.deletePackages(payload[0]);
+ },
},
});
},
@@ -54,23 +58,23 @@ describe('DeletePackage', () => {
const findButton = () => wrapper.findByTestId('trigger-button');
const clickOnButtonAndWait = (payload) => {
- findButton().trigger('click', payload);
+ findButton().trigger('click', [payload]);
return waitForPromises();
};
beforeEach(() => {
resolver = jest.fn().mockResolvedValue(packagesListQuery());
- mutationResolver = jest.fn().mockResolvedValue(packageDestroyMutation());
+ mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutation());
});
afterEach(() => {
wrapper.destroy();
});
- it('binds deletePackage method to the default slot', () => {
+ it('binds deletePackages method to the default slot', () => {
createComponent();
- findButton().trigger('click');
+ findButton().trigger('click', eventPayload);
expect(wrapper.emitted('start')).toEqual([[]]);
});
@@ -80,7 +84,7 @@ describe('DeletePackage', () => {
await clickOnButtonAndWait(eventPayload);
- expect(mutationResolver).toHaveBeenCalledWith(eventPayload);
+ expect(mutationResolver).toHaveBeenCalledWith(mutationPayload);
});
it('passes refetchQueries to apollo mutate', async () => {
@@ -91,10 +95,20 @@ describe('DeletePackage', () => {
await clickOnButtonAndWait(eventPayload);
- expect(mutationResolver).toHaveBeenCalledWith(eventPayload);
+ expect(mutationResolver).toHaveBeenCalledWith(mutationPayload);
expect(resolver).toHaveBeenCalledWith(variables);
});
+ describe('when payload contains multiple packages', () => {
+ it('calls apollo mutation with different payload', async () => {
+ createComponent();
+
+ await clickOnButtonAndWait(eventPayloadMultiple);
+
+ expect(mutationResolver).toHaveBeenCalledWith({ ids: ['1', '2'] });
+ });
+ });
+
describe('on mutation success', () => {
it('emits end event', async () => {
createComponent();
@@ -118,16 +132,29 @@ describe('DeletePackage', () => {
await clickOnButtonAndWait(eventPayload);
expect(createAlert).toHaveBeenCalledWith({
- message: DeletePackage.i18n.successMessage,
+ message: DeletePackages.i18n.successMessage,
variant: VARIANT_SUCCESS,
});
});
+
+ describe('when payload contains multiple packages', () => {
+ it('calls createAlert with success message when showSuccessAlert is true', async () => {
+ createComponent({ showSuccessAlert: true });
+
+ await clickOnButtonAndWait(eventPayloadMultiple);
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: DeletePackages.i18n.successMessageMultiple,
+ variant: VARIANT_SUCCESS,
+ });
+ });
+ });
});
describe.each`
errorType | mutationResolverResponse
${'connectionError'} | ${jest.fn().mockRejectedValue()}
- ${'localError'} | ${jest.fn().mockResolvedValue(packageDestroyMutationError())}
+ ${'localError'} | ${jest.fn().mockResolvedValue(packagesDestroyMutationError())}
`('on mutation $errorType', ({ mutationResolverResponse }) => {
beforeEach(() => {
mutationResolver = mutationResolverResponse;
@@ -147,11 +174,26 @@ describe('DeletePackage', () => {
await clickOnButtonAndWait(eventPayload);
expect(createAlert).toHaveBeenCalledWith({
- message: DeletePackage.i18n.errorMessage,
+ message: DeletePackages.i18n.errorMessage,
variant: VARIANT_WARNING,
captureError: true,
error: expect.any(Error),
});
});
+
+ describe('when payload contains multiple packages', () => {
+ it('calls createAlert with error message', async () => {
+ createComponent({ showSuccessAlert: true });
+
+ await clickOnButtonAndWait(eventPayloadMultiple);
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: DeletePackages.i18n.errorMessageMultiple,
+ variant: VARIANT_WARNING,
+ captureError: true,
+ error: expect.any(Error),
+ });
+ });
+ });
});
});
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
index a7de751aadd..ec8e77fa923 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/__snapshots__/package_list_row_spec.js.snap
@@ -54,12 +54,15 @@ exports[`packages_list_row renders 1`] = `
class="gl-display-flex gl-align-items-center gl-text-gray-500 gl-min-h-6 gl-min-w-0 gl-flex-grow-1"
>
<div
- class="gl-display-flex"
+ class="gl-display-flex gl-align-items-center"
data-testid="left-secondary-infos"
>
- <span>
- 1.0.0
- </span>
+ <gl-truncate-stub
+ class="gl-max-w-15 gl-md-max-w-26"
+ position="end"
+ text="1.0.0"
+ withtooltip="true"
+ />
<!---->
@@ -135,18 +138,6 @@ exports[`packages_list_row renders 1`] = `
</div>
</div>
- <div
- class="gl-display-flex"
- >
- <div
- class="gl-w-7"
- />
-
- <!---->
-
- <div
- class="gl-w-9"
- />
- </div>
+ <!---->
</div>
`;
diff --git a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
index bb04701a8b7..2a78cfb13f9 100644
--- a/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/components/list/package_list_row_spec.js
@@ -43,9 +43,11 @@ describe('packages_list_row', () => {
const findPackageLink = () => wrapper.findByTestId('details-link');
const findWarningIcon = () => wrapper.findByTestId('warning-icon');
const findLeftSecondaryInfos = () => wrapper.findByTestId('left-secondary-infos');
+ const findPackageVersion = () => findLeftSecondaryInfos().findComponent(GlTruncate);
const findPublishMethod = () => wrapper.findComponent(PublishMethod);
const findCreatedDateText = () => wrapper.findByTestId('created-date');
const findTimeAgoTooltip = () => wrapper.findComponent(TimeagoTooltip);
+ const findListItem = () => wrapper.findComponent(ListItem);
const findBulkDeleteAction = () => wrapper.findComponent(GlFormCheckbox);
const findPackageName = () => wrapper.findComponent(GlTruncate);
@@ -83,22 +85,13 @@ describe('packages_list_row', () => {
mountComponent();
expect(findPackageLink().props()).toMatchObject({
- event: 'click',
to: { name: 'details', params: { id: getIdFromGraphQLId(packageWithoutTags.id) } },
});
});
- it('does not have a link to navigate to the details page', () => {
- mountComponent({
- packageEntity: {
- ...packageWithoutTags,
- _links: {
- webPath: null,
- },
- },
- });
+ it('lists the package name', () => {
+ mountComponent();
- expect(findPackageLink().exists()).toBe(false);
expect(findPackageName().props()).toMatchObject({
text: '@gitlab-org/package-15',
});
@@ -155,11 +148,25 @@ describe('packages_list_row', () => {
describe(`when the package is in ${PACKAGE_ERROR_STATUS} status`, () => {
beforeEach(() => {
- mountComponent({ packageEntity: { ...packageWithoutTags, status: PACKAGE_ERROR_STATUS } });
+ mountComponent({
+ packageEntity: {
+ ...packageWithoutTags,
+ status: PACKAGE_ERROR_STATUS,
+ _links: {
+ webPath: null,
+ },
+ },
+ });
});
- it('details link is disabled', () => {
- expect(findPackageLink().props('event')).toBe('');
+ it('lists the package name', () => {
+ expect(findPackageName().props()).toMatchObject({
+ text: '@gitlab-org/package-15',
+ });
+ });
+
+ it('does not have a link to navigate to the details page', () => {
+ expect(findPackageLink().exists()).toBe(false);
});
it('has a warning icon', () => {
@@ -206,6 +213,9 @@ describe('packages_list_row', () => {
});
expect(findBulkDeleteAction().attributes('checked')).toBe('true');
+ expect(findListItem().props()).toMatchObject({
+ selected: true,
+ });
});
});
@@ -213,7 +223,10 @@ describe('packages_list_row', () => {
it('has the package version', () => {
mountComponent();
- expect(findLeftSecondaryInfos().text()).toContain(packageWithoutTags.version);
+ expect(findPackageVersion().props()).toMatchObject({
+ text: packageWithoutTags.version,
+ withTooltip: true,
+ });
});
it('if the pipeline exists show the author message', () => {
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 5e9cb8fbb0b..610640e0ca3 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
@@ -167,8 +167,8 @@ describe('packages_list', () => {
findPackageListDeleteModal().vm.$emit('ok');
});
- it('emits package:delete when modal confirms', () => {
- expect(wrapper.emitted('package:delete')[0]).toEqual([firstPackage]);
+ it('emits delete when modal confirms', () => {
+ expect(wrapper.emitted('delete')[0][0]).toEqual([firstPackage]);
});
it('tracks the right action', () => {
diff --git a/spec/frontend/packages_and_registries/package_registry/mock_data.js b/spec/frontend/packages_and_registries/package_registry/mock_data.js
index 9e9e08bc196..d897be1f344 100644
--- a/spec/frontend/packages_and_registries/package_registry/mock_data.js
+++ b/spec/frontend/packages_and_registries/package_registry/mock_data.js
@@ -97,14 +97,22 @@ export const packageProject = () => ({
__typename: 'Project',
});
+export const linksData = {
+ _links: {
+ webPath: '/gitlab-org/package-15',
+ },
+};
+
export const packageVersions = () => [
{
createdAt: '2021-08-10T09:33:54Z',
id: 'gid://gitlab/Packages::Package/243',
name: '@gitlab-org/package-15',
status: 'DEFAULT',
+ canDestroy: true,
tags: { nodes: packageTags() },
version: '1.0.1',
+ ...linksData,
__typename: 'Package',
},
{
@@ -112,19 +120,14 @@ export const packageVersions = () => [
id: 'gid://gitlab/Packages::Package/244',
name: '@gitlab-org/package-15',
status: 'DEFAULT',
+ canDestroy: true,
tags: { nodes: packageTags() },
version: '1.0.2',
+ ...linksData,
__typename: 'Package',
},
];
-export const linksData = {
- _links: {
- webPath: '/gitlab-org/package-15',
- __typeName: 'PackageLinks',
- },
-};
-
export const packageData = (extend) => ({
__typename: 'Package',
id: 'gid://gitlab/Packages::Package/111',
@@ -294,14 +297,6 @@ export const packageMetadataQuery = (packageType) => {
};
};
-export const packageDestroyMutation = () => ({
- data: {
- destroyPackage: {
- errors: [],
- },
- },
-});
-
export const packagesDestroyMutation = () => ({
data: {
destroyPackages: {
@@ -329,25 +324,6 @@ export const packagesDestroyMutationError = () => ({
],
});
-export const packageDestroyMutationError = () => ({
- data: {
- destroyPackage: null,
- },
- errors: [
- {
- message:
- "The resource that you are attempting to access does not exist or you don't have permission to perform this action",
- locations: [
- {
- line: 2,
- column: 3,
- },
- ],
- path: ['destroyPackage'],
- },
- ],
-});
-
export const packageDestroyFilesMutation = () => ({
data: {
destroyPackageFiles: {
diff --git a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
index eb3b999c1ca..b494965a3cb 100644
--- a/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
+++ b/spec/frontend/packages_and_registries/package_registry/pages/details_spec.js
@@ -15,7 +15,7 @@ import InstallationCommands from '~/packages_and_registries/package_registry/com
import PackageFiles from '~/packages_and_registries/package_registry/components/details/package_files.vue';
import PackageHistory from '~/packages_and_registries/package_registry/components/details/package_history.vue';
import PackageTitle from '~/packages_and_registries/package_registry/components/details/package_title.vue';
-import DeletePackage from '~/packages_and_registries/package_registry/components/functional/delete_package.vue';
+import DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue';
import PackageVersionsList from '~/packages_and_registries/package_registry/components/details/package_versions_list.vue';
import {
FETCH_PACKAGE_DETAILS_ERROR_MESSAGE,
@@ -85,7 +85,7 @@ describe('PackagesApp', () => {
provide,
stubs: {
PackageTitle,
- DeletePackage,
+ DeletePackages,
GlModal: {
template: `
<div>
@@ -128,7 +128,8 @@ describe('PackagesApp', () => {
const findDependenciesCountBadge = () => wrapper.findByTestId('dependencies-badge');
const findNoDependenciesMessage = () => wrapper.findByTestId('no-dependencies-message');
const findDependencyRows = () => wrapper.findAllComponents(DependencyRow);
- const findDeletePackage = () => wrapper.findComponent(DeletePackage);
+ const findDeletePackageModal = () => wrapper.findAllComponents(DeletePackages).at(1);
+ const findDeletePackages = () => wrapper.findComponent(DeletePackages);
afterEach(() => {
wrapper.destroy();
@@ -267,7 +268,7 @@ describe('PackagesApp', () => {
await waitForPromises();
- findDeletePackage().vm.$emit('end');
+ findDeletePackageModal().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith(
'projectListUrl?showSuccessDeleteAlert=true',
@@ -281,7 +282,7 @@ describe('PackagesApp', () => {
await waitForPromises();
- findDeletePackage().vm.$emit('end');
+ findDeletePackageModal().vm.$emit('end');
expect(window.location.replace).toHaveBeenCalledWith(
'groupListUrl?showSuccessDeleteAlert=true',
@@ -595,13 +596,56 @@ describe('PackagesApp', () => {
it('binds the correct props', async () => {
const versionNodes = packageVersions();
- createComponent({ packageEntity: { versions: { nodes: versionNodes } } });
+ createComponent();
+
await waitForPromises();
expect(findVersionsList().props()).toMatchObject({
+ canDestroy: true,
versions: expect.arrayContaining(versionNodes),
});
});
+
+ describe('delete packages', () => {
+ it('exists and has the correct props', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ expect(findDeletePackages().props()).toMatchObject({
+ refetchQueries: [{ query: getPackageDetails, variables: {} }],
+ showSuccessAlert: true,
+ });
+ });
+
+ it('deletePackages is bound to package-versions-list delete event', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findVersionsList().vm.$emit('delete', [{ id: 1 }]);
+
+ expect(findDeletePackages().emitted('start')).toEqual([[]]);
+ });
+
+ it('start and end event set loading correctly', async () => {
+ createComponent();
+
+ await waitForPromises();
+
+ findDeletePackages().vm.$emit('start');
+
+ await nextTick();
+
+ expect(findVersionsList().props('isLoading')).toBe(true);
+
+ findDeletePackages().vm.$emit('end');
+
+ await nextTick();
+
+ expect(findVersionsList().props('isLoading')).toBe(false);
+ });
+ });
});
describe('dependency links', () => {
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 b3cbd9f5dcf..a2ec527ce12 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,4 +1,4 @@
-import { GlAlert, GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
+import { GlEmptyState, GlSprintf, GlLink } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
@@ -8,26 +8,18 @@ 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 DeletePackages from '~/packages_and_registries/package_registry/components/functional/delete_packages.vue';
import {
PROJECT_RESOURCE_TYPE,
GROUP_RESOURCE_TYPE,
GRAPHQL_PAGE_SIZE,
EMPTY_LIST_HELP_URL,
PACKAGE_HELP_URL,
- DELETE_PACKAGES_ERROR_MESSAGE,
- DELETE_PACKAGES_SUCCESS_MESSAGE,
} from '~/packages_and_registries/package_registry/constants';
import getPackagesQuery from '~/packages_and_registries/package_registry/graphql/queries/get_packages.query.graphql';
import destroyPackagesMutation from '~/packages_and_registries/package_registry/graphql/mutations/destroy_packages.mutation.graphql';
-import {
- packagesListQuery,
- packageData,
- pagination,
- packagesDestroyMutation,
- packagesDestroyMutationError,
-} from '../mock_data';
+import { packagesListQuery, packageData, pagination } from '../mock_data';
jest.mock('~/flash');
@@ -53,12 +45,11 @@ describe('PackagesListApp', () => {
filters: { packageName: 'foo', packageType: 'CONAN' },
};
- const findAlert = () => wrapper.findComponent(GlAlert);
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 findDeletePackages = () => wrapper.findComponent(DeletePackages);
const mountComponent = ({
resolver = jest.fn().mockResolvedValue(packagesListQuery()),
@@ -82,7 +73,7 @@ describe('PackagesListApp', () => {
GlSprintf,
GlLink,
PackageList,
- DeletePackage,
+ DeletePackages,
},
});
};
@@ -243,26 +234,26 @@ describe('PackagesListApp', () => {
});
});
- describe('delete package', () => {
+ describe('delete packages', () => {
it('exists and has the correct props', async () => {
mountComponent();
await waitForFirstRequest();
- expect(findDeletePackage().props()).toMatchObject({
+ expect(findDeletePackages().props()).toMatchObject({
refetchQueries: [{ query: getPackagesQuery, variables: {} }],
showSuccessAlert: true,
});
});
- it('deletePackage is bound to package-list package:delete event', async () => {
+ it('deletePackages is bound to package-list delete event', async () => {
mountComponent();
await waitForFirstRequest();
- findListComponent().vm.$emit('package:delete', { id: 1 });
+ findListComponent().vm.$emit('delete', [{ id: 1 }]);
- expect(findDeletePackage().emitted('start')).toEqual([[]]);
+ expect(findDeletePackages().emitted('start')).toEqual([[]]);
});
it('start and end event set loading correctly', async () => {
@@ -270,59 +261,17 @@ describe('PackagesListApp', () => {
await waitForFirstRequest();
- findDeletePackage().vm.$emit('start');
+ findDeletePackages().vm.$emit('start');
await nextTick();
expect(findListComponent().props('isLoading')).toBe(true);
- findDeletePackage().vm.$emit('end');
+ findDeletePackages().vm.$emit('end');
await nextTick();
expect(findListComponent().props('isLoading')).toBe(false);
});
});
-
- describe('bulk delete package', () => {
- const items = [{ id: '1' }, { id: '2' }];
-
- it('calls mutation with the right values and shows success alert', async () => {
- const mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutation());
- mountComponent({
- mutationResolver,
- });
-
- await waitForFirstRequest();
-
- findListComponent().vm.$emit('delete', items);
-
- expect(mutationResolver).toHaveBeenCalledWith({
- ids: items.map((item) => item.id),
- });
-
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().props('variant')).toEqual('success');
- expect(findAlert().text()).toMatchInterpolatedText(DELETE_PACKAGES_SUCCESS_MESSAGE);
- });
-
- it('on error shows danger alert', async () => {
- const mutationResolver = jest.fn().mockResolvedValue(packagesDestroyMutationError());
- mountComponent({
- mutationResolver,
- });
-
- await waitForFirstRequest();
-
- findListComponent().vm.$emit('delete', items);
-
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().props('variant')).toEqual('danger');
- expect(findAlert().text()).toMatchInterpolatedText(DELETE_PACKAGES_ERROR_MESSAGE);
- });
- });
});
diff --git a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
index daf0ee85fdf..0fbbf4ae58f 100644
--- a/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
+++ b/spec/frontend/packages_and_registries/settings/project/settings/components/packages_cleanup_policy_form_spec.js
@@ -11,6 +11,7 @@ import {
KEEP_N_DUPLICATED_PACKAGE_FILES_LABEL,
KEEP_N_DUPLICATED_PACKAGE_FILES_DESCRIPTION,
} from '~/packages_and_registries/settings/project/constants';
+import packagesCleanupPolicyQuery from '~/packages_and_registries/settings/project/graphql/queries/get_packages_cleanup_policy.query.graphql';
import updatePackagesCleanupPolicyMutation from '~/packages_and_registries/settings/project/graphql/mutations/update_packages_cleanup_policy.mutation.graphql';
import Tracking from '~/tracking';
import { packagesCleanupPolicyPayload, packagesCleanupPolicyMutationPayload } from '../mock_data';
@@ -39,10 +40,13 @@ describe('Packages Cleanup Policy Settings Form', () => {
label: 'packages_cleanup_policies',
};
+ const defaultQueryResolver = jest.fn().mockResolvedValue(packagesCleanupPolicyPayload());
+
const findForm = () => wrapper.findComponent({ ref: 'form-element' });
const findSaveButton = () => wrapper.findByTestId('save-button');
const findKeepNDuplicatedPackageFilesDropdown = () =>
wrapper.findByTestId('keep-n-duplicated-package-files-dropdown');
+ const findNextRunAt = () => wrapper.findByTestId('next-run-at');
const submitForm = async () => {
findForm().trigger('submit');
@@ -77,10 +81,14 @@ describe('Packages Cleanup Policy Settings Form', () => {
const mountComponentWithApollo = ({
provide = defaultProvidedValues,
+ queryResolver = defaultQueryResolver,
mutationResolver,
queryPayload = packagesCleanupPolicyPayload(),
} = {}) => {
- const requestHandlers = [[updatePackagesCleanupPolicyMutation, mutationResolver]];
+ const requestHandlers = [
+ [updatePackagesCleanupPolicyMutation, mutationResolver],
+ [packagesCleanupPolicyQuery, queryResolver],
+ ];
fakeApollo = createMockApollo(requestHandlers);
@@ -160,6 +168,40 @@ describe('Packages Cleanup Policy Settings Form', () => {
});
});
+ describe('nextRunAt', () => {
+ it('when present renders time until next package cleanup', () => {
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
+
+ mountComponent({
+ props: { value: { ...defaultProps.value, nextRunAt: '2063-04-04T02:42:00Z' } },
+ });
+
+ expect(findNextRunAt().text()).toMatchInterpolatedText(
+ 'Packages and assets will not be deleted until cleanup runs in about 2 hours.',
+ );
+ });
+
+ it('renders message for cleanup when its before current date', () => {
+ jest.spyOn(Date, 'now').mockImplementation(() => new Date('2063-04-04T00:42:00Z').getTime());
+
+ mountComponent({
+ props: { value: { ...defaultProps.value, nextRunAt: '2063-03-04T00:42:00Z' } },
+ });
+
+ expect(findNextRunAt().text()).toMatchInterpolatedText(
+ 'Packages and assets cleanup is ready to be executed when the next cleanup job runs.',
+ );
+ });
+
+ it('when null hides time until next package cleanup', () => {
+ mountComponent({
+ props: { value: { ...defaultProps.value, nextRunAt: null } },
+ });
+
+ expect(findNextRunAt().exists()).toBe(false);
+ });
+ });
+
describe('form', () => {
describe('actions', () => {
describe('submit button', () => {
@@ -209,7 +251,7 @@ describe('Packages Cleanup Policy Settings Form', () => {
});
describe('form submit event', () => {
- it('dispatches the correct apollo mutation', () => {
+ it('dispatches the correct apollo mutation and refetches query', async () => {
const mutationResolver = jest
.fn()
.mockResolvedValue(packagesCleanupPolicyMutationPayload());
@@ -225,6 +267,12 @@ describe('Packages Cleanup Policy Settings Form', () => {
projectPath: 'path',
},
});
+
+ await waitForPromises();
+
+ expect(defaultQueryResolver).toHaveBeenCalledWith({
+ projectPath: 'path',
+ });
});
it('tracks the submit event', () => {
@@ -251,6 +299,18 @@ describe('Packages Cleanup Policy Settings Form', () => {
expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_SUCCESS_MESSAGE);
});
+ it('shows error toast when mutation responds with errors', async () => {
+ mountComponentWithApollo({
+ mutationResolver: jest
+ .fn()
+ .mockResolvedValue(packagesCleanupPolicyMutationPayload({ errors: [new Error()] })),
+ });
+
+ await submitForm();
+
+ expect(wrapper.vm.$toast.show).toHaveBeenCalledWith(UPDATE_SETTINGS_ERROR_MESSAGE);
+ });
+
describe('when submit fails', () => {
it('shows an error', async () => {
mountComponentWithApollo({
diff --git a/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js b/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js
index aaca58d21bb..2e2d5e26d33 100644
--- a/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js
+++ b/spec/frontend/packages_and_registries/shared/components/registry_list_spec.js
@@ -54,6 +54,28 @@ describe('Registry List', () => {
it('exists', () => {
expect(findSelectAll().exists()).toBe(true);
+ expect(findSelectAll().attributes('aria-label')).toBe('Select all');
+ expect(findSelectAll().attributes('disabled')).toBeUndefined();
+ expect(findSelectAll().attributes('indeterminate')).toBeUndefined();
+ });
+
+ it('sets disabled prop to true when items length is 0', () => {
+ mountComponent({ propsData: { ...defaultPropsData, items: [] } });
+
+ expect(findSelectAll().attributes('disabled')).toBe('true');
+ });
+
+ it('when few are selected, sets indeterminate prop to true', async () => {
+ await findScopedSlotSelectButton(0).trigger('click');
+
+ expect(findSelectAll().attributes('indeterminate')).toBe('true');
+ });
+
+ it('when all are selected, sets the right checkbox label', async () => {
+ findSelectAll().vm.$emit('change', true);
+ await nextTick();
+
+ expect(findSelectAll().attributes('aria-label')).toBe('Unselect all');
});
it('select and unselect all', async () => {
@@ -63,7 +85,7 @@ describe('Registry List', () => {
});
// simulate selection
- findSelectAll().vm.$emit('input', true);
+ findSelectAll().vm.$emit('change', true);
await nextTick();
// all rows selected
@@ -72,12 +94,12 @@ describe('Registry List', () => {
});
// simulate de-selection
- findSelectAll().vm.$emit('input', '');
+ findSelectAll().vm.$emit('change', false);
await nextTick();
// no row is not selected
items.forEach((item, index) => {
- expect(findScopedSlotIsSelectedValue(index).text()).toBe('');
+ expect(findScopedSlotIsSelectedValue(index).text()).toBe('false');
});
});
});
diff --git a/spec/frontend/pager_spec.js b/spec/frontend/pager_spec.js
index dfb3e87a342..2904ef547fe 100644
--- a/spec/frontend/pager_spec.js
+++ b/spec/frontend/pager_spec.js
@@ -4,6 +4,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { removeParams } from '~/lib/utils/url_utility';
import Pager from '~/pager';
@@ -49,7 +50,7 @@ describe('pager', () => {
const urlRegex = /(.*)some_list(.*)$/;
function mockSuccess(count = 0) {
- axiosMock.onGet(urlRegex).reply(200, {
+ axiosMock.onGet(urlRegex).reply(HTTP_STATUS_OK, {
count,
html: '',
});
diff --git a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
index 85ed94b748d..d422f5dade3 100644
--- a/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
+++ b/spec/frontend/pages/admin/application_settings/account_and_limits_spec.js
@@ -1,20 +1,15 @@
-import $ from 'jquery';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import initUserInternalRegexPlaceholder, {
+import initAccountAndLimitsSection, {
PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE,
PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE,
} from '~/pages/admin/application_settings/account_and_limits';
describe('AccountAndLimits', () => {
const FIXTURE = 'application_settings/accounts_and_limit.html';
- let $userDefaultExternal;
- let $userInternalRegex;
beforeEach(() => {
loadHTMLFixture(FIXTURE);
- initUserInternalRegexPlaceholder();
- $userDefaultExternal = $('#application_setting_user_default_external');
- $userInternalRegex = document.querySelector('#application_setting_user_default_internal_regex');
+ initAccountAndLimitsSection();
});
afterEach(() => {
@@ -22,18 +17,62 @@ describe('AccountAndLimits', () => {
});
describe('Changing of userInternalRegex when userDefaultExternal', () => {
+ /** @type {HTMLInputElement} */
+ let userDefaultExternalCheckbox;
+ /** @type {HTMLInputElement} */
+ let userInternalRegexInput;
+
+ beforeEach(() => {
+ userDefaultExternalCheckbox = document.getElementById(
+ 'application_setting_user_default_external',
+ );
+ userInternalRegexInput = document.getElementById(
+ 'application_setting_user_default_internal_regex',
+ );
+ });
+
it('is unchecked', () => {
- expect($userDefaultExternal.prop('checked')).toBe(false);
- expect($userInternalRegex.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE);
- expect($userInternalRegex.readOnly).toBe(true);
+ expect(userDefaultExternalCheckbox.checked).toBe(false);
+ expect(userInternalRegexInput.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_FALSE);
+ expect(userInternalRegexInput.readOnly).toBe(true);
});
it('is checked', () => {
- if (!$userDefaultExternal.prop('checked')) $userDefaultExternal.click();
+ if (!userDefaultExternalCheckbox.checked) userDefaultExternalCheckbox.click();
+
+ expect(userDefaultExternalCheckbox.checked).toBe(true);
+ expect(userInternalRegexInput.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE);
+ expect(userInternalRegexInput.readOnly).toBe(false);
+ });
+ });
+
+ describe('Dormant users period input logic', () => {
+ /** @type {HTMLInputElement} */
+ let checkbox;
+ /** @type {HTMLInputElement} */
+ let input;
+
+ const updateCheckbox = (checked) => {
+ checkbox.checked = checked;
+ checkbox.dispatchEvent(new Event('change'));
+ };
+
+ beforeEach(() => {
+ checkbox = document.getElementById('application_setting_deactivate_dormant_users');
+ input = document.getElementById('application_setting_deactivate_dormant_users_period');
+ });
+
+ it('initial state', () => {
+ expect(checkbox.checked).toBe(false);
+ expect(input.disabled).toBe(true);
+ });
+
+ it('changes field enabled flag on checkbox change', () => {
+ updateCheckbox(true);
+ expect(input.disabled).toBe(false);
- expect($userDefaultExternal.prop('checked')).toBe(true);
- expect($userInternalRegex.placeholder).toEqual(PLACEHOLDER_USER_EXTERNAL_DEFAULT_TRUE);
- expect($userInternalRegex.readOnly).toBe(false);
+ updateCheckbox(false);
+ expect(input.disabled).toBe(true);
});
});
});
diff --git a/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_modal_spec.js
index 17669331370..366d148a608 100644
--- a/spec/frontend/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
+++ b/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_modal_spec.js
@@ -4,30 +4,27 @@ import { GlModal } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
import { redirectTo } from '~/lib/utils/url_utility';
-import StopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
+import CancelJobsModal from '~/pages/admin/jobs/index/components/cancel_jobs_modal.vue';
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
redirectTo: jest.fn(),
}));
-describe('stop_jobs_modal.vue', () => {
+describe('Cancel jobs modal', () => {
const props = {
- url: `${TEST_HOST}/stop_jobs_modal.vue/stopAll`,
+ url: `${TEST_HOST}/cancel_jobs_modal.vue/cancelAll`,
+ modalId: 'cancel-jobs-modal',
};
let wrapper;
beforeEach(() => {
- wrapper = mount(StopJobsModal, { propsData: props });
- });
-
- afterEach(() => {
- wrapper.destroy();
+ wrapper = mount(CancelJobsModal, { propsData: props });
});
describe('on submit', () => {
- it('stops jobs and redirects to overview page', async () => {
- const responseURL = `${TEST_HOST}/stop_jobs_modal.vue/jobs`;
+ it('cancels jobs and redirects to overview page', async () => {
+ const responseURL = `${TEST_HOST}/cancel_jobs_modal.vue/jobs`;
// TODO: We can't use axios-mock-adapter because our current version
// does not support responseURL
//
@@ -47,10 +44,10 @@ describe('stop_jobs_modal.vue', () => {
expect(redirectTo).toHaveBeenCalledWith(responseURL);
});
- it('displays error if stopping jobs failed', async () => {
+ it('displays error if canceling jobs failed', async () => {
Vue.config.errorHandler = () => {}; // silencing thrown error
- const dummyError = new Error('stopping jobs failed');
+ const dummyError = new Error('canceling jobs failed');
// TODO: We can't use axios-mock-adapter because our current version
// does not support responseURL
//
diff --git a/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js b/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js
new file mode 100644
index 00000000000..ec6369e7119
--- /dev/null
+++ b/spec/frontend/pages/admin/jobs/index/components/cancel_jobs_spec.js
@@ -0,0 +1,57 @@
+import { GlButton } from '@gitlab/ui';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { TEST_HOST } from 'helpers/test_constants';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import CancelJobs from '~/pages/admin/jobs/index/components/cancel_jobs.vue';
+import CancelJobsModal from '~/pages/admin/jobs/index/components/cancel_jobs_modal.vue';
+import {
+ CANCEL_JOBS_MODAL_ID,
+ CANCEL_BUTTON_TOOLTIP,
+} from '~/pages/admin/jobs/index/components/constants';
+
+describe('CancelJobs component', () => {
+ let wrapper;
+
+ const findCancelJobs = () => wrapper.findComponent(CancelJobs);
+ const findButton = () => wrapper.findComponent(GlButton);
+ const findModal = () => wrapper.findComponent(CancelJobsModal);
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(CancelJobs, {
+ directives: {
+ GlModal: createMockDirective(),
+ GlTooltip: createMockDirective(),
+ },
+ propsData: {
+ url: `${TEST_HOST}/cancel_jobs_modal.vue/cancelAll`,
+ ...props,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('has correct inputs', () => {
+ expect(findCancelJobs().props().url).toBe(`${TEST_HOST}/cancel_jobs_modal.vue/cancelAll`);
+ });
+
+ it('has correct button variant', () => {
+ expect(findButton().props().variant).toBe('danger');
+ });
+
+ it('checks that button and modal are connected', () => {
+ const buttonModalDirective = getBinding(findButton().element, 'gl-modal');
+ const modalId = findModal().props('modalId');
+
+ expect(buttonModalDirective.value).toBe(CANCEL_JOBS_MODAL_ID);
+ expect(modalId).toBe(CANCEL_JOBS_MODAL_ID);
+ });
+
+ it('checks that tooltip is displayed', () => {
+ const buttonTooltipDirective = getBinding(findButton().element, 'gl-tooltip');
+
+ expect(buttonTooltipDirective.value).toBe(CANCEL_BUTTON_TOOLTIP);
+ });
+});
diff --git a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
index 909349569a8..834d14e0fb3 100644
--- a/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
+++ b/spec/frontend/pages/admin/projects/components/namespace_select_spec.js
@@ -1,99 +1,142 @@
-import { mount } from '@vue/test-utils';
+import { GlCollapsibleListbox } from '@gitlab/ui';
import { nextTick } from 'vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
+import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import NamespaceSelect from '~/pages/admin/projects/components/namespace_select.vue';
-describe('Dropdown select component', () => {
+const TEST_USER_NAMESPACE = { id: 10, kind: 'user', full_path: 'Administrator' };
+const TEST_GROUP_NAMESPACE = { id: 20, kind: 'group', full_path: 'GitLab Org' };
+
+describe('NamespaceSelect', () => {
let wrapper;
- const mountDropdown = (propsData) => {
- wrapper = mount(NamespaceSelect, { propsData });
+ const createComponent = (propsData) => {
+ wrapper = shallowMountExtended(NamespaceSelect, { propsData });
};
- const findDropdownToggle = () => wrapper.find('button.dropdown-toggle');
- const findNamespaceInput = () => wrapper.find('[data-testid="hidden-input"]');
- const findFilterInput = () => wrapper.find('.namespace-search-box input');
- const findDropdownOption = (match) => {
- const buttons = wrapper
- .findAll('button.dropdown-item')
- .filter((node) => node.text().match(match));
- return buttons.length ? buttons.at(0) : buttons;
- };
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findNamespaceInput = () => wrapper.findByTestId('hidden-input');
- const setFieldValue = async (field, value) => {
- await field.setValue(value);
- field.trigger('blur');
+ const search = async (searchString) => {
+ findListbox().vm.$emit('search', searchString);
+ await waitForPromises();
};
beforeEach(() => {
setHTMLFixture('<div class="test-container"></div>');
- jest.spyOn(Api, 'namespaces').mockImplementation((_, callback) =>
- callback([
- { id: 10, kind: 'user', full_path: 'Administrator' },
- { id: 20, kind: 'group', full_path: 'GitLab Org' },
- ]),
- );
+ jest
+ .spyOn(Api, 'namespaces')
+ .mockImplementation((_, callback) => callback([TEST_USER_NAMESPACE, TEST_GROUP_NAMESPACE]));
});
afterEach(() => {
resetHTMLFixture();
});
- it('creates a hidden input if fieldName is provided', () => {
- mountDropdown({ fieldName: 'namespace-input' });
+ describe('on mount', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('does not show hidden input', () => {
+ expect(findNamespaceInput().exists()).toBe(false);
+ });
+
+ it('sets appropriate props', async () => {
+ expect(findListbox().props()).toMatchObject({
+ items: [
+ { text: 'user: Administrator', value: '10' },
+ { text: 'group: GitLab Org', value: '20' },
+ ],
+ headerText: NamespaceSelect.i18n.headerText,
+ resetButtonLabel: NamespaceSelect.i18n.reset,
+ toggleText: 'Namespace',
+ searchPlaceholder: NamespaceSelect.i18n.searchPlaceholder,
+ searching: false,
+ searchable: true,
+ });
+ });
+ });
+
+ it('with fieldName, shows hidden input', () => {
+ createComponent({ fieldName: 'namespace-input' });
expect(findNamespaceInput().exists()).toBe(true);
expect(findNamespaceInput().attributes('name')).toBe('namespace-input');
});
- describe('clicking dropdown options', () => {
+ describe('select', () => {
+ describe.each`
+ selectId | expectToggleText
+ ${String(TEST_USER_NAMESPACE.id)} | ${`user: ${TEST_USER_NAMESPACE.full_path}`}
+ ${String(TEST_GROUP_NAMESPACE.id)} | ${`group: ${TEST_GROUP_NAMESPACE.full_path}`}
+ `('clicking listbox options (selectId=$selectId)', ({ selectId, expectToggleText }) => {
+ beforeEach(async () => {
+ createComponent({ fieldName: 'namespace-input' });
+ findListbox().vm.$emit('select', selectId);
+ await nextTick();
+ });
+
+ it('updates hidden field', () => {
+ expect(findNamespaceInput().attributes('value')).toBe(selectId);
+ });
+
+ it('updates the listbox value', async () => {
+ expect(findListbox().props()).toMatchObject({
+ selected: selectId,
+ toggleText: expectToggleText,
+ });
+ });
+
+ it('triggers a setNamespace event upon selection', () => {
+ expect(wrapper.emitted('setNamespace')).toEqual([[selectId]]);
+ });
+ });
+ });
+
+ describe('search', () => {
it('retrieves namespaces based on filter query', async () => {
- mountDropdown();
+ createComponent();
- await setFieldValue(findFilterInput(), 'test');
+ // Add space to assert that `?.trim` is called
+ await search('test ');
expect(Api.namespaces).toHaveBeenCalledWith('test', expect.anything());
});
- it('updates the dropdown value based upon selection', async () => {
- mountDropdown({ fieldName: 'namespace-input' });
-
- // wait for dropdown options to populate
- await nextTick();
-
- expect(findDropdownOption('user: Administrator').exists()).toBe(true);
- expect(findDropdownOption('group: GitLab Org').exists()).toBe(true);
- expect(findDropdownOption('group: Foobar').exists()).toBe(false);
+ it('when not found, does not change the placeholder text', async () => {
+ createComponent({
+ origSelectedId: String(TEST_USER_NAMESPACE.id),
+ origSelectedText: `user: ${TEST_USER_NAMESPACE.full_path}`,
+ });
- findDropdownOption('user: Administrator').trigger('click');
- await nextTick();
+ await search('not exist');
- expect(findNamespaceInput().attributes('value')).toBe('10');
- expect(findDropdownToggle().text()).toBe('user: Administrator');
+ expect(findListbox().props()).toMatchObject({
+ selected: String(TEST_USER_NAMESPACE.id),
+ toggleText: `user: ${TEST_USER_NAMESPACE.full_path}`,
+ });
});
+ });
- it('triggers a setNamespace event upon selection', async () => {
- mountDropdown();
-
- // wait for dropdown options to populate
- await nextTick();
-
- findDropdownOption('group: GitLab Org').trigger('click');
-
- expect(wrapper.emitted('setNamespace')).toHaveLength(1);
- expect(wrapper.emitted('setNamespace')[0][0]).toBe(20);
+ describe('reset', () => {
+ beforeEach(() => {
+ createComponent();
+ findListbox().vm.$emit('reset');
});
- it('displays "Any Namespace" option when showAny prop provided', () => {
- mountDropdown({ showAny: true });
- expect(wrapper.text()).toContain('Any namespace');
+ it('updates the listbox value', () => {
+ expect(findListbox().props()).toMatchObject({
+ selected: null,
+ toggleText: 'Namespace',
+ });
});
- it('does not display "Any Namespace" option when showAny prop not provided', () => {
- mountDropdown();
- expect(wrapper.text()).not.toContain('Any namespace');
+ it('triggers a setNamespace event upon reset', () => {
+ expect(wrapper.emitted('setNamespace')).toEqual([[null]]);
});
});
});
diff --git a/spec/frontend/pages/dashboard/todos/index/todos_spec.js b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
index 825aef27327..70d7cb9c839 100644
--- a/spec/frontend/pages/dashboard/todos/index/todos_spec.js
+++ b/spec/frontend/pages/dashboard/todos/index/todos_spec.js
@@ -3,6 +3,7 @@ import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { addDelimiter } from '~/lib/utils/text_utility';
import Todos from '~/pages/dashboard/todos/index/todos';
@@ -41,7 +42,7 @@ describe('Todos', () => {
// Arrange
mock
.onDelete(path)
- .replyOnce(200, { count: TEST_COUNT_BIG, done_count: TEST_DONE_COUNT_BIG });
+ .replyOnce(HTTP_STATUS_OK, { count: TEST_COUNT_BIG, done_count: TEST_DONE_COUNT_BIG });
onToggleSpy = jest.fn();
document.addEventListener('todo:toggle', onToggleSpy);
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 1a157beebe4..da3954b4918 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
@@ -2,6 +2,7 @@ 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';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
import LocalStorageSync from '~/vue_shared/components/local_storage_sync.vue';
import BulkImportsHistoryApp from '~/pages/import/bulk_imports/history/components/bulk_imports_history_app.vue';
@@ -78,7 +79,7 @@ describe('BulkImportsHistoryApp', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
});
afterEach(() => {
@@ -93,7 +94,7 @@ describe('BulkImportsHistoryApp', () => {
});
it('renders empty state when no data is available', async () => {
- mock.onGet(API_URL).reply(200, [], DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, [], DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
@@ -167,7 +168,7 @@ describe('BulkImportsHistoryApp', () => {
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);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, RESPONSE, DEFAULT_HEADERS);
createComponent({ shallow: false });
await axios.waitForAll();
@@ -192,7 +193,7 @@ describe('BulkImportsHistoryApp', () => {
describe('details button', () => {
beforeEach(() => {
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent({ shallow: false });
return axios.waitForAll();
});
diff --git a/spec/frontend/pages/import/history/components/import_error_details_spec.js b/spec/frontend/pages/import/history/components/import_error_details_spec.js
index 82a3e11186e..628ee8d7999 100644
--- a/spec/frontend/pages/import/history/components/import_error_details_spec.js
+++ b/spec/frontend/pages/import/history/components/import_error_details_spec.js
@@ -2,6 +2,7 @@ import { GlLoadingIcon } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ImportErrorDetails from '~/pages/import/history/components/import_error_details.vue';
describe('ImportErrorDetails', () => {
@@ -46,7 +47,7 @@ describe('ImportErrorDetails', () => {
it('renders import_error if it is available', async () => {
const FAKE_IMPORT_ERROR = 'IMPORT ERROR';
- mock.onGet(API_URL).reply(200, { import_error: FAKE_IMPORT_ERROR });
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, { import_error: FAKE_IMPORT_ERROR });
createComponent();
await axios.waitForAll();
@@ -55,7 +56,7 @@ describe('ImportErrorDetails', () => {
});
it('renders default text if error is not available', async () => {
- mock.onGet(API_URL).reply(200, { import_error: null });
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, { import_error: null });
createComponent();
await axios.waitForAll();
diff --git a/spec/frontend/pages/import/history/components/import_history_app_spec.js b/spec/frontend/pages/import/history/components/import_history_app_spec.js
index 5030adae2fa..7d79583be19 100644
--- a/spec/frontend/pages/import/history/components/import_history_app_spec.js
+++ b/spec/frontend/pages/import/history/components/import_history_app_spec.js
@@ -2,6 +2,7 @@ import { GlEmptyState, GlLoadingIcon, GlTable } from '@gitlab/ui';
import { mount, shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ImportErrorDetails from '~/pages/import/history/components/import_error_details.vue';
import ImportHistoryApp from '~/pages/import/history/components/import_history_app.vue';
import PaginationBar from '~/vue_shared/components/pagination_bar/pagination_bar.vue';
@@ -61,6 +62,7 @@ describe('ImportHistoryApp', () => {
const originalApiVersion = gon.api_version;
beforeAll(() => {
gon.api_version = 'v4';
+ gon.features = { fullPathProjectSearch: true };
});
afterAll(() => {
@@ -83,7 +85,7 @@ describe('ImportHistoryApp', () => {
});
it('renders empty state when no data is available', async () => {
- mock.onGet(API_URL).reply(200, [], DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, [], DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
@@ -92,7 +94,7 @@ describe('ImportHistoryApp', () => {
});
it('renders table with data when history is available', async () => {
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
@@ -104,7 +106,7 @@ describe('ImportHistoryApp', () => {
it('changes page when requested by pagination bar', async () => {
const NEW_PAGE = 4;
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
mock.resetHistory();
@@ -120,7 +122,7 @@ describe('ImportHistoryApp', () => {
},
];
- mock.onGet(API_URL).reply(200, FAKE_NEXT_PAGE_REPLY, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, FAKE_NEXT_PAGE_REPLY, DEFAULT_HEADERS);
wrapper.findComponent(PaginationBar).vm.$emit('set-page', NEW_PAGE);
await axios.waitForAll();
@@ -134,7 +136,7 @@ describe('ImportHistoryApp', () => {
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);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
mock.resetHistory();
@@ -151,7 +153,7 @@ describe('ImportHistoryApp', () => {
it('resets page to 1 when page size is changed', async () => {
const NEW_PAGE_SIZE = 4;
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent();
await axios.waitForAll();
wrapper.findComponent(PaginationBar).vm.$emit('set-page', 2);
@@ -169,7 +171,7 @@ describe('ImportHistoryApp', () => {
describe('details button', () => {
beforeEach(() => {
- mock.onGet(API_URL).reply(200, DUMMY_RESPONSE, DEFAULT_HEADERS);
+ mock.onGet(API_URL).reply(HTTP_STATUS_OK, DUMMY_RESPONSE, DEFAULT_HEADERS);
createComponent({ shallow: false });
return axios.waitForAll();
});
diff --git a/spec/frontend/pages/projects/find_file/ref_switcher/ref_switcher_utils_spec.js b/spec/frontend/pages/projects/find_file/ref_switcher/ref_switcher_utils_spec.js
new file mode 100644
index 00000000000..ef2e5d779d8
--- /dev/null
+++ b/spec/frontend/pages/projects/find_file/ref_switcher/ref_switcher_utils_spec.js
@@ -0,0 +1,39 @@
+import { generateRefDestinationPath } from '~/pages/projects/find_file/ref_switcher/ref_switcher_utils';
+import setWindowLocation from 'helpers/set_window_location_helper';
+
+const projectRootPath = 'root/Project1';
+const selectedRef = 'feature/test';
+
+describe('generateRefDestinationPath', () => {
+ it.each`
+ currentPath | result
+ ${`${projectRootPath}/-/find_file/flightjs/Flight`} | ${`http://test.host/${projectRootPath}/-/find_file/${selectedRef}`}
+ ${`${projectRootPath}/-/find_file/test/test1?test=something`} | ${`http://test.host/${projectRootPath}/-/find_file/${selectedRef}?test=something`}
+ ${`${projectRootPath}/-/find_file/simpletest?test=something&test=it`} | ${`http://test.host/${projectRootPath}/-/find_file/${selectedRef}?test=something&test=it`}
+ ${`${projectRootPath}/-/find_file/some_random_char?test=something&test[]=it&test[]=is`} | ${`http://test.host/${projectRootPath}/-/find_file/${selectedRef}?test=something&test[]=it&test[]=is`}
+ `('generates the correct destination path for $currentPath', ({ currentPath, result }) => {
+ setWindowLocation(currentPath);
+ expect(generateRefDestinationPath(selectedRef, '/-/find_file')).toBe(result);
+ });
+
+ it("returns original url if it's missing selectedRef param", () => {
+ setWindowLocation(`${projectRootPath}/-/find_file/flightjs/Flight`);
+ expect(generateRefDestinationPath(undefined, '/-/find_file')).toBe(
+ `http://test.host/${projectRootPath}/-/find_file/flightjs/Flight`,
+ );
+ });
+
+ it("returns original url if it's missing namespace param", () => {
+ setWindowLocation(`${projectRootPath}/-/find_file/flightjs/Flight`);
+ expect(generateRefDestinationPath(selectedRef, undefined)).toBe(
+ `http://test.host/${projectRootPath}/-/find_file/flightjs/Flight`,
+ );
+ });
+
+ it("returns original url if it's missing namespace and selectedRef param", () => {
+ setWindowLocation(`${projectRootPath}/-/find_file/flightjs/Flight`);
+ expect(generateRefDestinationPath(undefined, undefined)).toBe(
+ `http://test.host/${projectRootPath}/-/find_file/flightjs/Flight`,
+ );
+ });
+});
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 aee56247209..f0593a854b2 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
@@ -12,6 +12,7 @@ import ForkForm from '~/pages/projects/forks/new/components/fork_form.vue';
import createMockApollo from 'helpers/mock_apollo_helper';
import searchQuery from '~/pages/projects/forks/new/queries/search_forkable_namespaces.query.graphql';
import ProjectNamespace from '~/pages/projects/forks/new/components/project_namespace.vue';
+import { START_RULE, CONTAINS_RULE } from '~/projects/project_name_rules';
jest.mock('~/flash');
jest.mock('~/lib/utils/csrf', () => ({ token: 'mock-csrf-token' }));
@@ -475,6 +476,43 @@ describe('ForkForm component', () => {
expect(axios.post).not.toHaveBeenCalled();
});
+
+ describe('project name', () => {
+ it.each`
+ value | expectedErrorMessage
+ ${'?'} | ${START_RULE.msg}
+ ${'*'} | ${START_RULE.msg}
+ ${'a?'} | ${CONTAINS_RULE.msg}
+ ${'a*'} | ${CONTAINS_RULE.msg}
+ `(
+ 'shows "$expectedErrorMessage" error when value is $value',
+ async ({ value, expectedErrorMessage }) => {
+ createFullComponent();
+
+ findForkNameInput().vm.$emit('input', value);
+ await nextTick();
+ await submitForm();
+
+ const formGroup = wrapper.findComponent('[data-testid="fork-name-form-group"]');
+
+ expect(formGroup.vm.$attrs['invalid-feedback']).toBe(expectedErrorMessage);
+ expect(formGroup.vm.$attrs.description).toBe(null);
+ },
+ );
+
+ it.each(['a', '9', 'aa', '99'])('does not show error when value is %s', async (value) => {
+ createFullComponent();
+
+ findForkNameInput().vm.$emit('input', value);
+ await nextTick();
+ await submitForm();
+
+ const formGroup = wrapper.findComponent('[data-testid="fork-name-form-group"]');
+
+ expect(formGroup.vm.$attrs['invalid-feedback']).toBe('');
+ expect(formGroup.vm.$attrs.description).not.toBe(null);
+ });
+ });
});
describe('with valid form', () => {
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
deleted file mode 100644
index d67f842d011..00000000000
--- a/spec/frontend/pages/projects/graphs/__snapshots__/code_coverage_spec.js.snap
+++ /dev/null
@@ -1,109 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Code Coverage when fetching data is successful matches the snapshot 1`] = `
-<div>
- <div
- class="gl-display-flex gl-justify-content-space-between gl-align-items-center gl-border-t gl-pt-4 gl-mb-3"
- >
- <h4
- class="gl-m-0"
- sub-header=""
- >
- <gl-sprintf-stub
- message="Code coverage statistics for %{ref} %{start_date} - %{end_date}"
- />
- </h4>
-
- <gl-button-stub
- buttontextclasses=""
- category="primary"
- data-testid="download-button"
- href="url/"
- icon=""
- size="small"
- variant="default"
- >
-
- Download raw data (.csv)
-
- </gl-button-stub>
- </div>
-
- <div
- class="gl-mt-3 gl-mb-3"
- >
- <!---->
-
- <!---->
-
- <gl-base-dropdown-stub
- ariahaspopup="listbox"
- category="primary"
- icon=""
- size="medium"
- toggleid="dropdown-toggle-btn-6"
- toggletext="rspec"
- variant="default"
- >
-
- <!---->
-
- <!---->
-
- <ul
- aria-labelledby="dropdown-toggle-btn-6"
- class="gl-dropdown-contents gl-list-style-none gl-pl-0 gl-mb-0"
- id="listbox"
- role="listbox"
- tabindex="-1"
- >
- <gl-listbox-item-stub
- data-testid="listbox-item-0"
- isselected="true"
- >
-
- rspec
-
- </gl-listbox-item-stub>
- <gl-listbox-item-stub
- data-testid="listbox-item-1"
- >
-
- cypress
-
- </gl-listbox-item-stub>
- <gl-listbox-item-stub
- data-testid="listbox-item-2"
- >
-
- karma
-
- </gl-listbox-item-stub>
-
- <!---->
-
- <!---->
- </ul>
-
- <!---->
-
- </gl-base-dropdown-stub>
- </div>
-
- <gl-area-chart-stub
- annotations=""
- data="[object Object]"
- formattooltiptext="[Function]"
- height="200"
- includelegendavgmax="true"
- legendaveragetext="Avg"
- legendcurrenttext="Current"
- legendlayout="inline"
- legendmaxtext="Max"
- legendmintext="Min"
- option="[object Object]"
- responsive=""
- thresholds=""
- />
-</div>
-`;
diff --git a/spec/frontend/pages/projects/graphs/code_coverage_spec.js b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
index 2ff45266a07..5356953060a 100644
--- a/spec/frontend/pages/projects/graphs/code_coverage_spec.js
+++ b/spec/frontend/pages/projects/graphs/code_coverage_spec.js
@@ -68,10 +68,6 @@ describe('Code Coverage', () => {
expect(wrapper.vm.sortedData).toEqual(sortedDataByDates);
});
- it('matches the snapshot', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
it('shows no error messages', () => {
expect(findAlert().exists()).toBe(false);
});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap
deleted file mode 100644
index 83feb621478..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_section_card_spec.js.snap
+++ /dev/null
@@ -1,62 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Learn GitLab Section Card renders correctly 1`] = `
-<gl-card-stub
- bodyclass="gl-pt-0"
- class="gl-pt-0 h-100"
- footerclass=""
- headerclass="gl-bg-white gl-border-0 gl-pb-0"
->
- <img
- src="workspace.svg"
- />
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Set up your workspace
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Complete these tasks first so you can enjoy GitLab's features to their fullest:
- </p>
- <learn-gitlab-section-link-stub
- action="userAdded"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="issueCreated"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="gitWrite"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="mergeRequestCreated"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="securityScanEnabled"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="pipelineCreated"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="trialStarted"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="codeOwnersEnabled"
- value="[object Object]"
- />
- <learn-gitlab-section-link-stub
- action="requiredMrApprovalsEnabled"
- value="[object Object]"
- />
-</gl-card-stub>
-`;
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap b/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
deleted file mode 100644
index 6b6833b00c3..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/__snapshots__/learn_gitlab_spec.js.snap
+++ /dev/null
@@ -1,409 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`Learn GitLab renders correctly 1`] = `
-<div>
- <!---->
-
- <div
- class="row"
- >
- <div
- class="gl-mb-7 gl-ml-5"
- >
- <h1
- class="gl-font-size-h1"
- >
- Learn GitLab
- </h1>
-
- <p
- class="gl-text-gray-700 gl-mb-0"
- >
- Ready to get started with GitLab? Follow these steps to set up your workspace, plan and commit changes, and deploy your project.
- </p>
- </div>
- </div>
-
- <div
- class="gl-mb-3"
- >
- <p
- class="gl-text-gray-500 gl-mb-2"
- data-testid="completion-percentage"
- >
- 22% completed
- </p>
-
- <div
- class="progress"
- max="9"
- value="2"
- >
- <div
- aria-valuemax="9"
- aria-valuemin="0"
- aria-valuenow="2"
- class="progress-bar"
- role="progressbar"
- style="width: 22.22222222222222%;"
- />
- </div>
- </div>
-
- <div
- class="row"
- >
- <div
- class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4"
- >
- <div
- class="gl-card gl-pt-0 h-100"
- >
- <div
- class="gl-card-header gl-bg-white gl-border-0 gl-pb-0"
- >
- <img
- src="workspace.svg"
- />
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Set up your workspace
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Complete these tasks first so you can enjoy GitLab's features to their fullest:
- </p>
- </div>
-
- <div
- class="gl-card-body gl-pt-0"
- >
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <span
- class="gl-text-green-500"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="completed-icon"
- role="img"
- >
- <use
- href="#check-circle-filled"
- />
- </svg>
-
- Invite your colleagues
-
- <!---->
- </span>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <span
- class="gl-text-green-500"
- >
- <svg
- aria-hidden="true"
- class="gl-icon s16"
- data-testid="completed-icon"
- role="img"
- >
- <use
- href="#check-circle-filled"
- />
- </svg>
-
- Create a repository
-
- <!---->
- </span>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="set_up_your_first_project_s_ci_cd"
- href="http://example.com/"
- target="_self"
- >
- Set up your first project's CI/CD
- </a>
-
- <!---->
- </div>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="start_a_free_trial_of_gitlab_ultimate"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Start a free trial of GitLab Ultimate
- </a>
-
- <!---->
- </div>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="add_code_owners"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Add code owners
- </a>
-
- <span
- class="gl-font-style-italic gl-text-gray-500"
- data-testid="trial-only"
- >
-
- - Included in trial
-
- </span>
- </div>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="enable_require_merge_approvals"
- href="http://example.com/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Enable require merge approvals
- </a>
-
- <span
- class="gl-font-style-italic gl-text-gray-500"
- data-testid="trial-only"
- >
-
- - Included in trial
-
- </span>
- </div>
-
- <!---->
- </div>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4"
- >
- <div
- class="gl-card gl-pt-0 h-100"
- >
- <div
- class="gl-card-header gl-bg-white gl-border-0 gl-pb-0"
- >
- <img
- src="plan.svg"
- />
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Plan and execute
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Create a workflow for your new workspace, and learn how GitLab features work together:
- </p>
- </div>
-
- <div
- class="gl-card-body gl-pt-0"
- >
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="create_an_issue"
- href="http://example.com/"
- target="_self"
- >
- Create an issue
- </a>
-
- <!---->
- </div>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="submit_a_merge_request_mr"
- href="http://example.com/"
- target="_self"
- >
- Submit a merge request (MR)
- </a>
-
- <!---->
- </div>
-
- <!---->
- </div>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
- <div
- class="gl-mt-5 col-sm-12 col-mb-6 col-lg-4"
- >
- <div
- class="gl-card gl-pt-0 h-100"
- >
- <div
- class="gl-card-header gl-bg-white gl-border-0 gl-pb-0"
- >
- <img
- src="deploy.svg"
- />
-
- <h2
- class="gl-font-lg gl-mb-3"
- >
- Deploy
- </h2>
-
- <p
- class="gl-text-gray-700 gl-mb-6"
- >
- Use your new GitLab workflow to deploy your application, monitor its health, and keep it secure:
- </p>
- </div>
-
- <div
- class="gl-card-body gl-pt-0"
- >
- <div
- class="gl-mb-4"
- >
- <div
- class="flex align-items-center"
- >
- <div>
- <a
- class="gl-link"
- data-qa-selector="uncompleted_learn_gitlab_link"
- data-testid="uncompleted-learn-gitlab-link"
- data-track-action="click_link"
- data-track-label="run_a_security_scan_using_ci_cd"
- href="https://docs.gitlab.com/ee/foobar/"
- rel="noopener noreferrer"
- target="_blank"
- >
- Run a Security scan using CI/CD
- </a>
-
- <!---->
- </div>
-
- <!---->
- </div>
- </div>
- </div>
-
- <!---->
- </div>
- </div>
- </div>
-</div>
-`;
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_card_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_card_spec.js
deleted file mode 100644
index 3a511a009a9..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_card_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import LearnGitlabSectionCard from '~/pages/projects/learn_gitlab/components/learn_gitlab_section_card.vue';
-import { testActions } from './mock_data';
-
-const defaultSection = 'workspace';
-const testImage = 'workspace.svg';
-
-describe('Learn GitLab Section Card', () => {
- let wrapper;
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const createWrapper = () => {
- wrapper = shallowMount(LearnGitlabSectionCard, {
- propsData: { section: defaultSection, actions: testActions, svg: testImage },
- });
- };
-
- it('renders correctly', () => {
- createWrapper({ completed: false });
-
- expect(wrapper.element).toMatchSnapshot();
- });
-});
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
deleted file mode 100644
index 29335308370..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_section_link_spec.js
+++ /dev/null
@@ -1,233 +0,0 @@
-import { GlPopover, GlLink } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import { stubExperiments } from 'helpers/experimentation_helper';
-import { mockTracking, triggerEvent, unmockTracking } from 'helpers/tracking_helper';
-import eventHub from '~/invite_members/event_hub';
-import LearnGitlabSectionLink from '~/pages/projects/learn_gitlab/components/learn_gitlab_section_link.vue';
-import { ACTION_LABELS } from '~/pages/projects/learn_gitlab/constants';
-
-const defaultAction = 'gitWrite';
-const defaultProps = {
- title: 'Create Repository',
- description: 'Some description',
- url: 'https://example.com',
- completed: false,
- enabled: true,
-};
-
-const openInNewTabProps = {
- url: 'https://docs.gitlab.com/ee/user/application_security/security_dashboard/',
- openInNewTab: true,
-};
-
-describe('Learn GitLab Section Link', () => {
- let wrapper;
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
- const createWrapper = (action = defaultAction, props = {}) => {
- wrapper = extendedWrapper(
- mount(LearnGitlabSectionLink, {
- propsData: { action, value: { ...defaultProps, ...props } },
- }),
- );
- };
-
- const openInviteMembesrModalLink = () =>
- wrapper.find('[data-testid="invite-for-help-continuous-onboarding-experiment-link"]');
-
- const findUncompletedLink = () => wrapper.find('[data-testid="uncompleted-learn-gitlab-link"]');
- const findDisabledLink = () => wrapper.findByTestId('disabled-learn-gitlab-link');
- const findPopoverTrigger = () => wrapper.findByTestId('contact-admin-popover-trigger');
- const findPopover = () => wrapper.findComponent(GlPopover);
- const findPopoverLink = () => findPopover().findComponent(GlLink);
- const videoTutorialLink = () => wrapper.find('[data-testid="video-tutorial-link"]');
-
- it('renders no icon when not completed', () => {
- createWrapper(undefined, { completed: false });
-
- expect(wrapper.find('[data-testid="completed-icon"]').exists()).toBe(false);
- });
-
- it('renders the completion icon when completed', () => {
- createWrapper(undefined, { completed: true });
-
- expect(wrapper.find('[data-testid="completed-icon"]').exists()).toBe(true);
- });
-
- it('renders no trial only when it is not required', () => {
- createWrapper();
-
- expect(wrapper.find('[data-testid="trial-only"]').exists()).toBe(false);
- });
-
- it('renders trial only when trial is required', () => {
- createWrapper('codeOwnersEnabled');
-
- expect(wrapper.find('[data-testid="trial-only"]').exists()).toBe(true);
- });
-
- describe('disabled links', () => {
- beforeEach(() => {
- createWrapper('trialStarted', { enabled: false });
- });
-
- it('renders text without a link', () => {
- expect(findDisabledLink().exists()).toBe(true);
- expect(findDisabledLink().text()).toBe(ACTION_LABELS.trialStarted.title);
- expect(findDisabledLink().attributes('href')).toBeUndefined();
- });
-
- 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', () => {
- expect(findPopoverTrigger().attributes('id')).toBe(findPopover().props('target'));
- expect(findPopover().props()).toMatchObject({
- placement: 'top',
- triggers: 'hover focus',
- });
- });
-
- 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);
- });
- });
-
- describe('links marked with openInNewTab', () => {
- beforeEach(() => {
- createWrapper('securityScanEnabled', openInNewTabProps);
- });
-
- it('renders links with blank target', () => {
- const linkElement = findUncompletedLink();
-
- expect(linkElement.exists()).toBe(true);
- expect(linkElement.attributes('target')).toEqual('_blank');
- });
-
- it('tracks the click', () => {
- const trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
-
- findUncompletedLink().trigger('click');
-
- expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_link', {
- label: 'run_a_security_scan_using_ci_cd',
- });
-
- unmockTracking();
- });
- });
-
- describe('rendering a link to open the invite_members modal instead of a regular link', () => {
- it.each`
- action | experimentVariant | showModal
- ${'userAdded'} | ${'candidate'} | ${true}
- ${'userAdded'} | ${'control'} | ${false}
- ${defaultAction} | ${'candidate'} | ${false}
- ${defaultAction} | ${'control'} | ${false}
- `(
- 'when the invite_for_help_continuous_onboarding experiment has variant: $experimentVariant and action is $action, the modal link is shown: $showModal',
- ({ action, experimentVariant, showModal }) => {
- stubExperiments({ invite_for_help_continuous_onboarding: experimentVariant });
- createWrapper(action);
-
- expect(openInviteMembesrModalLink().exists()).toBe(showModal);
- },
- );
- });
-
- describe('clicking the link to open the invite_members modal', () => {
- beforeEach(() => {
- jest.spyOn(eventHub, '$emit').mockImplementation();
-
- stubExperiments({ invite_for_help_continuous_onboarding: 'candidate' });
- createWrapper('userAdded');
- });
-
- it('calls the eventHub', () => {
- openInviteMembesrModalLink().vm.$emit('click');
-
- expect(eventHub.$emit).toHaveBeenCalledWith('openModal', { source: 'learn_gitlab' });
- });
-
- it('tracks the click', async () => {
- const trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
-
- triggerEvent(openInviteMembesrModalLink().element);
-
- expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_link', {
- label: 'invite_your_colleagues',
- property: 'Growth::Activation::Experiment::InviteForHelpContinuousOnboarding',
- });
-
- unmockTracking();
- });
- });
-
- describe('video_tutorials_continuous_onboarding experiment', () => {
- describe('when control', () => {
- beforeEach(() => {
- stubExperiments({ video_tutorials_continuous_onboarding: 'control' });
- createWrapper('codeOwnersEnabled');
- });
-
- it('renders no video link', () => {
- expect(videoTutorialLink().exists()).toBe(false);
- });
- });
-
- describe('when candidate', () => {
- beforeEach(() => {
- stubExperiments({ video_tutorials_continuous_onboarding: 'candidate' });
- createWrapper('codeOwnersEnabled');
- });
-
- it('renders video link with blank target', () => {
- const videoLinkElement = videoTutorialLink();
-
- expect(videoLinkElement.exists()).toBe(true);
- expect(videoLinkElement.attributes('target')).toEqual('_blank');
- });
-
- it('tracks the click', () => {
- const trackingSpy = mockTracking('_category_', wrapper.element, jest.spyOn);
-
- videoTutorialLink().trigger('click');
-
- expect(trackingSpy).toHaveBeenCalledWith('_category_', 'click_video_link', {
- label: 'add_code_owners',
- property: 'Growth::Conversion::Experiment::LearnGitLab',
- context: {
- data: {
- experiment: 'video_tutorials_continuous_onboarding',
- variant: 'candidate',
- },
- schema: 'iglu:com.gitlab/gitlab_experiment/jsonschema/1-0-0',
- },
- });
-
- unmockTracking();
- });
- });
- });
-});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
deleted file mode 100644
index 0f63c243342..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_spec.js
+++ /dev/null
@@ -1,113 +0,0 @@
-import { GlProgressBar, GlAlert } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
-import Cookies from '~/lib/utils/cookies';
-import LearnGitlab from '~/pages/projects/learn_gitlab/components/learn_gitlab.vue';
-import eventHub from '~/invite_members/event_hub';
-import { INVITE_MODAL_OPEN_COOKIE } from '~/pages/projects/learn_gitlab/constants';
-import { testActions, testSections, testProject } from './mock_data';
-
-describe('Learn GitLab', () => {
- let wrapper;
- let sidebar;
-
- const createWrapper = () => {
- wrapper = mount(LearnGitlab, {
- propsData: {
- actions: testActions,
- sections: testSections,
- project: testProject,
- },
- });
- };
-
- beforeEach(() => {
- sidebar = document.createElement('div');
- sidebar.innerHTML = `
- <div class="sidebar-top-level-items">
- <div class="active">
- <div class="count"></div>
- </div>
- </div>
- `;
- document.body.appendChild(sidebar);
- createWrapper();
- });
-
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- sidebar.remove();
- });
-
- it('renders correctly', () => {
- expect(wrapper.element).toMatchSnapshot();
- });
-
- it('renders the progress percentage', () => {
- const text = wrapper.find('[data-testid="completion-percentage"]').text();
-
- expect(text).toBe('22% completed');
- });
-
- it('renders the progress bar with correct values', () => {
- const progressBar = wrapper.findComponent(GlProgressBar);
-
- expect(progressBar.attributes('value')).toBe('2');
- expect(progressBar.attributes('max')).toBe('9');
- });
-
- describe('Invite Members Modal', () => {
- let spy;
- let cookieSpy;
-
- beforeEach(() => {
- spy = jest.spyOn(eventHub, '$emit');
- cookieSpy = jest.spyOn(Cookies, 'remove');
- });
-
- afterEach(() => {
- Cookies.remove(INVITE_MODAL_OPEN_COOKIE);
- });
-
- it('emits openModal', () => {
- Cookies.set(INVITE_MODAL_OPEN_COOKIE, true);
-
- createWrapper();
-
- expect(spy).toHaveBeenCalledWith('openModal', {
- mode: 'celebrate',
- source: 'learn-gitlab',
- });
- expect(cookieSpy).toHaveBeenCalledWith(INVITE_MODAL_OPEN_COOKIE);
- });
-
- it('does not emit openModal when cookie is not set', () => {
- createWrapper();
-
- expect(spy).not.toHaveBeenCalled();
- expect(cookieSpy).toHaveBeenCalledWith(INVITE_MODAL_OPEN_COOKIE);
- });
- });
-
- describe('when the showSuccessfulInvitationsAlert event is fired', () => {
- const findAlert = () => wrapper.findComponent(GlAlert);
-
- beforeEach(() => {
- eventHub.$emit('showSuccessfulInvitationsAlert');
- });
-
- it('displays the successful invitations alert', () => {
- expect(findAlert().exists()).toBe(true);
- });
-
- it('displays a message with the project name', () => {
- expect(findAlert().text()).toBe(
- "Your team is growing! You've successfully invited new team members to the test-project project.",
- );
- });
-
- it('modifies the sidebar percentage', () => {
- expect(sidebar.textContent.trim()).toBe('22%');
- });
- });
-});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_trial_card_spec.js b/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_trial_card_spec.js
deleted file mode 100644
index 6ab57e31fed..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/learn_gitlab_trial_card_spec.js
+++ /dev/null
@@ -1,12 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import IncludedInTrialIndicator from '~/pages/projects/learn_gitlab/components/included_in_trial_indicator.vue';
-
-describe('Learn GitLab Trial Card', () => {
- it('renders correctly', () => {
- const wrapper = shallowMount(IncludedInTrialIndicator);
-
- expect(wrapper.text()).toEqual('- Included in trial');
-
- wrapper.destroy();
- });
-});
diff --git a/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js b/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js
deleted file mode 100644
index 1c29c68d2a9..00000000000
--- a/spec/frontend/pages/projects/learn_gitlab/components/mock_data.js
+++ /dev/null
@@ -1,73 +0,0 @@
-export const testActions = {
- gitWrite: {
- url: 'http://example.com/',
- completed: true,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- userAdded: {
- url: 'http://example.com/',
- completed: true,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- pipelineCreated: {
- url: 'http://example.com/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- trialStarted: {
- url: 'http://example.com/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- codeOwnersEnabled: {
- url: 'http://example.com/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- requiredMrApprovalsEnabled: {
- url: 'http://example.com/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- mergeRequestCreated: {
- url: 'http://example.com/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
- securityScanEnabled: {
- url: 'https://docs.gitlab.com/ee/foobar/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- openInNewTab: true,
- },
- issueCreated: {
- url: 'http://example.com/',
- completed: false,
- svg: 'http://example.com/images/illustration.svg',
- enabled: true,
- },
-};
-
-export const testSections = {
- workspace: {
- svg: 'workspace.svg',
- },
- deploy: {
- svg: 'deploy.svg',
- },
- plan: {
- svg: 'plan.svg',
- },
-};
-
-export const testProject = {
- name: 'test-project',
-};
diff --git a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
index 38f7a2e919d..ff20b72c72c 100644
--- a/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
+++ b/spec/frontend/pages/projects/shared/permissions/components/settings_panel_spec.js
@@ -75,10 +75,7 @@ describe('Settings Panel', () => {
return mountFn(settingsPanel, {
propsData,
provide: {
- glFeatures: {
- packageRegistryAccessLevel: false,
- ...glFeatures,
- },
+ glFeatures,
},
stubs,
});
@@ -110,10 +107,8 @@ describe('Settings Panel', () => {
findContainerRegistrySettings().findComponent(GlSprintf);
const findContainerRegistryAccessLevelInput = () =>
wrapper.find('[name="project[project_feature_attributes][container_registry_access_level]"]');
- const findPackageSettings = () => wrapper.findComponent({ ref: 'package-settings' });
const findPackageAccessLevel = () =>
wrapper.find('[data-testid="package-registry-access-level"]');
- const findPackagesEnabledInput = () => wrapper.find('[name="project[packages_enabled]"]');
const findPackageRegistryEnabledInput = () => wrapper.find('[name="package_registry_enabled"]');
const findPackageRegistryAccessLevelHiddenInput = () =>
wrapper.find(
@@ -512,180 +507,108 @@ describe('Settings Panel', () => {
});
describe('Packages', () => {
- it('should show the packages settings if packages are available', () => {
- wrapper = mountComponent({ packagesAvailable: true });
-
- expect(findPackageSettings().exists()).toBe(true);
- });
-
- it('should hide the packages settings if packages are not available', () => {
- wrapper = mountComponent({ packagesAvailable: false });
+ it('should hide the package access level settings with packagesAvailable = false', () => {
+ wrapper = mountComponent();
- expect(findPackageSettings().exists()).toBe(false);
+ expect(findPackageAccessLevel().exists()).toBe(false);
});
- it('should set the package settings help path', () => {
+ it('renders the package access level settings with packagesAvailable = true', () => {
wrapper = mountComponent({ packagesAvailable: true });
- expect(findPackageSettings().props('helpPath')).toBe(defaultProps.packagesHelpPath);
+ expect(findPackageAccessLevel().exists()).toBe(true);
});
- it('should enable the packages input when the repository is enabled', () => {
- wrapper = mountComponent({
- currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
- packagesAvailable: true,
- });
-
- expect(findPackagesEnabledInput().props('disabled')).toBe(false);
- });
-
- it('should disable the packages input when the repository is disabled', () => {
- wrapper = mountComponent({
- currentSettings: { repositoryAccessLevel: featureAccessLevel.NOT_ENABLED },
- packagesAvailable: true,
- });
-
- expect(findPackagesEnabledInput().props('disabled')).toBe(true);
- });
-
- it('has label for toggle', () => {
- wrapper = mountComponent({
- currentSettings: { repositoryAccessLevel: featureAccessLevel.EVERYONE },
- packagesAvailable: true,
- });
-
- expect(findPackagesEnabledInput().findComponent(GlToggle).props('label')).toBe(
- settingsPanel.i18n.packagesLabel,
- );
- });
-
- it('should hide the package access level settings', () => {
- wrapper = mountComponent();
+ it('has hidden input field for package registry access level', () => {
+ wrapper = mountComponent({ packagesAvailable: true });
- expect(findPackageAccessLevel().exists()).toBe(false);
+ expect(findPackageRegistryAccessLevelHiddenInput().exists()).toBe(true);
});
- describe('packageRegistryAccessLevel feature flag = true', () => {
- it('should hide the packages settings', () => {
+ it.each`
+ projectVisibilityLevel | packageRegistryEnabled | packageRegistryApiForEveryoneEnabled | expectedAccessLevel
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${false} | ${'disabled'} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${true} | ${false} | ${featureAccessLevel.PROJECT_MEMBERS}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${true} | ${true} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${false} | ${'disabled'} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${true} | ${false} | ${featureAccessLevel.EVERYONE}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${true} | ${true} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${false} | ${'hidden'} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${true} | ${'hidden'} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ `(
+ 'sets correct access level',
+ async ({
+ projectVisibilityLevel,
+ packageRegistryEnabled,
+ packageRegistryApiForEveryoneEnabled,
+ expectedAccessLevel,
+ }) => {
wrapper = mountComponent({
- glFeatures: { packageRegistryAccessLevel: true },
packagesAvailable: true,
+ currentSettings: {
+ visibilityLevel: projectVisibilityLevel,
+ },
});
- expect(findPackageSettings().exists()).toBe(false);
- });
-
- it('should hide the package access level settings with packagesAvailable = false', () => {
- wrapper = mountComponent({ glFeatures: { packageRegistryAccessLevel: true } });
+ await findPackageRegistryEnabledInput().vm.$emit('change', packageRegistryEnabled);
- expect(findPackageAccessLevel().exists()).toBe(false);
- });
+ const packageRegistryApiForEveryoneEnabledInput = findPackageRegistryApiForEveryoneEnabledInput();
- it('renders the package access level settings with packagesAvailable = true', () => {
- wrapper = mountComponent({
- glFeatures: { packageRegistryAccessLevel: true },
- packagesAvailable: true,
- });
+ if (packageRegistryApiForEveryoneEnabled === 'hidden') {
+ expect(packageRegistryApiForEveryoneEnabledInput.exists()).toBe(false);
+ } else if (packageRegistryApiForEveryoneEnabled === 'disabled') {
+ expect(packageRegistryApiForEveryoneEnabledInput.props('disabled')).toBe(true);
+ } else {
+ expect(packageRegistryApiForEveryoneEnabledInput.props('disabled')).toBe(false);
+ await packageRegistryApiForEveryoneEnabledInput.vm.$emit(
+ 'change',
+ packageRegistryApiForEveryoneEnabled,
+ );
+ }
- expect(findPackageAccessLevel().exists()).toBe(true);
- });
+ expect(wrapper.vm.packageRegistryAccessLevel).toBe(expectedAccessLevel);
+ },
+ );
- it('has hidden input field for package registry access level', () => {
+ it.each`
+ initialProjectVisibilityLevel | newProjectVisibilityLevel | initialAccessLevel | expectedAccessLevel
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.EVERYONE}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.EVERYONE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.PROJECT_MEMBERS}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
+ ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.EVERYONE}
+ `(
+ 'changes access level when project visibility level changed',
+ async ({
+ initialProjectVisibilityLevel,
+ newProjectVisibilityLevel,
+ initialAccessLevel,
+ expectedAccessLevel,
+ }) => {
wrapper = mountComponent({
- glFeatures: { packageRegistryAccessLevel: true },
packagesAvailable: true,
+ currentSettings: {
+ visibilityLevel: initialProjectVisibilityLevel,
+ packageRegistryAccessLevel: initialAccessLevel,
+ },
});
- expect(findPackageRegistryAccessLevelHiddenInput().exists()).toBe(true);
- });
-
- it.each`
- projectVisibilityLevel | packageRegistryEnabled | packageRegistryApiForEveryoneEnabled | expectedAccessLevel
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${false} | ${'disabled'} | ${featureAccessLevel.NOT_ENABLED}
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${true} | ${false} | ${featureAccessLevel.PROJECT_MEMBERS}
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${true} | ${true} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${false} | ${'disabled'} | ${featureAccessLevel.NOT_ENABLED}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${true} | ${false} | ${featureAccessLevel.EVERYONE}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${true} | ${true} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${false} | ${'hidden'} | ${featureAccessLevel.NOT_ENABLED}
- ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${true} | ${'hidden'} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- `(
- 'sets correct access level',
- async ({
- projectVisibilityLevel,
- packageRegistryEnabled,
- packageRegistryApiForEveryoneEnabled,
- expectedAccessLevel,
- }) => {
- wrapper = mountComponent({
- glFeatures: { packageRegistryAccessLevel: true },
- packagesAvailable: true,
- currentSettings: {
- visibilityLevel: projectVisibilityLevel,
- },
- });
-
- await findPackageRegistryEnabledInput().vm.$emit('change', packageRegistryEnabled);
-
- const packageRegistryApiForEveryoneEnabledInput = findPackageRegistryApiForEveryoneEnabledInput();
-
- if (packageRegistryApiForEveryoneEnabled === 'hidden') {
- expect(packageRegistryApiForEveryoneEnabledInput.exists()).toBe(false);
- } else if (packageRegistryApiForEveryoneEnabled === 'disabled') {
- expect(packageRegistryApiForEveryoneEnabledInput.props('disabled')).toBe(true);
- } else {
- expect(packageRegistryApiForEveryoneEnabledInput.props('disabled')).toBe(false);
- await packageRegistryApiForEveryoneEnabledInput.vm.$emit(
- 'change',
- packageRegistryApiForEveryoneEnabled,
- );
- }
-
- expect(wrapper.vm.packageRegistryAccessLevel).toBe(expectedAccessLevel);
- },
- );
+ await findProjectVisibilityLevelInput().setValue(newProjectVisibilityLevel);
- it.each`
- initialProjectVisibilityLevel | newProjectVisibilityLevel | initialAccessLevel | expectedAccessLevel
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${featureAccessLevel.EVERYONE}
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.PROJECT_MEMBERS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.EVERYONE} | ${featureAccessLevel.PROJECT_MEMBERS}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${featureAccessLevel.EVERYONE} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS}
- ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_PRIVATE_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.PROJECT_MEMBERS}
- ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${featureAccessLevel.NOT_ENABLED} | ${featureAccessLevel.NOT_ENABLED}
- ${VISIBILITY_LEVEL_PUBLIC_INTEGER} | ${VISIBILITY_LEVEL_INTERNAL_INTEGER} | ${FEATURE_ACCESS_LEVEL_ANONYMOUS} | ${featureAccessLevel.EVERYONE}
- `(
- 'changes access level when project visibility level changed',
- async ({
- initialProjectVisibilityLevel,
- newProjectVisibilityLevel,
- initialAccessLevel,
- expectedAccessLevel,
- }) => {
- wrapper = mountComponent({
- glFeatures: { packageRegistryAccessLevel: true },
- packagesAvailable: true,
- currentSettings: {
- visibilityLevel: initialProjectVisibilityLevel,
- packageRegistryAccessLevel: initialAccessLevel,
- },
- });
-
- await findProjectVisibilityLevelInput().setValue(newProjectVisibilityLevel);
-
- expect(wrapper.vm.packageRegistryAccessLevel).toBe(expectedAccessLevel);
- },
- );
- });
+ expect(wrapper.vm.packageRegistryAccessLevel).toBe(expectedAccessLevel);
+ },
+ );
});
describe('Pages', () => {
diff --git a/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap b/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap
deleted file mode 100644
index ce456d6c899..00000000000
--- a/spec/frontend/pages/search/show/__snapshots__/refresh_counts_spec.js.snap
+++ /dev/null
@@ -1,7 +0,0 @@
-// Jest Snapshot v1, https://goo.gl/fbAQLP
-
-exports[`pages/search/show/refresh_counts fetches and displays search counts 1`] = `
-"<div class=\\"badge\\">22</div>
-<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&amp;project_id=3&amp;scope=issues\\">4</div>
-<div class=\\"badge js-search-count\\" data-url=\\"http://test.host/search/count?search=lorem+ipsum&amp;project_id=3&amp;scope=merge_requests\\">5</div>"
-`;
diff --git a/spec/frontend/pages/search/show/refresh_counts_spec.js b/spec/frontend/pages/search/show/refresh_counts_spec.js
deleted file mode 100644
index 6f14f0c70bd..00000000000
--- a/spec/frontend/pages/search/show/refresh_counts_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import { TEST_HOST } from 'helpers/test_constants';
-import axios from '~/lib/utils/axios_utils';
-import refreshCounts from '~/pages/search/show/refresh_counts';
-
-const URL = `${TEST_HOST}/search/count?search=lorem+ipsum&project_id=3`;
-const urlWithScope = (scope) => `${URL}&scope=${scope}`;
-const counts = [
- { scope: 'issues', count: 4 },
- { scope: 'merge_requests', count: 5 },
-];
-const fixture = `<div class="badge">22</div>
-<div class="badge js-search-count hidden" data-url="${urlWithScope('issues')}"></div>
-<div class="badge js-search-count hidden" data-url="${urlWithScope('merge_requests')}"></div>`;
-
-describe('pages/search/show/refresh_counts', () => {
- let mock;
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- setHTMLFixture(fixture);
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- afterEach(() => {
- mock.restore();
- });
-
- it('fetches and displays search counts', () => {
- counts.forEach(({ scope, count }) => {
- mock.onGet(urlWithScope(scope)).reply(200, { count });
- });
-
- // assert before act behavior
- return refreshCounts().then(() => {
- expect(document.body.innerHTML).toMatchSnapshot();
- });
- });
-});
diff --git a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
index 67d0fbdd9d1..ffcfd1d9f78 100644
--- a/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
+++ b/spec/frontend/pages/shared/wikis/components/wiki_form_spec.js
@@ -110,17 +110,22 @@ describe('WikiForm', () => {
it('displays markdown editor', () => {
createWrapper({ persisted: true });
- expect(findMarkdownEditor().props()).toEqual(
+ const markdownEditor = findMarkdownEditor();
+
+ expect(markdownEditor.props()).toEqual(
expect.objectContaining({
value: pageInfoPersisted.content,
renderMarkdownPath: pageInfoPersisted.markdownPreviewPath,
markdownDocsPath: pageInfoPersisted.markdownHelpPath,
uploadsPath: pageInfoPersisted.uploadsPath,
autofocus: pageInfoPersisted.persisted,
- formFieldId: 'wiki_content',
- formFieldName: 'wiki[content]',
}),
);
+
+ expect(markdownEditor.props('formFieldProps')).toMatchObject({
+ id: 'wiki_content',
+ name: 'wiki[content]',
+ });
});
it.each`
diff --git a/spec/frontend/performance_bar/index_spec.js b/spec/frontend/performance_bar/index_spec.js
index 2da176dbfe4..f09b0cc3df8 100644
--- a/spec/frontend/performance_bar/index_spec.js
+++ b/spec/frontend/performance_bar/index_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import '~/performance_bar/components/performance_bar_app.vue';
import performanceBar from '~/performance_bar';
import PerformanceBarService from '~/performance_bar/services/performance_bar_service';
@@ -26,7 +27,7 @@ describe('performance bar wrapper', () => {
mock = new MockAdapter(axios);
mock.onGet('/-/peek/results').reply(
- 200,
+ HTTP_STATUS_OK,
{
data: {
gc: {
diff --git a/spec/frontend/persistent_user_callout_spec.js b/spec/frontend/persistent_user_callout_spec.js
index c9574208900..6519989661f 100644
--- a/spec/frontend/persistent_user_callout_spec.js
+++ b/spec/frontend/persistent_user_callout_spec.js
@@ -3,6 +3,7 @@ import { useMockLocationHelper } from 'helpers/mock_window_location_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PersistentUserCallout from '~/persistent_user_callout';
jest.mock('~/flash');
@@ -88,7 +89,7 @@ describe('PersistentUserCallout', () => {
${'primary'}
${'secondary'}
`('POSTs endpoint and removes container when clicking $button close', async ({ button }) => {
- mockAxios.onPost(dismissEndpoint).replyOnce(200);
+ mockAxios.onPost(dismissEndpoint).replyOnce(HTTP_STATUS_OK);
buttons[button].click();
@@ -101,7 +102,7 @@ describe('PersistentUserCallout', () => {
});
it('invokes Flash when the dismiss request fails', async () => {
- mockAxios.onPost(dismissEndpoint).replyOnce(500);
+ mockAxios.onPost(dismissEndpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
buttons.primary.click();
@@ -140,7 +141,7 @@ describe('PersistentUserCallout', () => {
it('defers loading of a link until callout is dismissed', async () => {
const { href, target } = deferredLink;
- mockAxios.onPost(dismissEndpoint).replyOnce(200);
+ mockAxios.onPost(dismissEndpoint).replyOnce(HTTP_STATUS_OK);
deferredLink.click();
@@ -161,7 +162,7 @@ describe('PersistentUserCallout', () => {
});
it('does not follow link when notification is closed', async () => {
- mockAxios.onPost(dismissEndpoint).replyOnce(200);
+ mockAxios.onPost(dismissEndpoint).replyOnce(HTTP_STATUS_OK);
button.click();
@@ -195,7 +196,7 @@ describe('PersistentUserCallout', () => {
it('uses a link to trigger callout and defers following until callout is finished', async () => {
const { href } = link;
- mockAxios.onPost(dismissEndpoint).replyOnce(200);
+ mockAxios.onPost(dismissEndpoint).replyOnce(HTTP_STATUS_OK);
link.click();
@@ -207,7 +208,7 @@ describe('PersistentUserCallout', () => {
});
it('invokes Flash when the dismiss request fails', async () => {
- mockAxios.onPost(dismissEndpoint).replyOnce(500);
+ mockAxios.onPost(dismissEndpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
link.click();
diff --git a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
index b7a9297d856..ab2056b4035 100644
--- a/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
+++ b/spec/frontend/pipelines/components/pipeline_mini_graph/pipeline_stage_spec.js
@@ -4,6 +4,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import CiIcon from '~/vue_shared/components/ci_icon.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PipelineStage from '~/pipelines/components/pipeline_mini_graph/pipeline_stage.vue';
import eventHub from '~/pipelines/event_hub';
import waitForPromises from 'helpers/wait_for_promises';
@@ -72,7 +73,7 @@ describe('Pipelines stage component', () => {
beforeEach(async () => {
createComponent({ updateDropdown: true });
- mock.onGet(dropdownPath).reply(200, stageReply);
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
await openStageDropdown();
});
@@ -121,7 +122,7 @@ describe('Pipelines stage component', () => {
describe('when user opens dropdown and stage request is successful', () => {
beforeEach(async () => {
- mock.onGet(dropdownPath).reply(200, stageReply);
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
createComponent();
await openStageDropdown();
@@ -148,7 +149,7 @@ describe('Pipelines stage component', () => {
describe('when user opens dropdown and stage request fails', () => {
it('should close the dropdown', async () => {
- mock.onGet(dropdownPath).reply(500);
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
await openStageDropdown();
@@ -163,7 +164,7 @@ describe('Pipelines stage component', () => {
beforeEach(async () => {
const copyStage = { ...stageReply };
copyStage.latest_statuses[0].name = 'this is the updated content';
- mock.onGet('bar.json').reply(200, copyStage);
+ mock.onGet('bar.json').reply(HTTP_STATUS_OK, copyStage);
createComponent({
stage: {
status: {
@@ -188,8 +189,8 @@ describe('Pipelines stage component', () => {
describe('job update in dropdown', () => {
beforeEach(async () => {
- mock.onGet(dropdownPath).reply(200, stageReply);
- mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(200);
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
+ mock.onPost(`${stageReply.latest_statuses[0].status.action.path}.json`).reply(HTTP_STATUS_OK);
createComponent();
await waitForPromises();
@@ -214,7 +215,7 @@ describe('Pipelines stage component', () => {
describe('With merge trains enabled', () => {
it('shows a warning on the dropdown', async () => {
- mock.onGet(dropdownPath).reply(200, stageReply);
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
createComponent({
isMergeTrain: true,
});
@@ -231,7 +232,7 @@ describe('Pipelines stage component', () => {
describe('With merge trains disabled', () => {
beforeEach(async () => {
- mock.onGet(dropdownPath).reply(200, stageReply);
+ mock.onGet(dropdownPath).reply(HTTP_STATUS_OK, stageReply);
createComponent();
await openStageDropdown();
diff --git a/spec/frontend/pipelines/graph/action_component_spec.js b/spec/frontend/pipelines/graph/action_component_spec.js
index a823e029281..e3eea503b46 100644
--- a/spec/frontend/pipelines/graph/action_component_spec.js
+++ b/spec/frontend/pipelines/graph/action_component_spec.js
@@ -4,6 +4,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 { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ActionComponent from '~/pipelines/components/jobs_shared/action_component.vue';
describe('pipeline graph action component', () => {
@@ -12,18 +13,22 @@ describe('pipeline graph action component', () => {
const findButton = () => wrapper.findComponent(GlButton);
const findTooltipWrapper = () => wrapper.find('[data-testid="ci-action-icon-tooltip-wrapper"]');
- beforeEach(() => {
- mock = new MockAdapter(axios);
-
- mock.onPost('foo.json').reply(200);
+ const defaultProps = {
+ tooltipText: 'bar',
+ link: 'foo',
+ actionIcon: 'cancel',
+ };
+ const createComponent = ({ props } = {}) => {
wrapper = mount(ActionComponent, {
- propsData: {
- tooltipText: 'bar',
- link: 'foo',
- actionIcon: 'cancel',
- },
+ propsData: { ...defaultProps, ...props },
});
+ };
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+
+ mock.onPost('foo.json').reply(HTTP_STATUS_OK);
});
afterEach(() => {
@@ -31,31 +36,39 @@ describe('pipeline graph action component', () => {
wrapper.destroy();
});
- it('should render the provided title as a bootstrap tooltip', () => {
- expect(findTooltipWrapper().attributes('title')).toBe('bar');
- });
+ describe('render', () => {
+ beforeEach(() => {
+ createComponent();
+ });
- it('should update bootstrap tooltip when title changes', async () => {
- wrapper.setProps({ tooltipText: 'changed' });
+ it('should render the provided title as a bootstrap tooltip', () => {
+ expect(findTooltipWrapper().attributes('title')).toBe('bar');
+ });
- await nextTick();
- expect(findTooltipWrapper().attributes('title')).toBe('changed');
- });
+ it('should update bootstrap tooltip when title changes', async () => {
+ wrapper.setProps({ tooltipText: 'changed' });
- it('should render an svg', () => {
- expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true);
- expect(wrapper.find('svg').exists()).toBe(true);
+ await nextTick();
+ expect(findTooltipWrapper().attributes('title')).toBe('changed');
+ });
+
+ it('should render an svg', () => {
+ expect(wrapper.find('.ci-action-icon-wrapper').exists()).toBe(true);
+ expect(wrapper.find('svg').exists()).toBe(true);
+ });
});
describe('on click', () => {
- it('emits `pipelineActionRequestComplete` after a successful request', async () => {
- jest.spyOn(wrapper.vm, '$emit');
+ beforeEach(() => {
+ createComponent();
+ });
+ it('emits `pipelineActionRequestComplete` after a successful request', async () => {
findButton().trigger('click');
await waitForPromises();
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('pipelineActionRequestComplete');
+ expect(wrapper.emitted().pipelineActionRequestComplete).toHaveLength(1);
});
it('renders a loading icon while waiting for request', async () => {
@@ -65,4 +78,40 @@ describe('pipeline graph action component', () => {
expect(wrapper.find('.js-action-icon-loading').exists()).toBe(true);
});
});
+
+ describe('when has a confirmation modal', () => {
+ beforeEach(() => {
+ createComponent({ props: { withConfirmationModal: true, shouldTriggerClick: false } });
+ });
+
+ describe('and a first click is initiated', () => {
+ beforeEach(async () => {
+ findButton().trigger('click');
+
+ await waitForPromises();
+ });
+
+ it('emits `showActionConfirmationModal` event', () => {
+ expect(wrapper.emitted().showActionConfirmationModal).toHaveLength(1);
+ });
+
+ it('does not emit `pipelineActionRequestComplete` event', () => {
+ expect(wrapper.emitted().pipelineActionRequestComplete).toBeUndefined();
+ });
+ });
+
+ describe('and the `shouldTriggerClick` value becomes true', () => {
+ beforeEach(async () => {
+ await wrapper.setProps({ shouldTriggerClick: true });
+ });
+
+ it('does not emit `showActionConfirmationModal` event', () => {
+ expect(wrapper.emitted().showActionConfirmationModal).toBeUndefined();
+ });
+
+ it('emits `actionButtonClicked` event', () => {
+ expect(wrapper.emitted().actionButtonClicked).toHaveLength(1);
+ });
+ });
+ });
});
diff --git a/spec/frontend/pipelines/graph/graph_component_spec.js b/spec/frontend/pipelines/graph/graph_component_spec.js
index 2abb5f7dc58..95207fd59ff 100644
--- a/spec/frontend/pipelines/graph/graph_component_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_spec.js
@@ -1,4 +1,5 @@
-import { mount, shallowMount } from '@vue/test-utils';
+import { shallowMount } from '@vue/test-utils';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { LAYER_VIEW, STAGE_VIEW } from '~/pipelines/components/graph/constants';
import PipelineGraph from '~/pipelines/components/graph/graph_component.vue';
import JobItem from '~/pipelines/components/graph/job_item.vue';
@@ -15,10 +16,11 @@ import {
describe('graph component', () => {
let wrapper;
+ const findDownstreamColumn = () => wrapper.findByTestId('downstream-pipelines');
const findLinkedColumns = () => wrapper.findAllComponents(LinkedPipelinesColumn);
const findLinksLayer = () => wrapper.findComponent(LinksLayer);
const findStageColumns = () => wrapper.findAllComponents(StageColumnComponent);
- const findStageNameInJob = () => wrapper.find('[data-testid="stage-name-in-job"]');
+ const findStageNameInJob = () => wrapper.findByTestId('stage-name-in-job');
const defaultProps = {
pipeline: generateResponse(mockPipelineResponse, 'root/fungi-xoxo'),
@@ -64,14 +66,9 @@ describe('graph component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
describe('with data', () => {
beforeEach(() => {
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
});
it('renders the main columns in the graph', () => {
@@ -96,10 +93,20 @@ describe('graph component', () => {
});
});
+ describe('when column request an update to the retry confirmation modal', () => {
+ beforeEach(() => {
+ findStageColumns().at(0).vm.$emit('setSkipRetryModal');
+ });
+
+ it('setSkipRetryModal is emitted', () => {
+ expect(wrapper.emitted().setSkipRetryModal).toHaveLength(1);
+ });
+ });
+
describe('when links are present', () => {
beforeEach(() => {
createComponent({
- mountFn: mount,
+ mountFn: mountExtended,
stubOverride: { 'job-item': false },
data: { hoveredJobName: 'test_a' },
});
@@ -116,7 +123,7 @@ describe('graph component', () => {
describe('when linked pipelines are not present', () => {
beforeEach(() => {
- createComponent({ mountFn: mount });
+ createComponent({ mountFn: mountExtended });
});
it('should not render a linked pipelines column', () => {
@@ -127,7 +134,7 @@ describe('graph component', () => {
describe('when linked pipelines are present', () => {
beforeEach(() => {
createComponent({
- mountFn: mount,
+ mountFn: mountExtended,
props: { pipeline: pipelineWithUpstreamDownstream(mockPipelineResponse) },
});
});
@@ -140,7 +147,7 @@ describe('graph component', () => {
describe('in layers mode', () => {
beforeEach(() => {
createComponent({
- mountFn: mount,
+ mountFn: mountExtended,
stubOverride: {
'job-item': false,
'job-group-dropdown': false,
@@ -156,4 +163,22 @@ describe('graph component', () => {
expect(findStageNameInJob().exists()).toBe(true);
});
});
+
+ describe('downstream pipelines', () => {
+ beforeEach(() => {
+ createComponent({
+ mountFn: mountExtended,
+ props: {
+ pipeline: pipelineWithUpstreamDownstream(mockPipelineResponse),
+ },
+ });
+ });
+
+ it('filters pipelines spawned from the same trigger job', () => {
+ // The mock data has one downstream with `retried: true and one
+ // with retried false. We filter the `retried: true` out so we
+ // should only pass one downstream
+ expect(findDownstreamColumn().props().linkedPipelines).toHaveLength(1);
+ });
+ });
});
diff --git a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
index 587a3c67168..99bccd21656 100644
--- a/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
+++ b/spec/frontend/pipelines/graph/graph_component_wrapper_spec.js
@@ -10,6 +10,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import getPipelineDetails from 'shared_queries/pipelines/get_pipeline_details.query.graphql';
import getUserCallouts from '~/graphql_shared/queries/get_user_callouts.query.graphql';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
PIPELINES_DETAIL_LINK_DURATION,
PIPELINES_DETAIL_LINKS_TOTAL,
@@ -199,6 +200,22 @@ describe('Pipeline graph wrapper', () => {
});
});
+ describe('events', () => {
+ beforeEach(async () => {
+ createComponentWithApollo();
+ await waitForPromises();
+ });
+ describe('when receiving `setSkipRetryModal` event', () => {
+ it('passes down `skipRetryModal` value as true', async () => {
+ expect(getGraph().props('skipRetryModal')).toBe(false);
+
+ await getGraph().vm.$emit('setSkipRetryModal');
+
+ expect(getGraph().props('skipRetryModal')).toBe(true);
+ });
+ });
+ });
+
describe('when there is an error with an action in the graph', () => {
beforeEach(async () => {
createComponentWithApollo();
@@ -530,7 +547,7 @@ describe('Pipeline graph wrapper', () => {
describe('with duration and no error', () => {
beforeEach(async () => {
mock = new MockAdapter(axios);
- mock.onPost(metricsPath).reply(200, {});
+ mock.onPost(metricsPath).reply(HTTP_STATUS_OK, {});
jest.spyOn(window.performance, 'getEntriesByName').mockImplementation(() => {
return [{ duration }];
diff --git a/spec/frontend/pipelines/graph/job_item_spec.js b/spec/frontend/pipelines/graph/job_item_spec.js
index 05776ec0706..3224c87ab6b 100644
--- a/spec/frontend/pipelines/graph/job_item_spec.js
+++ b/spec/frontend/pipelines/graph/job_item_spec.js
@@ -1,44 +1,78 @@
+import MockAdapter from 'axios-mock-adapter';
import { mount } from '@vue/test-utils';
import { nextTick } from 'vue';
-import { GlBadge } from '@gitlab/ui';
+import { GlBadge, GlModal } from '@gitlab/ui';
import JobItem from '~/pipelines/components/graph/job_item.vue';
+import axios from '~/lib/utils/axios_utils';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import {
delayedJob,
mockJob,
mockJobWithoutDetails,
mockJobWithUnauthorizedAction,
+ mockFailedJob,
triggerJob,
+ triggerJobWithRetryAction,
} from './mock_data';
describe('pipeline graph job item', () => {
+ useLocalStorageSpy();
+
let wrapper;
+ let mockAxios;
const findJobWithoutLink = () => wrapper.findByTestId('job-without-link');
const findJobWithLink = () => wrapper.findByTestId('job-with-link');
const findActionComponent = () => wrapper.findByTestId('ci-action-component');
const findBadge = () => wrapper.findComponent(GlBadge);
+ const findJobLink = () => wrapper.findByTestId('job-with-link');
+ const findModal = () => wrapper.findComponent(GlModal);
+
+ const clickOnModalPrimaryBtn = () => findModal().vm.$emit('primary');
+ const clickOnModalCancelBtn = () => findModal().vm.$emit('hide');
+ const clickOnModalCloseBtn = () => findModal().vm.$emit('close');
+
+ const myCustomClass1 = 'my-class-1';
+ const myCustomClass2 = 'my-class-2';
- const createWrapper = (propsData) => {
+ const defaultProps = {
+ job: mockJob,
+ };
+
+ const createWrapper = ({ props, data } = {}) => {
wrapper = extendedWrapper(
mount(JobItem, {
- propsData,
+ data() {
+ return {
+ ...data,
+ };
+ },
+ propsData: {
+ ...defaultProps,
+ ...props,
+ },
}),
);
};
const triggerActiveClass = 'gl-shadow-x0-y0-b3-s1-blue-500';
+ beforeEach(() => {
+ mockAxios = new MockAdapter(axios);
+ });
+
afterEach(() => {
- wrapper.destroy();
+ mockAxios.restore();
});
describe('name with link', () => {
it('should render the job name and status with a link', async () => {
- createWrapper({ job: mockJob });
+ createWrapper();
await nextTick();
- const link = wrapper.find('a');
+ const link = findJobLink();
expect(link.attributes('href')).toBe(mockJob.status.detailsPath);
@@ -53,15 +87,17 @@ describe('pipeline graph job item', () => {
describe('name without link', () => {
beforeEach(() => {
createWrapper({
- job: mockJobWithoutDetails,
- cssClassJobName: 'css-class-job-name',
- jobHovered: 'test',
+ props: {
+ job: mockJobWithoutDetails,
+ cssClassJobName: 'css-class-job-name',
+ jobHovered: 'test',
+ },
});
});
it('should render status and name', () => {
expect(wrapper.find('.ci-status-icon-success').exists()).toBe(true);
- expect(wrapper.find('a').exists()).toBe(false);
+ expect(findJobLink().exists()).toBe(false);
expect(wrapper.text()).toBe(mockJobWithoutDetails.name);
});
@@ -73,7 +109,7 @@ describe('pipeline graph job item', () => {
describe('action icon', () => {
it('should render the action icon', () => {
- createWrapper({ job: mockJob });
+ createWrapper();
const actionComponent = findActionComponent();
@@ -83,7 +119,11 @@ describe('pipeline graph job item', () => {
});
it('should render disabled action icon when user cannot run the action', () => {
- createWrapper({ job: mockJobWithUnauthorizedAction });
+ createWrapper({
+ props: {
+ job: mockJobWithUnauthorizedAction,
+ },
+ });
const actionComponent = findActionComponent();
@@ -91,18 +131,32 @@ describe('pipeline graph job item', () => {
expect(actionComponent.props('actionIcon')).toBe('stop');
expect(actionComponent.attributes('disabled')).toBe('disabled');
});
+
+ it('action icon tooltip text when job has passed but can be ran again', () => {
+ createWrapper({ props: { job: mockJob } });
+
+ expect(findActionComponent().props('tooltipText')).toBe('Run again');
+ });
+
+ it('action icon tooltip text when job has failed and can be retried', () => {
+ createWrapper({ props: { job: mockFailedJob } });
+
+ expect(findActionComponent().props('tooltipText')).toBe('Retry');
+ });
});
describe('job style', () => {
beforeEach(() => {
createWrapper({
- job: mockJob,
- cssClassJobName: 'css-class-job-name',
+ props: {
+ job: mockJob,
+ cssClassJobName: 'css-class-job-name',
+ },
});
});
it('should render provided class name', () => {
- expect(wrapper.find('a').classes()).toContain('css-class-job-name');
+ expect(findJobLink().classes()).toContain('css-class-job-name');
});
it('does not show a badge on the job item', () => {
@@ -117,11 +171,13 @@ describe('pipeline graph job item', () => {
describe('status label', () => {
it('should not render status label when it is not provided', () => {
createWrapper({
- job: {
- id: 4258,
- name: 'test',
- status: {
- icon: 'status_success',
+ props: {
+ job: {
+ id: 4258,
+ name: 'test',
+ status: {
+ icon: 'status_success',
+ },
},
},
});
@@ -131,13 +187,15 @@ describe('pipeline graph job item', () => {
it('should not render status label when it is provided', () => {
createWrapper({
- job: {
- id: 4259,
- name: 'test',
- status: {
- icon: 'status_success',
- label: 'success',
- tooltip: 'success',
+ props: {
+ job: {
+ id: 4259,
+ name: 'test',
+ status: {
+ icon: 'status_success',
+ label: 'success',
+ tooltip: 'success',
+ },
},
},
});
@@ -149,7 +207,9 @@ describe('pipeline graph job item', () => {
describe('for delayed job', () => {
it('displays remaining time in tooltip', () => {
createWrapper({
- job: delayedJob,
+ props: {
+ job: delayedJob,
+ },
});
expect(findJobWithLink().attributes('title')).toBe(
@@ -161,7 +221,11 @@ describe('pipeline graph job item', () => {
describe('trigger job', () => {
describe('card', () => {
beforeEach(() => {
- createWrapper({ job: triggerJob });
+ createWrapper({
+ props: {
+ job: triggerJob,
+ },
+ });
});
it('shows a badge on the job item', () => {
@@ -182,7 +246,12 @@ describe('pipeline graph job item', () => {
`(
`trigger job should stay highlighted when downstream is expanded`,
({ job, jobName, expanded, link }) => {
- createWrapper({ job, pipelineExpanded: { jobName, expanded } });
+ createWrapper({
+ props: {
+ job,
+ pipelineExpanded: { jobName, expanded },
+ },
+ });
const findJobEl = link ? findJobWithLink : findJobWithoutLink;
expect(findJobEl().classes()).toContain(triggerActiveClass);
@@ -196,7 +265,12 @@ describe('pipeline graph job item', () => {
`(
`trigger job should not be highlighted when downstream is not expanded`,
({ job, jobName, expanded, link }) => {
- createWrapper({ job, pipelineExpanded: { jobName, expanded } });
+ createWrapper({
+ props: {
+ job,
+ pipelineExpanded: { jobName, expanded },
+ },
+ });
const findJobEl = link ? findJobWithLink : findJobWithoutLink;
expect(findJobEl().classes()).not.toContain(triggerActiveClass);
@@ -208,60 +282,182 @@ describe('pipeline graph job item', () => {
describe('job classes', () => {
it('job class is shown', () => {
createWrapper({
- job: mockJob,
- cssClassJobName: 'my-class',
+ props: {
+ job: mockJob,
+ cssClassJobName: 'my-class',
+ },
});
- expect(wrapper.find('a').classes()).toContain('my-class');
+ const jobLinkEl = findJobLink();
- expect(wrapper.find('a').classes()).not.toContain(triggerActiveClass);
+ expect(jobLinkEl.classes()).toContain('my-class');
+
+ expect(jobLinkEl.classes()).not.toContain(triggerActiveClass);
});
it('job class is shown, along with hover', () => {
createWrapper({
- job: mockJob,
- cssClassJobName: 'my-class',
- sourceJobHovered: mockJob.name,
+ props: {
+ job: mockJob,
+ cssClassJobName: 'my-class',
+ sourceJobHovered: mockJob.name,
+ },
});
- expect(wrapper.find('a').classes()).toContain('my-class');
- expect(wrapper.find('a').classes()).toContain(triggerActiveClass);
+ const jobLinkEl = findJobLink();
+
+ expect(jobLinkEl.classes()).toContain('my-class');
+ expect(jobLinkEl.classes()).toContain(triggerActiveClass);
});
it('multiple job classes are shown', () => {
createWrapper({
- job: mockJob,
- cssClassJobName: ['my-class-1', 'my-class-2'],
+ props: {
+ job: mockJob,
+ cssClassJobName: [myCustomClass1, myCustomClass2],
+ },
});
- expect(wrapper.find('a').classes()).toContain('my-class-1');
- expect(wrapper.find('a').classes()).toContain('my-class-2');
+ const jobLinkEl = findJobLink();
+
+ expect(jobLinkEl.classes()).toContain(myCustomClass1);
+ expect(jobLinkEl.classes()).toContain(myCustomClass2);
- expect(wrapper.find('a').classes()).not.toContain(triggerActiveClass);
+ expect(jobLinkEl.classes()).not.toContain(triggerActiveClass);
});
it('multiple job classes are shown conditionally', () => {
createWrapper({
- job: mockJob,
- cssClassJobName: { 'my-class-1': true, 'my-class-2': true },
+ props: {
+ job: mockJob,
+ cssClassJobName: { [myCustomClass1]: true, [myCustomClass2]: true },
+ },
});
- expect(wrapper.find('a').classes()).toContain('my-class-1');
- expect(wrapper.find('a').classes()).toContain('my-class-2');
+ const jobLinkEl = findJobLink();
+
+ expect(jobLinkEl.classes()).toContain(myCustomClass1);
+ expect(jobLinkEl.classes()).toContain(myCustomClass2);
- expect(wrapper.find('a').classes()).not.toContain(triggerActiveClass);
+ expect(jobLinkEl.classes()).not.toContain(triggerActiveClass);
});
it('multiple job classes are shown, along with a hover', () => {
createWrapper({
- job: mockJob,
- cssClassJobName: ['my-class-1', 'my-class-2'],
- sourceJobHovered: mockJob.name,
+ props: {
+ job: mockJob,
+ cssClassJobName: [myCustomClass1, myCustomClass2],
+ sourceJobHovered: mockJob.name,
+ },
});
- expect(wrapper.find('a').classes()).toContain('my-class-1');
- expect(wrapper.find('a').classes()).toContain('my-class-2');
- expect(wrapper.find('a').classes()).toContain(triggerActiveClass);
+ const jobLinkEl = findJobLink();
+
+ expect(jobLinkEl.classes()).toContain(myCustomClass1);
+ expect(jobLinkEl.classes()).toContain(myCustomClass2);
+ expect(jobLinkEl.classes()).toContain(triggerActiveClass);
+ });
+ });
+
+ describe('confirmation modal', () => {
+ describe('when clicking on the action component', () => {
+ it.each`
+ skipRetryModal | exists | visibilityText
+ ${false} | ${true} | ${'shows'}
+ ${true} | ${false} | ${'hides'}
+ `(
+ '$visibilityText the modal when `skipRetryModal` is $skipRetryModal',
+ async ({ exists, skipRetryModal }) => {
+ createWrapper({
+ props: {
+ skipRetryModal,
+ job: triggerJobWithRetryAction,
+ },
+ });
+ await findActionComponent().trigger('click');
+
+ expect(findModal().exists()).toBe(exists);
+ },
+ );
+ });
+
+ describe('when showing the modal', () => {
+ it.each`
+ buttonName | shouldTriggerActionClick | actionBtn
+ ${'primary'} | ${true} | ${clickOnModalPrimaryBtn}
+ ${'cancel'} | ${false} | ${clickOnModalCancelBtn}
+ ${'close'} | ${false} | ${clickOnModalCloseBtn}
+ `(
+ 'clicking on $buttonName will pass down shouldTriggerActionClick as $shouldTriggerActionClick to the action component',
+ async ({ shouldTriggerActionClick, actionBtn }) => {
+ createWrapper({
+ props: {
+ skipRetryModal: false,
+ job: triggerJobWithRetryAction,
+ },
+ });
+ await findActionComponent().trigger('click');
+
+ await actionBtn();
+
+ expect(findActionComponent().props().shouldTriggerClick).toBe(shouldTriggerActionClick);
+ },
+ );
+ });
+
+ describe('when not checking the "do not show this again" checkbox', () => {
+ it.each`
+ actionName | actionBtn
+ ${'closing'} | ${clickOnModalCloseBtn}
+ ${'cancelling'} | ${clickOnModalCancelBtn}
+ ${'confirming'} | ${clickOnModalPrimaryBtn}
+ `(
+ 'does not emit any event and will not modify localstorage on $actionName',
+ async ({ actionBtn }) => {
+ createWrapper({
+ props: {
+ skipRetryModal: false,
+ job: triggerJobWithRetryAction,
+ },
+ });
+ await findActionComponent().trigger('click');
+ await actionBtn();
+
+ expect(wrapper.emitted().setSkipRetryModal).toBeUndefined();
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ },
+ );
+ });
+
+ describe('when checking the "do not show this again" checkbox', () => {
+ it.each`
+ actionName | actionBtn
+ ${'closing'} | ${clickOnModalCloseBtn}
+ ${'cancelling'} | ${clickOnModalCancelBtn}
+ ${'confirming'} | ${clickOnModalPrimaryBtn}
+ `(
+ 'emits "setSkipRetryModal" and set local storage key on $actionName the modal',
+ async ({ actionBtn }) => {
+ // We are passing the checkbox as a slot to the GlModal.
+ // The way GlModal is mounted, we can neither click on the box
+ // or emit an event directly. We therefore set the data property
+ // as it would be if the box was checked.
+ createWrapper({
+ data: {
+ currentSkipModalValue: true,
+ },
+ props: {
+ skipRetryModal: false,
+ job: triggerJobWithRetryAction,
+ },
+ });
+ await findActionComponent().trigger('click');
+ await actionBtn();
+
+ expect(wrapper.emitted().setSkipRetryModal).toHaveLength(1);
+ expect(localStorage.setItem).toHaveBeenCalledWith('skip_retry_modal', 'true');
+ },
+ );
});
});
});
diff --git a/spec/frontend/pipelines/graph/linked_pipeline_spec.js b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
index 399d52c3dff..f396fe2aff4 100644
--- a/spec/frontend/pipelines/graph/linked_pipeline_spec.js
+++ b/spec/frontend/pipelines/graph/linked_pipeline_spec.js
@@ -5,10 +5,10 @@ import { mount } from '@vue/test-utils';
import createMockApollo from 'helpers/mock_apollo_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import { convertToGraphQLId } from '~/graphql_shared/utils';
+import { TYPENAME_CI_PIPELINE } from '~/graphql_shared/constants';
import { BV_HIDE_TOOLTIP } from '~/lib/utils/constants';
import { ACTION_FAILURE, UPSTREAM, DOWNSTREAM } from '~/pipelines/components/graph/constants';
import LinkedPipelineComponent from '~/pipelines/components/graph/linked_pipeline.vue';
-import { PIPELINE_GRAPHQL_TYPE } from '~/pipelines/constants';
import CancelPipelineMutation from '~/pipelines/graphql/mutations/cancel_pipeline.mutation.graphql';
import RetryPipelineMutation from '~/pipelines/graphql/mutations/retry_pipeline.mutation.graphql';
import CiStatus from '~/vue_shared/components/ci_icon.vue';
@@ -219,7 +219,7 @@ describe('Linked pipeline', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: RetryPipelineMutation,
variables: {
- id: convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, mockPipeline.id),
+ id: convertToGraphQLId(TYPENAME_CI_PIPELINE, mockPipeline.id),
},
});
});
@@ -285,7 +285,7 @@ describe('Linked pipeline', () => {
expect(wrapper.vm.$apollo.mutate).toHaveBeenCalledWith({
mutation: CancelPipelineMutation,
variables: {
- id: convertToGraphQLId(PIPELINE_GRAPHQL_TYPE, mockPipeline.id),
+ id: convertToGraphQLId(TYPENAME_CI_PIPELINE, mockPipeline.id),
},
});
});
diff --git a/spec/frontend/pipelines/graph/mock_data.js b/spec/frontend/pipelines/graph/mock_data.js
index 6124d67af09..08624cc511d 100644
--- a/spec/frontend/pipelines/graph/mock_data.js
+++ b/spec/frontend/pipelines/graph/mock_data.js
@@ -1,5 +1,9 @@
import { unwrapPipelineData } from '~/pipelines/components/graph/utils';
-import { BUILD_KIND, BRIDGE_KIND } from '~/pipelines/components/graph/constants';
+import {
+ BUILD_KIND,
+ BRIDGE_KIND,
+ RETRY_ACTION_TITLE,
+} from '~/pipelines/components/graph/constants';
export const mockPipelineResponse = {
data: {
@@ -726,6 +730,7 @@ export const downstream = {
sourceJob: {
name: 'test_c',
id: '71',
+ retried: false,
__typename: 'CiJob',
},
project: {
@@ -756,6 +761,7 @@ export const downstream = {
sourceJob: {
id: '73',
name: 'test_d',
+ retried: true,
__typename: 'CiJob',
},
project: {
@@ -841,6 +847,7 @@ export const wrappedPipelineReturn = {
sourceJob: {
name: 'test_c',
id: '78',
+ retried: false,
__typename: 'CiJob',
},
project: {
@@ -1038,3 +1045,38 @@ export const triggerJob = {
action: null,
},
};
+
+export const triggerJobWithRetryAction = {
+ ...triggerJob,
+ status: {
+ ...triggerJob.status,
+ action: {
+ icon: 'retry',
+ title: RETRY_ACTION_TITLE,
+ path: '/root/ci-mock/builds/4259/retry',
+ method: 'post',
+ },
+ },
+};
+
+export const mockFailedJob = {
+ id: 3999,
+ name: 'failed job',
+ kind: BUILD_KIND,
+ status: {
+ id: 'failed-3999-3999',
+ icon: 'status_failed',
+ tooltip: 'failed - (stuck or timeout failure)',
+ hasDetails: true,
+ detailsPath: '/root/ci-project/-/jobs/3999',
+ group: 'failed',
+ label: 'failed',
+ action: {
+ id: 'Ci::BuildPresenter-failed-3999',
+ buttonTitle: 'Retry this job',
+ icon: 'retry',
+ path: '/root/ci-project/-/jobs/3999/retry',
+ title: 'Retry',
+ },
+ },
+};
diff --git a/spec/frontend/pipelines/linked_pipelines_mock.json b/spec/frontend/pipelines/linked_pipelines_mock.json
index 8ad19ef4865..a68283032d2 100644
--- a/spec/frontend/pipelines/linked_pipelines_mock.json
+++ b/spec/frontend/pipelines/linked_pipelines_mock.json
@@ -7,7 +7,7 @@
"state": "active",
"avatar_url": "https://assets.gitlab-static.net/uploads/-/system/user/avatar/3585/avatar.png",
"web_url": "https://gitlab.com/axil",
- "status_tooltip_html": "\u003cspan class=\"user-status-emoji has-tooltip\" title=\"I like pizza\" data-html=\"true\" data-placement=\"top\"\u003e\u003cgl-emoji title=\"slice of pizza\" data-name=\"pizza\" data-unicode-version=\"6.0\"\u003e🍕\u003c/gl-emoji\u003e\u003c/span\u003e",
+ "status_tooltip_html": "<span class=\"user-status-emoji has-tooltip\" title=\"I like pizza\" data-html=\"true\" data-placement=\"top\"><gl-emoji title=\"slice of pizza\" data-name=\"pizza\" data-unicode-version=\"6.0\">🍕</gl-emoji></span>",
"path": "/axil"
},
"active": false,
@@ -68,7 +68,7 @@
"title": "Play",
"path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -104,7 +104,7 @@
"title": "Play",
"path": "/gitlab-org/gitlab-runner/-/jobs/72469032/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -290,7 +290,9 @@
"dropdown_path": "/gitlab-org/gitlab-runner/pipelines/23211253/stage.json?stage=cleanup"
}
],
- "artifacts": [],
+ "artifacts": [
+
+ ],
"manual_actions": [
{
"name": "review-docs-cleanup",
@@ -305,7 +307,9 @@
"scheduled": false
}
],
- "scheduled_actions": []
+ "scheduled_actions": [
+
+ ]
},
"ref": {
"name": "docs/add-development-guide-to-readme",
@@ -319,7 +323,9 @@
"short_id": "8083eb0a",
"title": "Add link to development guide in readme",
"created_at": "2018-06-05T11:30:48.000Z",
- "parent_ids": ["1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"],
+ "parent_ids": [
+ "1d7cf79b5a1a2121b9474ac20d61c1b8f621289d"
+ ],
"message": "Add link to development guide in readme\n\nCloses https://gitlab.com/gitlab-org/gitlab-runner/issues/3122\n",
"author_name": "Achilleas Pipinellis",
"author_email": "axil@gitlab.com",
@@ -337,7 +343,7 @@
"status_tooltip_html": null,
"path": "/axil"
},
- "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80\u0026d=identicon",
+ "author_gravatar_url": "https://secure.gravatar.com/avatar/1d37af00eec153a8333a4ce18e9aea41?s=80&d=identicon",
"commit_url": "https://gitlab.com/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46",
"commit_path": "/gitlab-org/gitlab-runner/commit/8083eb0a920572214d0dccedd7981f05d535ad46"
},
@@ -402,7 +408,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -437,7 +443,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -466,7 +472,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -501,7 +507,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -530,7 +536,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -565,7 +571,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -782,7 +788,9 @@
"dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
}
],
- "artifacts": [],
+ "artifacts": [
+
+ ],
"manual_actions": [
{
"name": "image:bootstrap",
@@ -809,7 +817,9 @@
"scheduled": false
}
],
- "scheduled_actions": []
+ "scheduled_actions": [
+
+ ]
},
"project": {
"id": 1794617,
@@ -875,7 +885,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -910,7 +920,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -939,7 +949,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -974,7 +984,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1003,7 +1013,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1038,7 +1048,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1255,7 +1265,9 @@
"dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
}
],
- "artifacts": [],
+ "artifacts": [
+
+ ],
"manual_actions": [
{
"name": "image:bootstrap",
@@ -1282,7 +1294,9 @@
"scheduled": false
}
],
- "scheduled_actions": []
+ "scheduled_actions": [
+
+ ]
},
"project": {
"id": 1794617,
@@ -1291,7 +1305,9 @@
"full_name": "GitLab.com / GitLab Docs"
}
},
- "triggered": []
+ "triggered": [
+
+ ]
},
"triggered": [
{
@@ -1352,7 +1368,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1387,7 +1403,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1416,7 +1432,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1451,7 +1467,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1480,7 +1496,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1515,7 +1531,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1732,7 +1748,9 @@
"dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
}
],
- "artifacts": [],
+ "artifacts": [
+
+ ],
"manual_actions": [
{
"name": "image:bootstrap",
@@ -1759,7 +1777,9 @@
"scheduled": false
}
],
- "scheduled_actions": []
+ "scheduled_actions": [
+
+ ]
},
"project": {
"id": 1794617,
@@ -1767,7 +1787,10 @@
"full_path": "/gitlab-com/gitlab-docs",
"full_name": "GitLab.com / GitLab Docs"
},
- "triggered": [{}]
+ "triggered": [
+ {
+ }
+ ]
},
{
"id": 34993052,
@@ -1827,7 +1850,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1862,7 +1885,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982853/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1891,7 +1914,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1926,7 +1949,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982854/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -1955,7 +1978,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -1990,7 +2013,7 @@
"title": "Play",
"path": "/gitlab-com/gitlab-docs/-/jobs/114982855/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -2207,7 +2230,9 @@
"dropdown_path": "/gitlab-com/gitlab-docs/pipelines/34993051/stage.json?stage=deploy"
}
],
- "artifacts": [],
+ "artifacts": [
+
+ ],
"manual_actions": [
{
"name": "image:bootstrap",
@@ -2234,7 +2259,9 @@
"scheduled": false
}
],
- "scheduled_actions": []
+ "scheduled_actions": [
+
+ ]
},
"project": {
"id": 1794617,
@@ -3373,7 +3400,7 @@
"title": "Play",
"path": "/h5bp/html5-boilerplate/-/jobs/545/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
},
"jobs": [
@@ -3409,7 +3436,7 @@
"title": "Play",
"path": "/h5bp/html5-boilerplate/-/jobs/545/play",
"method": "post",
- "button_title": "Trigger this manual action"
+ "button_title": "Run job"
}
}
}
@@ -3467,7 +3494,9 @@
"scheduled": false
}
],
- "scheduled_actions": []
+ "scheduled_actions": [
+
+ ]
},
"ref": {
"name": "master",
@@ -3481,7 +3510,9 @@
"short_id": "bad98c45",
"title": "remove instances of shrink-to-fit=no (#2103)",
"created_at": "2018-12-17T20:52:18.000Z",
- "parent_ids": ["49130f6cfe9ff1f749015d735649a2bc6f66cf3a"],
+ "parent_ids": [
+ "49130f6cfe9ff1f749015d735649a2bc6f66cf3a"
+ ],
"message": "remove instances of shrink-to-fit=no (#2103)\n\ncloses #2102\r\n\r\nPer my findings, the need for it as a default was rectified with the release of iOS 9.3, where the viewport no longer shrunk to accommodate overflow, as was introduced in iOS 9.",
"author_name": "Scott O'Hara",
"author_email": "scottaohara@users.noreply.github.com",
@@ -3490,7 +3521,7 @@
"committer_email": "rob@drunkenfist.com",
"committed_date": "2018-12-17T20:52:18.000Z",
"author": null,
- "author_gravatar_url": "https://www.gravatar.com/avatar/6d597df7cf998d16cbe00ccac063b31e?s=80\u0026d=identicon",
+ "author_gravatar_url": "https://www.gravatar.com/avatar/6d597df7cf998d16cbe00ccac063b31e?s=80&d=identicon",
"commit_url": "http://localhost:3001/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b",
"commit_path": "/h5bp/html5-boilerplate/commit/bad98c453eab56d20057f3929989251d45cd1a8b"
},
@@ -3522,7 +3553,9 @@
"full_name": "Gitlab Org / Gitlab Test"
}
},
- "triggered": [],
+ "triggered": [
+
+ ],
"project": {
"id": 1794617,
"name": "GitLab Docs",
diff --git a/spec/frontend/pipelines/mock_data.js b/spec/frontend/pipelines/mock_data.js
index 36bce65dd56..dd7e81f3f22 100644
--- a/spec/frontend/pipelines/mock_data.js
+++ b/spec/frontend/pipelines/mock_data.js
@@ -837,7 +837,6 @@ export const mockPipelineTag = () => {
duration: 93,
finished_at: '2022-02-02T15:40:59.384Z',
event_type_name: 'Pipeline',
- name: 'Pipeline',
manual_actions: [],
scheduled_actions: [],
},
@@ -1045,7 +1044,6 @@ export const mockPipelineBranch = () => {
duration: 75,
finished_at: '2022-01-14T18:02:35.842Z',
event_type_name: 'Pipeline',
- name: 'Pipeline',
manual_actions: [],
scheduled_actions: [],
},
diff --git a/spec/frontend/pipelines/pipeline_multi_actions_spec.js b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
index f0dae8ebcbe..bedde71c48d 100644
--- a/spec/frontend/pipelines/pipeline_multi_actions_spec.js
+++ b/spec/frontend/pipelines/pipeline_multi_actions_spec.js
@@ -5,6 +5,7 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PipelineMultiActions, {
i18n,
} from '~/pipelines/components/pipelines_list/pipeline_multi_actions.vue';
@@ -79,7 +80,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
describe('Artifacts', () => {
it('should fetch artifacts and show search box on dropdown click', async () => {
const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
- mockAxios.onGet(endpoint).replyOnce(200, { artifacts });
+ mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_OK, { artifacts });
createComponent();
findDropdown().vm.$emit('show');
await waitForPromises();
@@ -140,7 +141,7 @@ describe('Pipeline Multi Actions Dropdown', () => {
describe('with a failing request', () => {
it('should render an error message', async () => {
const endpoint = artifactsEndpoint.replace(artifactsEndpointPlaceholder, pipelineId);
- mockAxios.onGet(endpoint).replyOnce(500);
+ mockAxios.onGet(endpoint).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
findDropdown().vm.$emit('show');
await waitForPromises();
diff --git a/spec/frontend/pipelines/pipelines_actions_spec.js b/spec/frontend/pipelines/pipelines_actions_spec.js
index a70ef10aa7b..e034d52a33c 100644
--- a/spec/frontend/pipelines/pipelines_actions_spec.js
+++ b/spec/frontend/pipelines/pipelines_actions_spec.js
@@ -7,6 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { TEST_HOST } from 'spec/test_constants';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { confirmAction } from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
import PipelinesManualActions from '~/pipelines/components/pipelines_list/pipelines_manual_actions.vue';
import GlCountdown from '~/vue_shared/components/gl_countdown.vue';
@@ -70,7 +71,7 @@ describe('Pipelines Actions dropdown', () => {
describe('on click', () => {
it('makes a request and toggles the loading state', async () => {
- mock.onPost(mockActions.path).reply(200);
+ mock.onPost(mockActions.path).reply(HTTP_STATUS_OK);
findAllDropdownItems().at(0).vm.$emit('click');
@@ -82,7 +83,7 @@ describe('Pipelines Actions dropdown', () => {
});
it('makes a failed request and toggles the loading state', async () => {
- mock.onPost(mockActions.path).reply(500);
+ mock.onPost(mockActions.path).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
findAllDropdownItems().at(0).vm.$emit('click');
@@ -132,7 +133,7 @@ describe('Pipelines Actions dropdown', () => {
});
it('makes post request after confirming', async () => {
- mock.onPost(scheduledJobAction.path).reply(200);
+ mock.onPost(scheduledJobAction.path).reply(HTTP_STATUS_OK);
confirmAction.mockResolvedValueOnce(true);
findAllDropdownItems().at(0).vm.$emit('click');
@@ -145,7 +146,7 @@ describe('Pipelines Actions dropdown', () => {
});
it('does not make post request if confirmation is cancelled', async () => {
- mock.onPost(scheduledJobAction.path).reply(200);
+ mock.onPost(scheduledJobAction.path).reply(HTTP_STATUS_OK);
confirmAction.mockResolvedValueOnce(false);
findAllDropdownItems().at(0).vm.$emit('click');
diff --git a/spec/frontend/pipelines/pipelines_spec.js b/spec/frontend/pipelines/pipelines_spec.js
index 351572fc83a..2523b901506 100644
--- a/spec/frontend/pipelines/pipelines_spec.js
+++ b/spec/frontend/pipelines/pipelines_spec.js
@@ -13,6 +13,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import Api from '~/api';
import { createAlert, VARIANT_WARNING } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import NavigationControls from '~/pipelines/components/pipelines_list/nav_controls.vue';
import PipelinesComponent from '~/pipelines/components/pipelines_list/pipelines.vue';
import PipelinesCiTemplates from '~/pipelines/components/pipelines_list/empty_state/pipelines_ci_templates.vue';
@@ -141,7 +142,7 @@ describe('Pipelines', () => {
beforeEach(() => {
mock
.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
- .reply(200, mockPipelinesResponse);
+ .reply(HTTP_STATUS_OK, mockPipelinesResponse);
});
describe('when user has no permissions', () => {
@@ -233,7 +234,7 @@ describe('Pipelines', () => {
beforeEach(async () => {
mock
.onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } })
- .reply(200, {
+ .reply(HTTP_STATUS_OK, {
pipelines: [mockFinishedPipeline],
count: mockPipelinesResponse.count,
});
@@ -277,7 +278,7 @@ describe('Pipelines', () => {
beforeEach(async () => {
mock
.onGet(mockPipelinesEndpoint, { params: { scope: 'branches', page: '1' } })
- .reply(200, {
+ .reply(HTTP_STATUS_OK, {
pipelines: [],
count: mockPipelinesResponse.count,
});
@@ -320,7 +321,7 @@ describe('Pipelines', () => {
.onGet(mockPipelinesEndpoint, {
params: expectedParams,
})
- .replyOnce(200, {
+ .replyOnce(HTTP_STATUS_OK, {
pipelines: [mockFilteredPipeline],
count: mockPipelinesResponse.count,
});
@@ -398,7 +399,7 @@ describe('Pipelines', () => {
beforeEach(async () => {
mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply(
- 200,
+ HTTP_STATUS_OK,
{
pipelines: firstPage,
count: mockPipelinesResponse.count,
@@ -406,7 +407,7 @@ describe('Pipelines', () => {
mockPageHeaders({ page: 1 }),
);
mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '2' } }).reply(
- 200,
+ HTTP_STATUS_OK,
{
pipelines: secondPage,
count: mockPipelinesResponse.count,
@@ -448,6 +449,26 @@ describe('Pipelines', () => {
`${window.location.pathname}?page=2&scope=all`,
);
});
+
+ it('should reset page to 1 when filtering pipelines', () => {
+ expect(window.history.pushState).toHaveBeenCalledTimes(1);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?page=2&scope=all`,
+ );
+
+ findFilteredSearch().vm.$emit('submit', [
+ { type: 'status', value: { data: 'success', operator: '=' } },
+ ]);
+
+ expect(window.history.pushState).toHaveBeenCalledTimes(2);
+ expect(window.history.pushState).toHaveBeenCalledWith(
+ expect.anything(),
+ expect.anything(),
+ `${window.location.pathname}?page=1&scope=all&status=success`,
+ );
+ });
});
});
@@ -461,13 +482,13 @@ describe('Pipelines', () => {
// Mock no pipelines in the first attempt
mock
.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
- .replyOnce(200, emptyResponse, {
+ .replyOnce(HTTP_STATUS_OK, emptyResponse, {
'POLL-INTERVAL': 100,
});
// Mock pipelines in the next attempt
mock
.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
- .reply(200, mockPipelinesResponse, {
+ .reply(HTTP_STATUS_OK, mockPipelinesResponse, {
'POLL-INTERVAL': 100,
});
});
@@ -508,10 +529,12 @@ describe('Pipelines', () => {
describe('when no pipelines exist', () => {
beforeEach(() => {
- mock.onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } }).reply(200, {
- pipelines: [],
- count: { all: '0' },
- });
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'all', page: '1' } })
+ .reply(HTTP_STATUS_OK, {
+ pipelines: [],
+ count: { all: '0' },
+ });
});
describe('when CI is enabled and user has permissions', () => {
@@ -550,10 +573,12 @@ describe('Pipelines', () => {
});
it('renders tab empty state finished scope', async () => {
- mock.onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } }).reply(200, {
- pipelines: [],
- count: { all: '0' },
- });
+ mock
+ .onGet(mockPipelinesEndpoint, { params: { scope: 'finished', page: '1' } })
+ .reply(HTTP_STATUS_OK, {
+ pipelines: [],
+ count: { all: '0' },
+ });
findNavigationTabs().vm.$emit('onChangeTab', 'finished');
@@ -643,7 +668,7 @@ describe('Pipelines', () => {
beforeEach(() => {
mock.onGet(mockPipelinesEndpoint, { scope: 'all', page: '1' }).reply(
- 200,
+ HTTP_STATUS_OK,
{
pipelines: [mockPipelineWithStages],
count: { all: '1' },
@@ -653,7 +678,9 @@ describe('Pipelines', () => {
},
);
- mock.onGet(mockPipelineWithStages.details.stages[0].dropdown_path).reply(200, stageReply);
+ mock
+ .onGet(mockPipelineWithStages.details.stages[0].dropdown_path)
+ .reply(HTTP_STATUS_OK, stageReply);
createComponent();
@@ -664,7 +691,7 @@ describe('Pipelines', () => {
describe('when a request is being made', () => {
beforeEach(async () => {
- mock.onGet(mockPipelinesEndpoint).reply(200, mockPipelinesResponse);
+ mock.onGet(mockPipelinesEndpoint).reply(HTTP_STATUS_OK, mockPipelinesResponse);
await waitForPromises();
});
@@ -702,7 +729,7 @@ describe('Pipelines', () => {
describe('when pipelines cannot be loaded', () => {
beforeEach(async () => {
- mock.onGet(mockPipelinesEndpoint).reply(500, {});
+ mock.onGet(mockPipelinesEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
});
describe('when user has no permissions', () => {
diff --git a/spec/frontend/pipelines/pipelines_table_spec.js b/spec/frontend/pipelines/pipelines_table_spec.js
index 9359bd9b95f..6ec8901038b 100644
--- a/spec/frontend/pipelines/pipelines_table_spec.js
+++ b/spec/frontend/pipelines/pipelines_table_spec.js
@@ -121,6 +121,14 @@ describe('Pipelines Table', () => {
expect(findPipelineMiniGraph().props('stages').length).toBe(stagesLength);
});
+ it('should render the latest downstream pipelines only', () => {
+ // component receives two downstream pipelines. one of them is already outdated
+ // because we retried the trigger job, so the mini pipeline graph will only
+ // render the newly created downstream pipeline instead
+ expect(pipeline.triggered).toHaveLength(2);
+ expect(findPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
+ });
+
describe('when pipeline does not have stages', () => {
beforeEach(() => {
pipeline = createMockPipeline();
diff --git a/spec/frontend/pipelines/test_reports/stores/actions_spec.js b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
index 6e61ef97257..f6287107ed0 100644
--- a/spec/frontend/pipelines/test_reports/stores/actions_spec.js
+++ b/spec/frontend/pipelines/test_reports/stores/actions_spec.js
@@ -4,6 +4,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/pipelines/stores/test_reports/actions';
import * as types from '~/pipelines/stores/test_reports/mutation_types';
@@ -35,7 +36,7 @@ describe('Actions TestReports Store', () => {
describe('fetch report summary', () => {
beforeEach(() => {
- mock.onGet(summaryEndpoint).replyOnce(200, summary, {});
+ mock.onGet(summaryEndpoint).replyOnce(HTTP_STATUS_OK, summary, {});
});
it('sets testReports and shows tests', () => {
@@ -66,7 +67,7 @@ describe('Actions TestReports Store', () => {
testReports.test_suites[0].build_ids = buildIds;
mock
.onGet(suiteEndpoint, { params: { build_ids: buildIds } })
- .replyOnce(200, testReports.test_suites[0], {});
+ .replyOnce(HTTP_STATUS_OK, testReports.test_suites[0], {});
});
it('sets test suite and shows tests', () => {
diff --git a/spec/frontend/pipelines/utils_spec.js b/spec/frontend/pipelines/utils_spec.js
index 1c23a7e4fcf..51e0e0705ff 100644
--- a/spec/frontend/pipelines/utils_spec.js
+++ b/spec/frontend/pipelines/utils_spec.js
@@ -3,6 +3,7 @@ import {
makeLinksFromNodes,
filterByAncestors,
generateColumnsFromLayersListBare,
+ keepLatestDownstreamPipelines,
listByLayers,
parseData,
removeOrphanNodes,
@@ -10,6 +11,8 @@ import {
} from '~/pipelines/components/parsing_utils';
import { createNodeDict } from '~/pipelines/utils';
+import { mockDownstreamPipelinesRest } from '../vue_merge_request_widget/mock_data';
+import { mockDownstreamPipelinesGraphql } from '../commit/mock_data';
import { mockParsedGraphQLNodes, missingJob } from './components/dag/mock_data';
import { generateResponse, mockPipelineResponse } from './graph/mock_data';
@@ -159,3 +162,37 @@ describe('DAG visualization parsing utilities', () => {
});
});
});
+
+describe('linked pipeline utilities', () => {
+ describe('keepLatestDownstreamPipelines', () => {
+ it('filters data from GraphQL', () => {
+ const downstream = mockDownstreamPipelinesGraphql().nodes;
+ const latestDownstream = keepLatestDownstreamPipelines(downstream);
+
+ expect(downstream).toHaveLength(3);
+ expect(latestDownstream).toHaveLength(1);
+ });
+
+ it('filters data from REST', () => {
+ const downstream = mockDownstreamPipelinesRest();
+ const latestDownstream = keepLatestDownstreamPipelines(downstream);
+
+ expect(downstream).toHaveLength(2);
+ expect(latestDownstream).toHaveLength(1);
+ });
+
+ it('returns downstream pipelines if sourceJob.retried is null', () => {
+ const downstream = mockDownstreamPipelinesGraphql({ includeSourceJobRetried: false }).nodes;
+ const latestDownstream = keepLatestDownstreamPipelines(downstream);
+
+ expect(latestDownstream).toHaveLength(downstream.length);
+ });
+
+ it('returns downstream pipelines if source_job.retried is null', () => {
+ const downstream = mockDownstreamPipelinesRest({ includeSourceJobRetried: false });
+ const latestDownstream = keepLatestDownstreamPipelines(downstream);
+
+ expect(latestDownstream).toHaveLength(downstream.length);
+ });
+ });
+});
diff --git a/spec/frontend/profile/account/components/update_username_spec.js b/spec/frontend/profile/account/components/update_username_spec.js
index 575df9fb3c0..fa0e86a7b05 100644
--- a/spec/frontend/profile/account/components/update_username_spec.js
+++ b/spec/frontend/profile/account/components/update_username_spec.js
@@ -5,7 +5,7 @@ import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
-
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import UpdateUsername from '~/profile/account/components/update_username.vue';
jest.mock('~/flash');
@@ -97,7 +97,7 @@ describe('UpdateUsername component', () => {
});
it('executes API call on confirmation button click', async () => {
- axiosMock.onPut(actionUrl).replyOnce(() => [200, { message: 'Username changed' }]);
+ axiosMock.onPut(actionUrl).replyOnce(() => [HTTP_STATUS_OK, { message: 'Username changed' }]);
jest.spyOn(axios, 'put');
await wrapper.vm.onConfirm();
@@ -114,7 +114,7 @@ describe('UpdateUsername component', () => {
expect(openModalBtn.props('disabled')).toBe(false);
expect(openModalBtn.props('loading')).toBe(true);
- return [200, { message: 'Username changed' }];
+ return [HTTP_STATUS_OK, { message: 'Username changed' }];
});
await wrapper.vm.onConfirm();
@@ -133,7 +133,7 @@ describe('UpdateUsername component', () => {
expect(openModalBtn.props('disabled')).toBe(false);
expect(openModalBtn.props('loading')).toBe(true);
- return [400, { message: 'Invalid username' }];
+ return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }];
});
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
@@ -144,7 +144,7 @@ describe('UpdateUsername component', () => {
it('shows an error message if the error response has a `message` property', async () => {
axiosMock.onPut(actionUrl).replyOnce(() => {
- return [400, { message: 'Invalid username' }];
+ return [HTTP_STATUS_BAD_REQUEST, { message: 'Invalid username' }];
});
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
@@ -156,7 +156,7 @@ describe('UpdateUsername component', () => {
it("shows a fallback error message if the error response doesn't have a `message` property", async () => {
axiosMock.onPut(actionUrl).replyOnce(() => {
- return [400];
+ return [HTTP_STATUS_BAD_REQUEST];
});
await expect(wrapper.vm.onConfirm()).rejects.toThrow();
diff --git a/spec/frontend/profile/components/activity_tab_spec.js b/spec/frontend/profile/components/activity_tab_spec.js
new file mode 100644
index 00000000000..9363aad70fd
--- /dev/null
+++ b/spec/frontend/profile/components/activity_tab_spec.js
@@ -0,0 +1,19 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import ActivityTab from '~/profile/components/activity_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('ActivityTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ActivityTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Activity'));
+ });
+});
diff --git a/spec/frontend/profile/components/contributed_projects_tab_spec.js b/spec/frontend/profile/components/contributed_projects_tab_spec.js
new file mode 100644
index 00000000000..1ee55dc033d
--- /dev/null
+++ b/spec/frontend/profile/components/contributed_projects_tab_spec.js
@@ -0,0 +1,21 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import ContributedProjectsTab from '~/profile/components/contributed_projects_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('ContributedProjectsTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ContributedProjectsTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(
+ s__('UserProfile|Contributed projects'),
+ );
+ });
+});
diff --git a/spec/frontend/profile/components/followers_tab_spec.js b/spec/frontend/profile/components/followers_tab_spec.js
new file mode 100644
index 00000000000..4af428c4e0c
--- /dev/null
+++ b/spec/frontend/profile/components/followers_tab_spec.js
@@ -0,0 +1,19 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import FollowersTab from '~/profile/components/followers_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('FollowersTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(FollowersTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Followers'));
+ });
+});
diff --git a/spec/frontend/profile/components/following_tab_spec.js b/spec/frontend/profile/components/following_tab_spec.js
new file mode 100644
index 00000000000..75123274ccb
--- /dev/null
+++ b/spec/frontend/profile/components/following_tab_spec.js
@@ -0,0 +1,19 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import FollowingTab from '~/profile/components/following_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('FollowingTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(FollowingTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Following'));
+ });
+});
diff --git a/spec/frontend/profile/components/groups_tab_spec.js b/spec/frontend/profile/components/groups_tab_spec.js
new file mode 100644
index 00000000000..ec480924bdb
--- /dev/null
+++ b/spec/frontend/profile/components/groups_tab_spec.js
@@ -0,0 +1,19 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import GroupsTab from '~/profile/components/groups_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('GroupsTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(GroupsTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Groups'));
+ });
+});
diff --git a/spec/frontend/profile/components/overview_tab_spec.js b/spec/frontend/profile/components/overview_tab_spec.js
new file mode 100644
index 00000000000..eb27515bca3
--- /dev/null
+++ b/spec/frontend/profile/components/overview_tab_spec.js
@@ -0,0 +1,19 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import OverviewTab from '~/profile/components/overview_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('OverviewTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(OverviewTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Overview'));
+ });
+});
diff --git a/spec/frontend/profile/components/personal_projects_tab_spec.js b/spec/frontend/profile/components/personal_projects_tab_spec.js
new file mode 100644
index 00000000000..a701856c544
--- /dev/null
+++ b/spec/frontend/profile/components/personal_projects_tab_spec.js
@@ -0,0 +1,21 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import PersonalProjectsTab from '~/profile/components/personal_projects_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('PersonalProjectsTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(PersonalProjectsTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(
+ s__('UserProfile|Personal projects'),
+ );
+ });
+});
diff --git a/spec/frontend/profile/components/profile_tabs_spec.js b/spec/frontend/profile/components/profile_tabs_spec.js
new file mode 100644
index 00000000000..11ab372f1dd
--- /dev/null
+++ b/spec/frontend/profile/components/profile_tabs_spec.js
@@ -0,0 +1,36 @@
+import ProfileTabs from '~/profile/components/profile_tabs.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+import OverviewTab from '~/profile/components/overview_tab.vue';
+import ActivityTab from '~/profile/components/activity_tab.vue';
+import GroupsTab from '~/profile/components/groups_tab.vue';
+import ContributedProjectsTab from '~/profile/components/contributed_projects_tab.vue';
+import PersonalProjectsTab from '~/profile/components/personal_projects_tab.vue';
+import StarredProjectsTab from '~/profile/components/starred_projects_tab.vue';
+import SnippetsTab from '~/profile/components/snippets_tab.vue';
+import FollowersTab from '~/profile/components/followers_tab.vue';
+import FollowingTab from '~/profile/components/following_tab.vue';
+
+describe('ProfileTabs', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(ProfileTabs);
+ };
+
+ it.each([
+ OverviewTab,
+ ActivityTab,
+ GroupsTab,
+ ContributedProjectsTab,
+ PersonalProjectsTab,
+ StarredProjectsTab,
+ SnippetsTab,
+ FollowersTab,
+ FollowingTab,
+ ])('renders $i18n.title tab', (tab) => {
+ createComponent();
+
+ expect(wrapper.findComponent(tab).exists()).toBe(true);
+ });
+});
diff --git a/spec/frontend/profile/components/snippets_tab_spec.js b/spec/frontend/profile/components/snippets_tab_spec.js
new file mode 100644
index 00000000000..1306757314c
--- /dev/null
+++ b/spec/frontend/profile/components/snippets_tab_spec.js
@@ -0,0 +1,19 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import SnippetsTab from '~/profile/components/snippets_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('SnippetsTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(SnippetsTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(s__('UserProfile|Snippets'));
+ });
+});
diff --git a/spec/frontend/profile/components/starred_projects_tab_spec.js b/spec/frontend/profile/components/starred_projects_tab_spec.js
new file mode 100644
index 00000000000..b9f2839172f
--- /dev/null
+++ b/spec/frontend/profile/components/starred_projects_tab_spec.js
@@ -0,0 +1,21 @@
+import { GlTab } from '@gitlab/ui';
+
+import { s__ } from '~/locale';
+import StarredProjectsTab from '~/profile/components/starred_projects_tab.vue';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+
+describe('StarredProjectsTab', () => {
+ let wrapper;
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(StarredProjectsTab);
+ };
+
+ it('renders `GlTab` and sets `title` prop', () => {
+ createComponent();
+
+ expect(wrapper.findComponent(GlTab).attributes('title')).toBe(
+ s__('UserProfile|Starred projects'),
+ );
+ });
+});
diff --git a/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap b/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap
index 3025a2f87ae..f675b6cf15c 100644
--- a/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap
+++ b/spec/frontend/profile/preferences/components/__snapshots__/diffs_colors_preview_spec.js.snap
@@ -396,7 +396,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="k"
>
@@ -447,7 +446,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="k"
>
@@ -620,7 +618,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="k"
>
@@ -676,7 +673,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="k"
>
@@ -736,7 +732,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="bp"
>
@@ -782,7 +777,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="bp"
>
@@ -832,7 +826,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="bp"
>
@@ -878,7 +871,6 @@ exports[`DiffsColorsPreview component renders diff colors preview 1`] = `
<span>
</span>
-
<span
class="bp"
>
diff --git a/spec/frontend/project_select_combo_button_spec.js b/spec/frontend/project_select_combo_button_spec.js
deleted file mode 100644
index b8d5a1a61f3..00000000000
--- a/spec/frontend/project_select_combo_button_spec.js
+++ /dev/null
@@ -1,165 +0,0 @@
-import $ from 'jquery';
-import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
-import ProjectSelectComboButton from '~/project_select_combo_button';
-
-const fixturePath = 'static/project_select_combo_button.html';
-
-describe('Project Select Combo Button', () => {
- let testContext;
-
- beforeEach(() => {
- testContext = {};
- });
-
- beforeEach(() => {
- testContext.defaults = {
- label: 'Select project to create issue',
- groupId: 12345,
- projectMeta: {
- name: 'My Cool Project',
- url: 'http://mycoolproject.com',
- },
- newProjectMeta: {
- name: 'My Other Cool Project',
- url: 'http://myothercoolproject.com',
- },
- vulnerableProject: {
- name: 'Self XSS',
- // eslint-disable-next-line no-script-url
- url: 'javascript:alert(1)',
- },
- localStorageKey: 'group-12345-new-issue-recent-project',
- relativePath: 'issues/new',
- };
-
- loadHTMLFixture(fixturePath);
-
- testContext.newItemBtn = document.querySelector('.js-new-project-item-link');
- testContext.projectSelectInput = document.querySelector('.project-item-select');
- });
-
- afterEach(() => {
- resetHTMLFixture();
- });
-
- describe('on page load when localStorage is empty', () => {
- beforeEach(() => {
- testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput);
- });
-
- it('newItemBtn href is null', () => {
- expect(testContext.newItemBtn.getAttribute('href')).toBe('');
- });
-
- it('newItemBtn text is the plain default label', () => {
- expect(testContext.newItemBtn.textContent).toBe(testContext.defaults.label);
- });
- });
-
- describe('on page load when localStorage is filled', () => {
- beforeEach(() => {
- window.localStorage.setItem(
- testContext.defaults.localStorageKey,
- JSON.stringify(testContext.defaults.projectMeta),
- );
- testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput);
- });
-
- it('newItemBtn href is correctly set', () => {
- expect(testContext.newItemBtn.getAttribute('href')).toBe(
- testContext.defaults.projectMeta.url,
- );
- });
-
- it('newItemBtn text is the cached label', () => {
- expect(testContext.newItemBtn.textContent).toBe(
- `New issue in ${testContext.defaults.projectMeta.name}`,
- );
- });
-
- afterEach(() => {
- window.localStorage.clear();
- });
- });
-
- describe('after selecting a new project', () => {
- beforeEach(() => {
- testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput);
-
- // mock the effect of selecting an item from the projects dropdown (select2)
- $('.project-item-select')
- .val(JSON.stringify(testContext.defaults.newProjectMeta))
- .trigger('change');
- });
-
- it('newItemBtn href is correctly set', () => {
- expect(testContext.newItemBtn.getAttribute('href')).toBe(
- 'http://myothercoolproject.com/issues/new',
- );
- });
-
- it('newItemBtn text is the selected project label', () => {
- expect(testContext.newItemBtn.textContent).toBe(
- `New issue in ${testContext.defaults.newProjectMeta.name}`,
- );
- });
-
- afterEach(() => {
- window.localStorage.clear();
- });
- });
-
- describe('after selecting a vulnerable project', () => {
- beforeEach(() => {
- testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput);
-
- // mock the effect of selecting an item from the projects dropdown (select2)
- $('.project-item-select')
- .val(JSON.stringify(testContext.defaults.vulnerableProject))
- .trigger('change');
- });
-
- it('newItemBtn href is correctly sanitized', () => {
- expect(testContext.newItemBtn.getAttribute('href')).toBe('about:blank');
- });
-
- afterEach(() => {
- window.localStorage.clear();
- });
- });
-
- describe('deriveTextVariants', () => {
- beforeEach(() => {
- testContext.mockExecutionContext = {
- resourceType: '',
- resourceLabel: '',
- };
-
- testContext.comboButton = new ProjectSelectComboButton(testContext.projectSelectInput);
-
- testContext.method = testContext.comboButton.deriveTextVariants.bind(
- testContext.mockExecutionContext,
- );
- });
-
- it('correctly derives test variants for merge requests', () => {
- testContext.mockExecutionContext.resourceType = 'merge_requests';
- testContext.mockExecutionContext.resourceLabel = 'New merge request';
-
- const returnedVariants = testContext.method();
-
- expect(returnedVariants.localStorageItemType).toBe('new-merge-request');
- expect(returnedVariants.presetTextSuffix).toBe('merge request');
- });
-
- it('correctly derives text variants for issues', () => {
- testContext.mockExecutionContext.resourceType = 'issues';
- testContext.mockExecutionContext.resourceLabel = 'New issue';
-
- const returnedVariants = testContext.method();
-
- expect(returnedVariants.localStorageItemType).toBe('new-issue');
- expect(returnedVariants.presetTextSuffix).toBe('issue');
- });
- });
-});
diff --git a/spec/frontend/projects/commit/components/branches_dropdown_spec.js b/spec/frontend/projects/commit/components/branches_dropdown_spec.js
index a84dd246f5d..6aa5a9a5a3a 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);
@@ -35,11 +34,11 @@ 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);
+
+ beforeEach(() => {
+ createComponent({ value: '' });
+ });
afterEach(() => {
wrapper.destroy();
@@ -48,138 +47,36 @@ describe('BranchesDropdown', () => {
});
describe('On mount', () => {
- beforeEach(() => {
- createComponent({ value: '' });
- });
-
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 });
+ describe('Value prop changes in parent component', () => {
+ it('triggers fetchBranches call', async () => {
+ await wrapper.setProps({ value: 'new value' });
- 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,
- });
+ expect(spyFetchBranches).toHaveBeenCalled();
});
});
- describe('Search term is empty', () => {
- beforeEach(() => {
- createComponent({ value: '' });
- });
+ describe('Selecting Dropdown Item', () => {
+ it('emits event', async () => {
+ findDropdown().vm.$emit('select', '_anything_');
- 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);
+ expect(wrapper.emitted()).toHaveProperty('input');
});
});
describe('When searching', () => {
- beforeEach(() => {
- createComponent({ value: '' });
- });
-
it('invokes fetchBranches', async () => {
const spy = jest.spyOn(wrapper.vm, 'fetchBranches');
- findSearchBoxByType().vm.$emit('input', '_anything_');
+ findDropdown().vm.$emit('search', '_anything_');
await nextTick();
expect(spy).toHaveBeenCalledWith('_anything_');
- expect(wrapper.vm.searchTerm).toBe('_anything_');
- });
- });
-
- 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_');
});
});
});
diff --git a/spec/frontend/projects/commit/components/form_modal_spec.js b/spec/frontend/projects/commit/components/form_modal_spec.js
index 20c312ec771..c59cf700e0d 100644
--- a/spec/frontend/projects/commit/components/form_modal_spec.js
+++ b/spec/frontend/projects/commit/components/form_modal_spec.js
@@ -157,7 +157,7 @@ describe('CommitFormModal', () => {
});
it('Changes the start_branch input value', async () => {
- findBranchesDropdown().vm.$emit('selectBranch', '_changed_branch_value_');
+ findBranchesDropdown().vm.$emit('input', '_changed_branch_value_');
await nextTick();
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/commit/mock_data.js b/spec/frontend/projects/commit/mock_data.js
index 34e9c400af4..e398d46e69c 100644
--- a/spec/frontend/projects/commit/mock_data.js
+++ b/spec/frontend/projects/commit/mock_data.js
@@ -24,5 +24,9 @@ export default {
openModal: '_open_modal_',
},
mockBranches: ['_branch_1', '_abc_', '_main_'],
- mockProjects: ['_project_1', '_abc_', '_project_'],
+ mockProjects: [
+ { id: 1, name: '_project_1', refsUrl: '/_project_1/refs' },
+ { id: 2, name: '_abc_', refsUrl: '/_abc_/refs' },
+ { id: 3, name: '_project_', refsUrl: '/_project_/refs' },
+ ],
};
diff --git a/spec/frontend/projects/commit/store/getters_spec.js b/spec/frontend/projects/commit/store/getters_spec.js
index 38c45af7aa0..f45f3114550 100644
--- a/spec/frontend/projects/commit/store/getters_spec.js
+++ b/spec/frontend/projects/commit/store/getters_spec.js
@@ -29,9 +29,15 @@ describe('Commit form modal getters', () => {
});
it('should provide a uniq list of projects', () => {
- const projects = ['_project_', '_project_', '_some_other_project'];
+ const projects = [
+ { id: 1, name: '_project_', refsUrl: '/_project_/refs' },
+ { id: 1, name: '_project_', refsUrl: '/_project_/refs' },
+ { id: 3, name: '_some_other_project', refsUrl: '/_some_other_project/refs' },
+ ];
const state = { projects };
+ expect(state.projects.length).toBe(3);
+ expect(getters.sortedProjects(state).length).toBe(2);
expect(getters.sortedProjects(state)).toEqual(projects.slice(1));
});
});
diff --git a/spec/frontend/projects/commit_box/info/load_branches_spec.js b/spec/frontend/projects/commit_box/info/load_branches_spec.js
index 9456e6ef5f5..e49d92188ed 100644
--- a/spec/frontend/projects/commit_box/info/load_branches_spec.js
+++ b/spec/frontend/projects/commit_box/info/load_branches_spec.js
@@ -2,6 +2,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { setHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { loadBranches } from '~/projects/commit_box/info/load_branches';
const mockCommitPath = '/commit/abcd/branches';
@@ -22,7 +23,7 @@ describe('~/projects/commit_box/info/load_branches', () => {
</div>`);
mock = new MockAdapter(axios);
- mock.onGet(mockCommitPath).reply(200, mockBranchesRes);
+ mock.onGet(mockCommitPath).reply(HTTP_STATUS_OK, mockBranchesRes);
});
it('loads and renders branches info', async () => {
@@ -45,7 +46,7 @@ describe('~/projects/commit_box/info/load_branches', () => {
beforeEach(() => {
mock
.onGet(mockCommitPath)
- .reply(200, '<a onload="alert(\'xss!\');" href="/-/commits/main">main</a>');
+ .reply(HTTP_STATUS_OK, '<a onload="alert(\'xss!\');" href="/-/commits/main">main</a>');
});
it('displays sanitized html', async () => {
@@ -60,7 +61,7 @@ describe('~/projects/commit_box/info/load_branches', () => {
describe('when branches request fails', () => {
beforeEach(() => {
- mock.onGet(mockCommitPath).reply(500, 'Error!');
+ mock.onGet(mockCommitPath).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, 'Error!');
});
it('attempts to load and renders an error', async () => {
diff --git a/spec/frontend/projects/commits/store/actions_spec.js b/spec/frontend/projects/commits/store/actions_spec.js
index 930b801af71..bae9c48fc1e 100644
--- a/spec/frontend/projects/commits/store/actions_spec.js
+++ b/spec/frontend/projects/commits/store/actions_spec.js
@@ -2,6 +2,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import actions from '~/projects/commits/store/actions';
import * as types from '~/projects/commits/store/mutation_types';
import createState from '~/projects/commits/store/state';
@@ -51,7 +52,7 @@ describe('Project commits actions', () => {
state.projectId = '8';
const data = [{ id: 1 }];
- mock.onGet(path).replyOnce(200, data);
+ mock.onGet(path).replyOnce(HTTP_STATUS_OK, data);
testAction(
actions.fetchAuthors,
null,
@@ -63,7 +64,7 @@ describe('Project commits actions', () => {
it('dispatches request/receive on error', () => {
const path = '/-/autocomplete/users.json';
- mock.onGet(path).replyOnce(500);
+ mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
testAction(actions.fetchAuthors, null, state, [], [{ type: 'receiveAuthorsError' }]);
});
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
index c21c0f4f9d1..53763bd7d8f 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_legacy_spec.js
@@ -4,6 +4,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown_legacy.vue';
const defaultProps = {
@@ -50,7 +51,7 @@ describe('RevisionDropdown component', () => {
const Branches = ['branch-1', 'branch-2'];
const Tags = ['tag-1', 'tag-2', 'tag-3'];
- axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(200, {
+ axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(HTTP_STATUS_OK, {
Branches,
Tags,
});
@@ -64,7 +65,7 @@ describe('RevisionDropdown component', () => {
});
it('sets branches and tags to be an empty array when no tags or branches are given', async () => {
- axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(200, {
+ axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(HTTP_STATUS_OK, {
Branches: undefined,
Tags: undefined,
});
@@ -76,7 +77,7 @@ describe('RevisionDropdown component', () => {
});
it('shows flash message on error', async () => {
- axiosMock.onGet('some/invalid/path').replyOnce(404);
+ axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
await wrapper.vm.fetchBranchesAndTags();
expect(createAlert).toHaveBeenCalled();
diff --git a/spec/frontend/projects/compare/components/revision_dropdown_spec.js b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
index d598bafea92..db4a1158996 100644
--- a/spec/frontend/projects/compare/components/revision_dropdown_spec.js
+++ b/spec/frontend/projects/compare/components/revision_dropdown_spec.js
@@ -4,6 +4,7 @@ import AxiosMockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import RevisionDropdown from '~/projects/compare/components/revision_dropdown.vue';
import { revisionDropdownDefaultProps as defaultProps } from './mock_data';
@@ -49,7 +50,7 @@ describe('RevisionDropdown component', () => {
const Branches = ['branch-1', 'branch-2'];
const Tags = ['tag-1', 'tag-2', 'tag-3'];
- axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(200, {
+ axiosMock.onGet(defaultProps.refsProjectPath).replyOnce(HTTP_STATUS_OK, {
Branches,
Tags,
});
@@ -62,7 +63,7 @@ describe('RevisionDropdown component', () => {
});
it('shows flash message on error', async () => {
- axiosMock.onGet('some/invalid/path').replyOnce(404);
+ axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
createComponent();
@@ -88,7 +89,7 @@ describe('RevisionDropdown component', () => {
describe('search', () => {
it('shows flash message on error', async () => {
- axiosMock.onGet('some/invalid/path').replyOnce(404);
+ axiosMock.onGet('some/invalid/path').replyOnce(HTTP_STATUS_NOT_FOUND);
createComponent();
diff --git a/spec/frontend/projects/project_find_file_spec.js b/spec/frontend/projects/project_find_file_spec.js
index eec54dd04bc..efc9d411a98 100644
--- a/spec/frontend/projects/project_find_file_spec.js
+++ b/spec/frontend/projects/project_find_file_spec.js
@@ -4,6 +4,7 @@ import { TEST_HOST } from 'helpers/test_constants';
import waitForPromises from 'helpers/wait_for_promises';
import { sanitize } from '~/lib/dompurify';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ProjectFindFile from '~/projects/project_find_file';
jest.mock('~/lib/dompurify', () => ({
@@ -60,7 +61,7 @@ describe('ProjectFindFile', () => {
element = $(TEMPLATE);
mock.onGet(FILE_FIND_URL).replyOnce(
- 200,
+ HTTP_STATUS_OK,
files.map((x) => x.path),
);
getProjectFindFileInstance(); // This triggers a load / axios call + subsequent render in the constructor
diff --git a/spec/frontend/projects/project_new_spec.js b/spec/frontend/projects/project_new_spec.js
index d69bfc4ec92..8a1e9904a3f 100644
--- a/spec/frontend/projects/project_new_spec.js
+++ b/spec/frontend/projects/project_new_spec.js
@@ -9,6 +9,7 @@ describe('New Project', () => {
let $projectPath;
let $projectName;
let $projectNameError;
+ let $projectNameDescription;
const mockKeyup = (el) => el.dispatchEvent(new KeyboardEvent('keyup'));
const mockChange = (el) => el.dispatchEvent(new Event('change'));
@@ -31,7 +32,8 @@ describe('New Project', () => {
</div>
</div>
<input id="project_name" />
- <div class="gl-field-error hidden" id="project_name_error" />
+ <small id="js-project-name-description" />
+ <div class="gl-field-error gl-display-none" id="js-project-name-error" />
<input id="project_path" />
</div>
<div class="js-user-readme-repo"></div>
@@ -44,7 +46,8 @@ describe('New Project', () => {
$projectImportUrl = document.querySelector('#project_import_url');
$projectPath = document.querySelector('#project_path');
$projectName = document.querySelector('#project_name');
- $projectNameError = document.querySelector('#project_name_error');
+ $projectNameError = document.querySelector('#js-project-name-error');
+ $projectNameDescription = document.querySelector('#js-project-name-description');
});
afterEach(() => {
@@ -98,7 +101,7 @@ describe('New Project', () => {
});
it('no error message by default', () => {
- expect($projectNameError.classList.contains('hidden')).toBe(true);
+ expect($projectNameError.classList.contains('gl-display-none')).toBe(true);
});
it('show error message if name is validate', () => {
@@ -106,15 +109,16 @@ describe('New Project', () => {
triggerEvent($projectName, 'change');
expect($projectNameError.innerText).toBe(
- "Name must start with a letter, digit, emoji, or '_'",
+ 'Name must start with a letter, digit, emoji, or underscore.',
);
- expect($projectNameError.classList.contains('hidden')).toBe(false);
+ expect($projectNameError.classList.contains('gl-display-none')).toBe(false);
+ expect($projectNameDescription.classList.contains('gl-display-none')).toBe(true);
});
});
describe('project name rule', () => {
describe("Name must start with a letter, digit, emoji, or '_'", () => {
- const errormsg = "Name must start with a letter, digit, emoji, or '_'";
+ const errormsg = 'Name must start with a letter, digit, emoji, or underscore.';
it("'.foo' should error", () => {
const text = '.foo';
expect(checkRules(text)).toBe(errormsg);
@@ -127,7 +131,7 @@ describe('New Project', () => {
describe("Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces", () => {
const errormsg =
- "Name can contain only letters, digits, emojis, '_', '.', '+', dashes, or spaces";
+ 'Name can contain only lowercase or uppercase letters, digits, emojis, spaces, dots, underscores, dashes, or pluses.';
it("'foo(#^.^#)foo' should error", () => {
const text = 'foo(#^.^#)foo';
expect(checkRules(text)).toBe(errormsg);
diff --git a/spec/frontend/projects/prune_unreachable_objects_button_spec.js b/spec/frontend/projects/prune_unreachable_objects_button_spec.js
new file mode 100644
index 00000000000..b345f264ca7
--- /dev/null
+++ b/spec/frontend/projects/prune_unreachable_objects_button_spec.js
@@ -0,0 +1,72 @@
+import { GlButton, GlModal, GlLink } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
+import { s__ } from '~/locale';
+import PruneObjectsButton from '~/projects/prune_unreachable_objects_button.vue';
+
+jest.mock('~/lib/utils/csrf', () => ({ token: 'test-csrf-token' }));
+
+describe('Project remove modal', () => {
+ let wrapper;
+
+ const findFormElement = () => wrapper.find('form');
+ const findAuthenticityTokenInput = () => findFormElement().find('input[name=authenticity_token]');
+ const findModal = () => wrapper.findComponent(GlModal);
+ const findBtn = () => wrapper.findComponent(GlButton);
+ const defaultProps = {
+ pruneObjectsPath: 'prunepath',
+ pruneObjectsDocPath: 'prunedocspath',
+ };
+
+ const createComponent = () => {
+ wrapper = shallowMountExtended(PruneObjectsButton, {
+ propsData: defaultProps,
+ directives: {
+ GlModal: createMockDirective(),
+ },
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ wrapper = null;
+ });
+
+ describe('intialized', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets a csrf token on the authenticity form input', () => {
+ expect(findAuthenticityTokenInput().element.value).toEqual('test-csrf-token');
+ });
+
+ it('sets the form action to the provided path', () => {
+ expect(findFormElement().attributes('action')).toEqual(defaultProps.pruneObjectsPath);
+ });
+
+ it('sets the documentation link to the provided path', () => {
+ expect(findModal().findComponent(GlLink).attributes('href')).toEqual(
+ defaultProps.pruneObjectsDocPath,
+ );
+ });
+
+ it('button opens modal', () => {
+ const buttonModalDirective = getBinding(findBtn().element, 'gl-modal');
+
+ expect(findModal().props('modalId')).toBe(buttonModalDirective.value);
+ expect(findModal().text()).toContain(s__('UpdateProject|Are you sure you want to prune?'));
+ });
+ });
+
+ describe('when the modal is confirmed', () => {
+ beforeEach(() => {
+ createComponent();
+ findModal().vm.$emit('ok');
+ });
+
+ it('submits the form element', () => {
+ expect(findFormElement().element.submit).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/projects/merge_requests/components/report_abuse_dropdown_item_spec.js b/spec/frontend/projects/report_abuse/components/report_abuse_dropdown_item_spec.js
index 35b10375821..de0c889e8c9 100644
--- a/spec/frontend/projects/merge_requests/components/report_abuse_dropdown_item_spec.js
+++ b/spec/frontend/projects/report_abuse/components/report_abuse_dropdown_item_spec.js
@@ -3,14 +3,14 @@ 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 ReportAbuseDropdownItem from '~/projects/report_abuse/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 USER_ID = 1;
const REPORTED_FROM_URL = 'http://example.com';
const createComponent = (props) => {
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
index bc373d9deb7..714e0df596e 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/index_spec.js
@@ -1,23 +1,21 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
import * as util from '~/lib/utils/url_utility';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import RuleView from '~/projects/settings/branch_rules/components/view/index.vue';
+import Protection from '~/projects/settings/branch_rules/components/view/protection.vue';
import {
I18N,
ALL_BRANCHES_WILDCARD,
} from '~/projects/settings/branch_rules/components/view/constants';
-import Protection from '~/projects/settings/branch_rules/components/view/protection.vue';
-import branchRulesQuery from '~/projects/settings/branch_rules/queries/branch_rules_details.query.graphql';
+import branchRulesQuery from 'ee_else_ce/projects/settings/branch_rules/queries/branch_rules_details.query.graphql';
import { sprintf } from '~/locale';
import {
branchProtectionsMockResponse,
- approvalRulesMock,
- statusChecksRulesMock,
matchingBranchesCount,
-} from './mock_data';
+} from 'ee_else_ce_jest/projects/settings/branch_rules/components/view/mock_data';
jest.mock('~/lib/utils/url_utility', () => ({
getParameterByName: jest.fn().mockReturnValue('main'),
@@ -29,18 +27,18 @@ Vue.use(VueApollo);
const protectionMockProps = {
headerLinkHref: 'protected/branches',
- headerLinkTitle: 'Manage in Protected Branches',
- roles: [{ accessLevelDescription: 'Maintainers' }],
- users: [{ avatarUrl: 'test.com/user.png', name: 'peter', webUrl: 'test.com' }],
+ headerLinkTitle: I18N.manageProtectionsLinkTitle,
};
+const roles = [
+ { accessLevelDescription: 'Maintainers' },
+ { accessLevelDescription: 'Maintainers + Developers' },
+];
describe('View branch rules', () => {
let wrapper;
let fakeApollo;
const projectPath = 'test/testing';
const protectedBranchesPath = 'protected/branches';
- const approvalRulesPath = 'approval/rules';
- const statusChecksPath = 'status/checks';
const branchProtectionsMockRequestHandler = jest
.fn()
.mockResolvedValue(branchProtectionsMockResponse);
@@ -50,7 +48,8 @@ describe('View branch rules', () => {
wrapper = shallowMountExtended(RuleView, {
apolloProvider: fakeApollo,
- provide: { projectPath, protectedBranchesPath, approvalRulesPath, statusChecksPath },
+ provide: { projectPath, protectedBranchesPath },
+ stubs: { Protection },
});
await waitForPromises();
@@ -106,41 +105,53 @@ describe('View branch rules', () => {
it('renders a branch protection component for push rules', () => {
expect(findBranchProtections().at(0).props()).toMatchObject({
- header: sprintf(I18N.allowedToPushHeader, { total: 2 }),
+ header: sprintf(I18N.allowedToPushHeader, {
+ total: 2,
+ }),
...protectionMockProps,
});
});
+ it('passes expected roles for push rules via props', () => {
+ findBranchProtections()
+ .at(0)
+ .props()
+ .roles.forEach((role, i) => {
+ expect(role).toMatchObject({
+ accessLevelDescription: roles[i].accessLevelDescription,
+ });
+ });
+ });
+
it('renders force push protection', () => {
expect(findForcePushTitle().exists()).toBe(true);
});
it('renders a branch protection component for merge rules', () => {
expect(findBranchProtections().at(1).props()).toMatchObject({
- header: sprintf(I18N.allowedToMergeHeader, { total: 2 }),
+ header: sprintf(I18N.allowedToMergeHeader, {
+ total: 2,
+ }),
...protectionMockProps,
});
});
- it('renders a branch protection component for approvals', () => {
- expect(findApprovalsTitle().exists()).toBe(true);
-
- expect(findBranchProtections().at(2).props()).toMatchObject({
- header: sprintf(I18N.approvalsHeader, { total: 3 }),
- headerLinkHref: approvalRulesPath,
- headerLinkTitle: I18N.manageApprovalsLinkTitle,
- approvals: approvalRulesMock,
- });
+ it('passes expected roles form merge rules via props', () => {
+ findBranchProtections()
+ .at(1)
+ .props()
+ .roles.forEach((role, i) => {
+ expect(role).toMatchObject({
+ accessLevelDescription: roles[i].accessLevelDescription,
+ });
+ });
});
- it('renders a branch protection component for status checks', () => {
- expect(findStatusChecksTitle().exists()).toBe(true);
+ it('does not render a branch protection component for approvals', () => {
+ expect(findApprovalsTitle().exists()).toBe(false);
+ });
- expect(findBranchProtections().at(3).props()).toMatchObject({
- header: sprintf(I18N.statusChecksHeader, { total: 2 }),
- headerLinkHref: statusChecksPath,
- headerLinkTitle: I18N.statusChecksLinkTitle,
- statusChecks: statusChecksRulesMock,
- });
+ it('does not render a branch protection component for status checks', () => {
+ expect(findStatusChecksTitle().exists()).toBe(false);
});
});
diff --git a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
index 821dba75b62..c64af7767cc 100644
--- a/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
+++ b/spec/frontend/projects/settings/branch_rules/components/view/mock_data.js
@@ -85,16 +85,8 @@ export const accessLevelsMockResponse = [
__typename: 'PushAccessLevelEdge',
node: {
__typename: 'PushAccessLevel',
- accessLevel: 40,
- accessLevelDescription: 'Jona Langworth',
- group: null,
- user: {
- __typename: 'UserCore',
- id: '123',
- webUrl: 'test.com',
- name: 'peter',
- avatarUrl: 'test.com/user.png',
- },
+ accessLevel: 30,
+ accessLevelDescription: 'Maintainers',
},
},
{
@@ -102,9 +94,7 @@ export const accessLevelsMockResponse = [
node: {
__typename: 'PushAccessLevel',
accessLevel: 40,
- accessLevelDescription: 'Maintainers',
- group: null,
- user: null,
+ accessLevelDescription: 'Maintainers + Developers',
},
},
];
@@ -122,10 +112,10 @@ export const branchProtectionsMockResponse = {
{
__typename: 'BranchRule',
name: 'main',
+ matchingBranchesCount,
branchProtection: {
__typename: 'BranchProtection',
allowForcePush: true,
- codeOwnerApprovalRequired: true,
mergeAccessLevels: {
__typename: 'MergeAccessLevelConnection',
edges: accessLevelsMockResponse,
@@ -135,41 +125,23 @@ export const branchProtectionsMockResponse = {
edges: accessLevelsMockResponse,
},
},
- approvalRules: {
- __typename: 'ApprovalProjectRuleConnection',
- nodes: approvalRulesMock,
- },
- externalStatusChecks: {
- __typename: 'ExternalStatusCheckConnection',
- nodes: statusChecksRulesMock,
- },
- matchingBranchesCount,
},
{
__typename: 'BranchRule',
name: '*',
+ matchingBranchesCount,
branchProtection: {
__typename: 'BranchProtection',
allowForcePush: true,
- codeOwnerApprovalRequired: true,
mergeAccessLevels: {
__typename: 'MergeAccessLevelConnection',
- edges: [],
+ edges: accessLevelsMockResponse,
},
pushAccessLevels: {
__typename: 'PushAccessLevelConnection',
- edges: [],
+ edges: accessLevelsMockResponse,
},
},
- approvalRules: {
- __typename: 'ApprovalProjectRuleConnection',
- nodes: [],
- },
- externalStatusChecks: {
- __typename: 'ExternalStatusCheckConnection',
- nodes: [],
- },
- matchingBranchesCount,
},
],
},
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 bfbf3e234f4..ca9a72663d2 100644
--- a/spec/frontend/projects/settings/components/default_branch_selector_spec.js
+++ b/spec/frontend/projects/settings/components/default_branch_selector_spec.js
@@ -34,6 +34,7 @@ describe('projects/settings/components/default_branch_selector', () => {
projectId,
refType: null,
state: true,
+ toggleButtonClass: null,
translations: {
dropdownHeader: expect.any(String),
searchPlaceholder: expect.any(String),
diff --git a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
index 1b06f7874a3..26297d0c3ff 100644
--- a/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
+++ b/spec/frontend/projects/settings/components/new_access_dropdown_spec.js
@@ -5,6 +5,7 @@ import {
GlDropdownSectionHeader,
GlSearchBoxByType,
} from '@gitlab/ui';
+import { last } from 'lodash';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
@@ -254,7 +255,6 @@ describe('Access Level Dropdown', () => {
createComponent({ preselectedItems });
await waitForPromises();
- const spy = jest.spyOn(wrapper.vm, '$emit');
const dropdownItems = findAllDropdownItems();
// select new item from each group
findDropdownItemWithText(dropdownItems, 'role1').trigger('click');
@@ -267,7 +267,7 @@ describe('Access Level Dropdown', () => {
findDropdownItemWithText(dropdownItems, 'user8').trigger('click');
findDropdownItemWithText(dropdownItems, 'key11').trigger('click');
- expect(spy).toHaveBeenLastCalledWith('select', [
+ expect(last(wrapper.emitted('select'))[0]).toStrictEqual([
{ access_level: 1 },
{ id: 112, access_level: 2, _destroy: true },
{ id: 113, access_level: 3 },
@@ -347,12 +347,10 @@ describe('Access Level Dropdown', () => {
});
it('should emit `hidden` event with dropdown selection', () => {
- jest.spyOn(wrapper.vm, '$emit');
-
findAllDropdownItems().at(1).trigger('click');
findDropdown().vm.$emit('hidden');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('hidden', [{ access_level: 2 }]);
+ expect(wrapper.emitted('hidden')[0][0]).toStrictEqual([{ access_level: 2 }]);
});
});
});
diff --git a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
index 329060b9d10..f82ad80135e 100644
--- a/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
+++ b/spec/frontend/projects/settings/components/shared_runners_toggle_spec.js
@@ -4,6 +4,7 @@ import MockAxiosAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK, HTTP_STATUS_UNAUTHORIZED } from '~/lib/utils/http_status';
import SharedRunnersToggleComponent from '~/projects/settings/components/shared_runners_toggle.vue';
const TEST_UPDATE_PATH = '/test/update_shared_runners';
@@ -36,7 +37,7 @@ describe('projects/settings/components/shared_runners', () => {
beforeEach(() => {
mockAxios = new MockAxiosAdapter(axios);
- mockAxios.onPost(TEST_UPDATE_PATH).reply(200);
+ mockAxios.onPost(TEST_UPDATE_PATH).reply(HTTP_STATUS_OK);
});
afterEach(() => {
@@ -132,7 +133,9 @@ describe('projects/settings/components/shared_runners', () => {
describe('when request encounters an error', () => {
it('should show custom error message from API if it exists', async () => {
- mockAxios.onPost(TEST_UPDATE_PATH).reply(401, { error: 'Custom API Error message' });
+ mockAxios
+ .onPost(TEST_UPDATE_PATH)
+ .reply(HTTP_STATUS_UNAUTHORIZED, { error: 'Custom API Error message' });
createComponent();
expect(getToggleValue()).toBe(false);
@@ -144,7 +147,7 @@ describe('projects/settings/components/shared_runners', () => {
});
it('should show default error message if API does not return a custom error message', async () => {
- mockAxios.onPost(TEST_UPDATE_PATH).reply(401);
+ mockAxios.onPost(TEST_UPDATE_PATH).reply(HTTP_STATUS_UNAUTHORIZED);
createComponent();
expect(getToggleValue()).toBe(false);
diff --git a/spec/frontend/projects/settings/mock_data.js b/spec/frontend/projects/settings/mock_data.js
index 0262c0e3e43..86e5396bd25 100644
--- a/spec/frontend/projects/settings/mock_data.js
+++ b/spec/frontend/projects/settings/mock_data.js
@@ -47,11 +47,7 @@ export const pushAccessLevelsMockResult = {
groups: [],
roles: [
{
- __typename: 'PushAccessLevel',
- accessLevel: 40,
accessLevelDescription: 'Maintainers',
- group: null,
- user: null,
},
],
};
diff --git a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
index 447d7e86ceb..56b39f04580 100644
--- a/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
+++ b/spec/frontend/projects/settings/repository/branch_rules/app_spec.js
@@ -1,9 +1,10 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
+import { GlModal } from '@gitlab/ui';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import BranchRules, { i18n } from '~/projects/settings/repository/branch_rules/app.vue';
+import BranchRules from '~/projects/settings/repository/branch_rules/app.vue';
import BranchRule from '~/projects/settings/repository/branch_rules/components/branch_rule.vue';
import branchRulesQuery from 'ee_else_ce/projects/settings/repository/branch_rules/graphql/queries/branch_rules.query.graphql';
import { createAlert } from '~/flash';
@@ -11,8 +12,19 @@ import {
branchRulesMockResponse,
appProvideMock,
} from 'ee_else_ce_jest/projects/settings/repository/branch_rules/mock_data';
+import {
+ I18N,
+ BRANCH_PROTECTION_MODAL_ID,
+ PROTECTED_BRANCHES_ANCHOR,
+} from '~/projects/settings/repository/branch_rules/constants';
+import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
+import { expandSection } from '~/settings_panels';
+import { scrollToElement } from '~/lib/utils/common_utils';
+import { createMockDirective, getBinding } from 'helpers/vue_mock_directive';
jest.mock('~/flash');
+jest.mock('~/settings_panels');
+jest.mock('~/lib/utils/common_utils');
Vue.use(VueApollo);
@@ -28,6 +40,8 @@ describe('Branch rules app', () => {
wrapper = mountExtended(BranchRules, {
apolloProvider: fakeApollo,
provide: appProvideMock,
+ stubs: { GlModal: stubComponent(GlModal, { template: RENDER_ALL_SLOTS_TEMPLATE }) },
+ directives: { GlModal: createMockDirective() },
});
await waitForPromises();
@@ -35,17 +49,19 @@ describe('Branch rules app', () => {
const findAllBranchRules = () => wrapper.findAllComponents(BranchRule);
const findEmptyState = () => wrapper.findByTestId('empty');
+ const findAddBranchRuleButton = () => wrapper.findByRole('button', I18N.addBranchRule);
+ const findModal = () => wrapper.findComponent(GlModal);
beforeEach(() => createComponent());
it('displays an error if branch rules query fails', async () => {
await createComponent({ queryHandler: jest.fn().mockRejectedValue() });
- expect(createAlert).toHaveBeenCalledWith({ message: i18n.queryError });
+ expect(createAlert).toHaveBeenCalledWith({ message: I18N.queryError });
});
it('displays an empty state if no branch rules are present', async () => {
await createComponent({ queryHandler: jest.fn().mockRejectedValue() });
- expect(findEmptyState().text()).toBe(i18n.emptyState);
+ expect(findEmptyState().text()).toBe(I18N.emptyState);
});
it('renders branch rules', () => {
@@ -61,4 +77,38 @@ describe('Branch rules app', () => {
expect(findAllBranchRules().at(1).props('branchProtection')).toEqual(nodes[1].branchProtection);
});
+
+ describe('Add branch rule', () => {
+ it('renders an Add branch rule button', () => {
+ expect(findAddBranchRuleButton().exists()).toBe(true);
+ });
+
+ it('renders a modal with correct props/attributes', () => {
+ expect(findModal().props()).toMatchObject({
+ modalId: BRANCH_PROTECTION_MODAL_ID,
+ title: I18N.addBranchRule,
+ });
+
+ expect(findModal().attributes('ok-title')).toBe(I18N.createProtectedBranch);
+ });
+
+ it('renders correct modal id for the default action', () => {
+ const binding = getBinding(findAddBranchRuleButton().element, 'gl-modal');
+
+ expect(binding.value).toBe(BRANCH_PROTECTION_MODAL_ID);
+ });
+
+ it('renders the correct modal content', () => {
+ expect(findModal().text()).toContain(I18N.branchRuleModalDescription);
+ expect(findModal().text()).toContain(I18N.branchRuleModalContent);
+ });
+
+ it('when the primary modal action is clicked, takes user to the correct location', () => {
+ findAddBranchRuleButton().trigger('click');
+ findModal().vm.$emit('ok');
+
+ expect(expandSection).toHaveBeenCalledWith(PROTECTED_BRANCHES_ANCHOR);
+ expect(scrollToElement).toHaveBeenCalledWith(PROTECTED_BRANCHES_ANCHOR);
+ });
+ });
});
diff --git a/spec/frontend/prometheus_metrics/custom_metrics_spec.js b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
index a079b0b97fd..3852f2678b7 100644
--- a/spec/frontend/prometheus_metrics/custom_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/custom_metrics_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PANEL_STATE from '~/prometheus_metrics/constants';
import CustomMetrics from '~/prometheus_metrics/custom_metrics';
import { metrics1 as metrics } from './mock_data';
@@ -13,7 +14,7 @@ describe('PrometheusMetrics', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(customMetricsEndpoint).reply(200, {
+ mock.onGet(customMetricsEndpoint).reply(HTTP_STATUS_OK, {
metrics,
});
loadHTMLFixture(FIXTURE);
diff --git a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
index a65cbe1a47a..45654d6a2eb 100644
--- a/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
+++ b/spec/frontend/prometheus_metrics/prometheus_metrics_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import PANEL_STATE from '~/prometheus_metrics/constants';
import PrometheusMetrics from '~/prometheus_metrics/prometheus_metrics';
import { metrics2 as metrics, missingVarMetrics } from './mock_data';
@@ -116,7 +117,7 @@ describe('PrometheusMetrics', () => {
let mock;
function mockSuccess() {
- mock.onGet(prometheusMetrics.activeMetricsEndpoint).reply(200, {
+ mock.onGet(prometheusMetrics.activeMetricsEndpoint).reply(HTTP_STATUS_OK, {
data: metrics,
success: true,
});
diff --git a/spec/frontend/protected_branches/protected_branch_edit_spec.js b/spec/frontend/protected_branches/protected_branch_edit_spec.js
index 0aec4fbc037..b4029d94980 100644
--- a/spec/frontend/protected_branches/protected_branch_edit_spec.js
+++ b/spec/frontend/protected_branches/protected_branch_edit_spec.js
@@ -4,6 +4,7 @@ import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import { TEST_HOST } from 'helpers/test_constants';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ProtectedBranchEdit from '~/protected_branches/protected_branch_edit';
jest.mock('~/flash');
@@ -115,7 +116,9 @@ describe('ProtectedBranchEdit', () => {
describe('when clicked', () => {
beforeEach(async () => {
- mock.onPatch(TEST_URL, { protected_branch: { [patchParam]: true } }).replyOnce(200, {});
+ mock
+ .onPatch(TEST_URL, { protected_branch: { [patchParam]: true } })
+ .replyOnce(HTTP_STATUS_OK, {});
});
it('checks and disables button', async () => {
@@ -142,7 +145,7 @@ describe('ProtectedBranchEdit', () => {
describe('when clicked and BE error', () => {
beforeEach(() => {
- mock.onPatch(TEST_URL).replyOnce(500);
+ mock.onPatch(TEST_URL).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
toggle.click();
});
diff --git a/spec/frontend/ref/components/ref_selector_spec.js b/spec/frontend/ref/components/ref_selector_spec.js
index 4997c13bbb2..40d3a291074 100644
--- a/spec/frontend/ref/components/ref_selector_spec.js
+++ b/spec/frontend/ref/components/ref_selector_spec.js
@@ -1,5 +1,4 @@
-import { GlLoadingIcon, GlSearchBoxByType, GlDropdownItem, GlDropdown, GlIcon } from '@gitlab/ui';
-import { mount } from '@vue/test-utils';
+import { GlLoadingIcon, GlCollapsibleListbox, GlListboxItem } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
@@ -8,8 +7,13 @@ import Vuex from 'vuex';
import commit from 'test_fixtures/api/commits/commit.json';
import branches from 'test_fixtures/api/branches/branches.json';
import tags from 'test_fixtures/api/tags/tags.json';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import { trimText } from 'helpers/text_helper';
-import { ENTER_KEY } from '~/lib/utils/keys';
+import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_NOT_FOUND,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
import { sprintf } from '~/locale';
import RefSelector from '~/ref/components/ref_selector.vue';
import {
@@ -37,7 +41,7 @@ describe('Ref selector component', () => {
let requestSpies;
const createComponent = (mountOverrides = {}, propsData = {}) => {
- wrapper = mount(
+ wrapper = mountExtended(
RefSelector,
merge(
{
@@ -52,9 +56,6 @@ describe('Ref selector component', () => {
wrapper.setProps({ value: selectedRef });
},
},
- stubs: {
- GlSearchBoxByType: true,
- },
store: createStore(),
},
mountOverrides,
@@ -68,9 +69,11 @@ describe('Ref selector component', () => {
branchesApiCallSpy = jest
.fn()
- .mockReturnValue([200, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]);
- tagsApiCallSpy = jest.fn().mockReturnValue([200, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]);
- commitApiCallSpy = jest.fn().mockReturnValue([200, fixtures.commit]);
+ .mockReturnValue([HTTP_STATUS_OK, fixtures.branches, { [X_TOTAL_HEADER]: '123' }]);
+ tagsApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_OK, fixtures.tags, { [X_TOTAL_HEADER]: '456' }]);
+ commitApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, fixtures.commit]);
requestSpies = { branchesApiCallSpy, tagsApiCallSpy, commitApiCallSpy };
mock
@@ -84,76 +87,63 @@ describe('Ref selector component', () => {
.reply((config) => commitApiCallSpy(config));
});
- afterEach(() => {
- wrapper.destroy();
- wrapper = null;
- });
-
//
// Finders
//
- const findButtonContent = () => wrapper.find('button');
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+
+ const findButtonToggle = () => wrapper.findByTestId('base-dropdown-toggle');
- const findNoResults = () => wrapper.find('[data-testid="no-results"]');
+ const findNoResults = () => wrapper.findByTestId('listbox-no-results-text');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const findSearchBox = () => wrapper.findComponent(GlSearchBoxByType);
+ const findListBoxSection = (section) => {
+ const foundSections = wrapper
+ .findAll('[role="group"]')
+ .filter((ul) => ul.text().includes(section));
+ return foundSections.length > 0 ? foundSections.at(0) : foundSections;
+ };
+
+ const findErrorListWrapper = () => wrapper.findByTestId('red-selector-error-list');
- const findBranchesSection = () => wrapper.find('[data-testid="branches-section"]');
- const findBranchDropdownItems = () => findBranchesSection().findAllComponents(GlDropdownItem);
- const findFirstBranchDropdownItem = () => findBranchDropdownItems().at(0);
+ const findBranchesSection = () => findListBoxSection('Branches');
+ const findBranchDropdownItems = () => wrapper.findAllComponents(GlListboxItem);
- const findTagsSection = () => wrapper.find('[data-testid="tags-section"]');
- const findTagDropdownItems = () => findTagsSection().findAllComponents(GlDropdownItem);
- const findFirstTagDropdownItem = () => findTagDropdownItems().at(0);
+ const findTagsSection = () => findListBoxSection('Tags');
- const findCommitsSection = () => wrapper.find('[data-testid="commits-section"]');
- const findCommitDropdownItems = () => findCommitsSection().findAllComponents(GlDropdownItem);
- const findFirstCommitDropdownItem = () => findCommitDropdownItems().at(0);
+ const findCommitsSection = () => findListBoxSection('Commits');
- const findHiddenInputField = () => wrapper.find('[data-testid="selected-ref-form-field"]');
+ const findHiddenInputField = () => wrapper.findByTestId('selected-ref-form-field');
//
// Expecters
//
- const branchesSectionContainsErrorMessage = () => {
- const branchesSection = findBranchesSection();
+ const sectionContainsErrorMessage = (message) => {
+ const errorSection = findErrorListWrapper();
- return branchesSection.text().includes(DEFAULT_I18N.branchesErrorMessage);
- };
-
- const tagsSectionContainsErrorMessage = () => {
- const tagsSection = findTagsSection();
-
- return tagsSection.text().includes(DEFAULT_I18N.tagsErrorMessage);
- };
-
- const commitsSectionContainsErrorMessage = () => {
- const commitsSection = findCommitsSection();
-
- return commitsSection.text().includes(DEFAULT_I18N.commitsErrorMessage);
+ return errorSection ? errorSection.text().includes(message) : false;
};
//
// Convenience methods
//
const updateQuery = (newQuery) => {
- findSearchBox().vm.$emit('input', newQuery);
+ findListbox().vm.$emit('search', newQuery);
};
const selectFirstBranch = async () => {
- findFirstBranchDropdownItem().vm.$emit('click');
+ findListbox().vm.$emit('select', fixtures.branches[0].name);
await nextTick();
};
const selectFirstTag = async () => {
- findFirstTagDropdownItem().vm.$emit('click');
+ findListbox().vm.$emit('select', fixtures.tags[0].name);
await nextTick();
};
const selectFirstCommit = async () => {
- findFirstCommitDropdownItem().vm.$emit('click');
+ findListbox().vm.$emit('select', fixtures.commit.id);
await nextTick();
};
@@ -188,7 +178,7 @@ describe('Ref selector component', () => {
});
describe('when name property is provided', () => {
- it('renders an forrm input hidden field', () => {
+ it('renders an form input hidden field', () => {
const name = 'default_tag';
createComponent({ propsData: { name } });
@@ -198,7 +188,7 @@ describe('Ref selector component', () => {
});
describe('when name property is not provided', () => {
- it('renders an forrm input hidden field', () => {
+ it('renders an form input hidden field', () => {
createComponent();
expect(findHiddenInputField().exists()).toBe(false);
@@ -217,7 +207,7 @@ describe('Ref selector component', () => {
});
it('adds the provided ID to the GlDropdown instance', () => {
- expect(wrapper.findComponent(GlDropdown).attributes().id).toBe(id);
+ expect(findListbox().attributes().id).toBe(id);
});
});
@@ -231,7 +221,7 @@ describe('Ref selector component', () => {
});
it('renders the pre-selected ref name', () => {
- expect(findButtonContent().text()).toBe(preselectedRef);
+ expect(findButtonToggle().text()).toBe(preselectedRef);
});
it('binds hidden input field to the pre-selected ref', () => {
@@ -252,7 +242,7 @@ describe('Ref selector component', () => {
wrapper.setProps({ value: updatedRef });
await nextTick();
- expect(findButtonContent().text()).toBe(updatedRef);
+ expect(findButtonToggle().text()).toBe(updatedRef);
});
});
@@ -289,28 +279,13 @@ describe('Ref selector component', () => {
});
});
- describe('when the Enter is pressed', () => {
- beforeEach(() => {
- createComponent();
-
- return waitForRequests({ andClearMocks: true });
- });
-
- it('requeries the endpoints when Enter is pressed', () => {
- findSearchBox().vm.$emit('keydown', new KeyboardEvent({ key: ENTER_KEY }));
-
- return waitForRequests().then(() => {
- expect(branchesApiCallSpy).toHaveBeenCalledTimes(1);
- expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
- });
- });
- });
-
describe('when no results are found', () => {
beforeEach(() => {
- branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
- tagsApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
- commitApiCallSpy = jest.fn().mockReturnValue([404]);
+ branchesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
+ tagsApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
+ commitApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_NOT_FOUND]);
createComponent();
@@ -348,27 +323,10 @@ describe('Ref selector component', () => {
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', () => {
- expect(
- findBranchesSection().find('[data-testid="section-header"]').text(),
- ).toMatchInterpolatedText('Branches 123');
});
it("does not render an error message in the branches section's body", () => {
- expect(branchesSectionContainsErrorMessage()).toBe(false);
- });
-
- it('renders each non-default branch as a selectable item', () => {
- const dropdownItems = findBranchDropdownItems();
-
- fixtures.branches.forEach((b, i) => {
- if (!b.default) {
- expect(dropdownItems.at(i).text()).toBe(b.name);
- }
- });
+ expect(findErrorListWrapper().exists()).toBe(false);
});
it('renders the default branch as a selectable item with a "default" badge', () => {
@@ -385,7 +343,9 @@ describe('Ref selector component', () => {
describe('when the branches search returns no results', () => {
beforeEach(() => {
- branchesApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+ branchesApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@@ -399,7 +359,7 @@ describe('Ref selector component', () => {
describe('when the branches search returns an error', () => {
beforeEach(() => {
- branchesApiCallSpy = jest.fn().mockReturnValue([500]);
+ branchesApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
createComponent();
@@ -407,11 +367,11 @@ describe('Ref selector component', () => {
});
it('renders the branches section in the dropdown', () => {
- expect(findBranchesSection().exists()).toBe(true);
+ expect(findBranchesSection().exists()).toBe(false);
});
it("renders an error message in the branches section's body", () => {
- expect(branchesSectionContainsErrorMessage()).toBe(true);
+ expect(sectionContainsErrorMessage(DEFAULT_I18N.branchesErrorMessage)).toBe(true);
});
});
});
@@ -426,31 +386,24 @@ describe('Ref selector component', () => {
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', () => {
- expect(
- findTagsSection().find('[data-testid="section-header"]').text(),
- ).toMatchInterpolatedText('Tags 456');
+ expect(findTagsSection().find('[role="presentation"]').text()).toMatchInterpolatedText(
+ `Tags ${fixtures.tags.length}`,
+ );
});
it("does not render an error message in the tags section's body", () => {
- expect(tagsSectionContainsErrorMessage()).toBe(false);
- });
-
- it('renders each tag as a selectable item', () => {
- const dropdownItems = findTagDropdownItems();
-
- fixtures.tags.forEach((t, i) => {
- expect(dropdownItems.at(i).text()).toBe(t.name);
- });
+ expect(findErrorListWrapper().exists()).toBe(false);
});
});
describe('when the tags search returns no results', () => {
beforeEach(() => {
- tagsApiCallSpy = jest.fn().mockReturnValue([200, [], { [X_TOTAL_HEADER]: '0' }]);
+ tagsApiCallSpy = jest
+ .fn()
+ .mockReturnValue([HTTP_STATUS_OK, [], { [X_TOTAL_HEADER]: '0' }]);
createComponent();
@@ -464,7 +417,7 @@ describe('Ref selector component', () => {
describe('when the tags search returns an error', () => {
beforeEach(() => {
- tagsApiCallSpy = jest.fn().mockReturnValue([500]);
+ tagsApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
createComponent();
@@ -472,11 +425,11 @@ describe('Ref selector component', () => {
});
it('renders the tags section in the dropdown', () => {
- expect(findTagsSection().exists()).toBe(true);
+ expect(findTagsSection().exists()).toBe(false);
});
it("renders an error message in the tags section's body", () => {
- expect(tagsSectionContainsErrorMessage()).toBe(true);
+ expect(sectionContainsErrorMessage(DEFAULT_I18N.tagsErrorMessage)).toBe(true);
});
});
});
@@ -496,25 +449,19 @@ describe('Ref selector component', () => {
});
it('renders the "Commits" heading with a total number indicator', () => {
- expect(
- findCommitsSection().find('[data-testid="section-header"]').text(),
- ).toMatchInterpolatedText('Commits 1');
- });
-
- it("does not render an error message in the comits section's body", () => {
- expect(commitsSectionContainsErrorMessage()).toBe(false);
+ expect(findCommitsSection().find('[role="presentation"]').text()).toMatchInterpolatedText(
+ `Commits 1`,
+ );
});
- it('renders each commit as a selectable item with the short SHA and commit title', () => {
- const dropdownItems = findCommitDropdownItems();
-
- expect(dropdownItems.at(0).text()).toBe(`${commit.short_id} ${commit.title}`);
+ it("does not render an error message in the commits section's body", () => {
+ expect(findErrorListWrapper().exists()).toBe(false);
});
});
describe('when the commit search returns no results (i.e. a 404)', () => {
beforeEach(() => {
- commitApiCallSpy = jest.fn().mockReturnValue([404]);
+ commitApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_NOT_FOUND]);
createComponent();
@@ -530,7 +477,7 @@ describe('Ref selector component', () => {
describe('when the commit search returns an error (other than a 404)', () => {
beforeEach(() => {
- commitApiCallSpy = jest.fn().mockReturnValue([500]);
+ commitApiCallSpy = jest.fn().mockReturnValue([HTTP_STATUS_INTERNAL_SERVER_ERROR]);
createComponent();
@@ -540,11 +487,11 @@ describe('Ref selector component', () => {
});
it('renders the commits section in the dropdown', () => {
- expect(findCommitsSection().exists()).toBe(true);
+ expect(findCommitsSection().exists()).toBe(false);
});
it("renders an error message in the commits section's body", () => {
- expect(commitsSectionContainsErrorMessage()).toBe(true);
+ expect(sectionContainsErrorMessage(DEFAULT_I18N.commitsErrorMessage)).toBe(true);
});
});
});
@@ -558,26 +505,13 @@ describe('Ref selector component', () => {
return waitForRequests();
});
- it('renders a checkmark by the selected item', async () => {
- expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).toHaveClass(
- 'gl-visibility-hidden',
- );
-
- await selectFirstBranch();
-
- expect(findFirstBranchDropdownItem().findComponent(GlIcon).element).not.toHaveClass(
- 'gl-visibility-hidden',
- );
- });
-
- describe('when a branch is seleceted', () => {
+ describe('when a branch is selected', () => {
it("displays the branch name in the dropdown's button", async () => {
- expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
+ expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected);
await selectFirstBranch();
- await nextTick();
- expect(findButtonContent().text()).toBe(fixtures.branches[0].name);
+ expect(findButtonToggle().text()).toBe(fixtures.branches[0].name);
});
it("updates the v-model binding with the branch's name", async () => {
@@ -591,12 +525,11 @@ describe('Ref selector component', () => {
describe('when a tag is seleceted', () => {
it("displays the tag name in the dropdown's button", async () => {
- expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
+ expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected);
await selectFirstTag();
- await nextTick();
- expect(findButtonContent().text()).toBe(fixtures.tags[0].name);
+ expect(findButtonToggle().text()).toBe(fixtures.tags[0].name);
});
it("updates the v-model binding with the tag's name", async () => {
@@ -610,12 +543,11 @@ describe('Ref selector component', () => {
describe('when a commit is selected', () => {
it("displays the full SHA in the dropdown's button", async () => {
- expect(findButtonContent().text()).toBe(DEFAULT_I18N.noRefSelected);
+ expect(findButtonToggle().text()).toBe(DEFAULT_I18N.noRefSelected);
await selectFirstCommit();
- await nextTick();
- expect(findButtonContent().text()).toBe(fixtures.commit.id);
+ expect(findButtonToggle().text()).toBe(fixtures.commit.id);
});
it("updates the v-model binding with the commit's full SHA", async () => {
@@ -675,21 +607,6 @@ describe('Ref selector component', () => {
expect(tagsApiCallSpy).toHaveBeenCalledTimes(1);
});
- it('if a ref type becomes disabled, its section is hidden, even if it had some results in store', async () => {
- createComponent({ propsData: { enabledRefTypes: [REF_TYPE_BRANCHES, REF_TYPE_COMMITS] } });
- updateQuery('abcd1234');
- await waitForRequests();
-
- expect(findBranchesSection().exists()).toBe(true);
- expect(findCommitsSection().exists()).toBe(true);
-
- wrapper.setProps({ enabledRefTypes: [REF_TYPE_COMMITS] });
- await waitForRequests();
-
- expect(findBranchesSection().exists()).toBe(false);
- expect(findCommitsSection().exists()).toBe(true);
- });
-
it.each`
enabledRefType | findVisibleSection | findHiddenSections
${REF_TYPE_BRANCHES} | ${findBranchesSection} | ${[findTagsSection, findCommitsSection]}
@@ -713,8 +630,7 @@ describe('Ref selector component', () => {
describe('validation state', () => {
const invalidClass = 'gl-inset-border-1-red-500!';
- const isInvalidClassApplied = () =>
- wrapper.findComponent(GlDropdown).props('toggleClass')[invalidClass];
+ const isInvalidClassApplied = () => findListbox().props('toggleClass')[0][invalidClass];
describe('valid state', () => {
describe('when the state prop is not provided', () => {
diff --git a/spec/frontend/ref/format_refs_spec.js b/spec/frontend/ref/format_refs_spec.js
new file mode 100644
index 00000000000..6dd49574721
--- /dev/null
+++ b/spec/frontend/ref/format_refs_spec.js
@@ -0,0 +1,38 @@
+import { formatListBoxItems, formatErrors } from '~/ref/format_refs';
+import { DEFAULT_I18N } from '~/ref/constants';
+import {
+ MOCK_BRANCHES,
+ MOCK_COMMITS,
+ MOCK_ERROR,
+ MOCK_TAGS,
+ FORMATTED_BRANCHES,
+ FORMATTED_TAGS,
+ FORMATTED_COMMITS,
+} from './mock_data';
+
+describe('formatListBoxItems', () => {
+ it.each`
+ branches | tags | commits | expectedResult
+ ${MOCK_BRANCHES} | ${MOCK_TAGS} | ${MOCK_COMMITS} | ${[FORMATTED_BRANCHES, FORMATTED_TAGS, FORMATTED_COMMITS]}
+ ${MOCK_BRANCHES} | ${[]} | ${MOCK_COMMITS} | ${[FORMATTED_BRANCHES, FORMATTED_COMMITS]}
+ ${[]} | ${[]} | ${MOCK_COMMITS} | ${[FORMATTED_COMMITS]}
+ ${undefined} | ${undefined} | ${MOCK_COMMITS} | ${[FORMATTED_COMMITS]}
+ ${MOCK_BRANCHES} | ${undefined} | ${null} | ${[FORMATTED_BRANCHES]}
+ `('should correctly format listbox items', ({ branches, tags, commits, expectedResult }) => {
+ expect(formatListBoxItems(branches, tags, commits)).toEqual(expectedResult);
+ });
+});
+
+describe('formatErrors', () => {
+ const { branchesErrorMessage, tagsErrorMessage, commitsErrorMessage } = DEFAULT_I18N;
+ it.each`
+ branches | tags | commits | expectedResult
+ ${MOCK_ERROR} | ${MOCK_ERROR} | ${MOCK_ERROR} | ${[branchesErrorMessage, tagsErrorMessage, commitsErrorMessage]}
+ ${MOCK_ERROR} | ${[]} | ${MOCK_ERROR} | ${[branchesErrorMessage, commitsErrorMessage]}
+ ${[]} | ${[]} | ${MOCK_ERROR} | ${[commitsErrorMessage]}
+ ${undefined} | ${undefined} | ${MOCK_ERROR} | ${[commitsErrorMessage]}
+ ${MOCK_ERROR} | ${undefined} | ${null} | ${[branchesErrorMessage]}
+ `('should correctly format listbox errors', ({ branches, tags, commits, expectedResult }) => {
+ expect(formatErrors(branches, tags, commits)).toEqual(expectedResult);
+ });
+});
diff --git a/spec/frontend/ref/mock_data.js b/spec/frontend/ref/mock_data.js
new file mode 100644
index 00000000000..c02d4da7aed
--- /dev/null
+++ b/spec/frontend/ref/mock_data.js
@@ -0,0 +1,87 @@
+export const MOCK_BRANCHES = [
+ {
+ default: true,
+ name: 'main',
+ value: undefined,
+ },
+ {
+ default: false,
+ name: 'test1',
+ value: undefined,
+ },
+ {
+ default: false,
+ name: 'test2',
+ value: undefined,
+ },
+];
+
+export const MOCK_TAGS = [
+ {
+ name: 'test_tag',
+ value: undefined,
+ },
+ {
+ name: 'test_tag2',
+ value: undefined,
+ },
+];
+
+export const MOCK_COMMITS = [
+ {
+ name: 'test_commit',
+ value: undefined,
+ },
+];
+
+export const FORMATTED_BRANCHES = {
+ text: 'Branches',
+ options: [
+ {
+ default: true,
+ text: 'main',
+ value: 'main',
+ },
+ {
+ default: false,
+ text: 'test1',
+ value: 'test1',
+ },
+ {
+ default: false,
+ text: 'test2',
+ value: 'test2',
+ },
+ ],
+};
+
+export const FORMATTED_TAGS = {
+ text: 'Tags',
+ options: [
+ {
+ text: 'test_tag',
+ value: 'test_tag',
+ default: undefined,
+ },
+ {
+ text: 'test_tag2',
+ value: 'test_tag2',
+ default: undefined,
+ },
+ ],
+};
+
+export const FORMATTED_COMMITS = {
+ text: 'Commits',
+ options: [
+ {
+ text: 'test_commit',
+ value: 'test_commit',
+ default: undefined,
+ },
+ ],
+};
+
+export const MOCK_ERROR = {
+ error: new Error('test_error'),
+};
diff --git a/spec/frontend/related_issues/components/related_issuable_input_spec.js b/spec/frontend/related_issues/components/related_issuable_input_spec.js
index f6a13856042..f7333bf6893 100644
--- a/spec/frontend/related_issues/components/related_issuable_input_spec.js
+++ b/spec/frontend/related_issues/components/related_issuable_input_spec.js
@@ -1,8 +1,9 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { TEST_HOST } from 'helpers/test_constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import RelatedIssuableInput from '~/related_issues/components/related_issuable_input.vue';
-import { issuableTypesMap, PathIdSeparator } from '~/related_issues/constants';
+import { PathIdSeparator } from '~/related_issues/constants';
jest.mock('ee_else_ce/gfm_auto_complete', () => {
return function gfmAutoComplete() {
@@ -21,7 +22,7 @@ describe('RelatedIssuableInput', () => {
inputValue: '',
references: [],
pathIdSeparator: PathIdSeparator.Issue,
- issuableType: issuableTypesMap.issue,
+ issuableType: TYPE_ISSUE,
autoCompleteSources: {
issues: `${TEST_HOST}/h5bp/html5-boilerplate/-/autocomplete_sources/issues`,
},
diff --git a/spec/frontend/releases/components/app_edit_new_spec.js b/spec/frontend/releases/components/app_edit_new_spec.js
index 649d8eef6ec..bd61e4537f9 100644
--- a/spec/frontend/releases/components/app_edit_new_spec.js
+++ b/spec/frontend/releases/components/app_edit_new_spec.js
@@ -5,11 +5,13 @@ import Vuex from 'vuex';
import { nextTick } from 'vue';
import { GlDatepicker, GlFormCheckbox } from '@gitlab/ui';
import originalOneReleaseForEditingQueryResponse from 'test_fixtures/graphql/releases/graphql/queries/one_release_for_editing.query.graphql.json';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { convertOneReleaseGraphQLResponse } from '~/releases/util';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import setWindowLocation from 'helpers/set_window_location_helper';
import { TEST_HOST } from 'helpers/test_constants';
import ReleaseEditNewApp from '~/releases/components/app_edit_new.vue';
+import { putCreateReleaseNotification } from '~/releases/release_notification_service';
import AssetLinksForm from '~/releases/components/asset_links_form.vue';
import ConfirmDeleteModal from '~/releases/components/confirm_delete_modal.vue';
import { BACK_URL_PARAM } from '~/releases/constants';
@@ -19,6 +21,8 @@ const originalRelease = originalOneReleaseForEditingQueryResponse.data.project.r
const originalMilestones = originalRelease.milestones;
const releasesPagePath = 'path/to/releases/page';
const upcomingReleaseDocsPath = 'path/to/upcoming/release/docs';
+const projectPath = 'project/path';
+jest.mock('~/releases/release_notification_service');
describe('Release edit/new component', () => {
let wrapper;
@@ -32,6 +36,7 @@ describe('Release edit/new component', () => {
state = {
release,
isExistingRelease: true,
+ projectPath,
markdownDocsPath: 'path/to/markdown/docs',
releasesPagePath,
projectId: '8',
@@ -91,7 +96,7 @@ describe('Release edit/new component', () => {
mock = new MockAdapter(axios);
gon.api_version = 'v4';
- mock.onGet('/api/v4/projects/8/milestones').reply(200, originalMilestones);
+ mock.onGet('/api/v4/projects/8/milestones').reply(HTTP_STATUS_OK, originalMilestones);
release = convertOneReleaseGraphQLResponse(originalOneReleaseForEditingQueryResponse).data;
});
@@ -125,7 +130,7 @@ describe('Release edit/new component', () => {
it('renders the description text at the top of the page', () => {
expect(wrapper.find('.js-subtitle-text').text()).toBe(
- 'Releases are based on Git tags. We recommend tags that use semantic versioning, for example v1.0.0, v2.1.0-pre.',
+ 'Releases are based on Git tags. We recommend tags that use semantic versioning, for example 1.0.0, 2.1.0-pre.',
);
});
@@ -163,6 +168,13 @@ describe('Release edit/new component', () => {
expect(actions.saveRelease).toHaveBeenCalledTimes(1);
});
+
+ it('sets release created notification when the form is submitted', () => {
+ findForm().trigger('submit');
+ const releaseName = originalOneReleaseForEditingQueryResponse.data.project.release.name;
+ expect(putCreateReleaseNotification).toHaveBeenCalledTimes(1);
+ expect(putCreateReleaseNotification).toHaveBeenCalledWith(projectPath, releaseName);
+ });
});
describe(`when the URL does not contain a "${BACK_URL_PARAM}" parameter`, () => {
diff --git a/spec/frontend/releases/components/app_index_spec.js b/spec/frontend/releases/components/app_index_spec.js
index 48589a54ec4..ef3bd5ca873 100644
--- a/spec/frontend/releases/components/app_index_spec.js
+++ b/spec/frontend/releases/components/app_index_spec.js
@@ -92,10 +92,6 @@ describe('app_index.vue', () => {
queryMock = jest.fn();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
// Finders
const findLoadingIndicator = () => wrapper.findComponent(ReleaseSkeletonLoader);
const findEmptyState = () => wrapper.findComponent(ReleasesEmptyState);
@@ -179,12 +175,14 @@ describe('app_index.vue', () => {
expect(findPagination().exists()).toBe(pagination);
});
- it('does render the "New release" button', () => {
- expect(findNewReleaseButton().exists()).toBe(true);
+ it('does render the "New release" button only for non-empty state', () => {
+ const shouldRenderNewReleaseButton = !emptyState;
+ expect(findNewReleaseButton().exists()).toBe(shouldRenderNewReleaseButton);
});
- it('does render the sort controls', () => {
- expect(findSort().exists()).toBe(true);
+ it('does render the sort controls only for non-empty state', () => {
+ const shouldRenderControls = !emptyState;
+ expect(findSort().exists()).toBe(shouldRenderControls);
});
},
);
diff --git a/spec/frontend/releases/components/app_show_spec.js b/spec/frontend/releases/components/app_show_spec.js
index c5cb8589ee8..efe72e8000a 100644
--- a/spec/frontend/releases/components/app_show_spec.js
+++ b/spec/frontend/releases/components/app_show_spec.js
@@ -5,12 +5,14 @@ import oneReleaseQueryResponse from 'test_fixtures/graphql/releases/graphql/quer
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
+import { popCreateReleaseNotification } from '~/releases/release_notification_service';
import ReleaseShowApp from '~/releases/components/app_show.vue';
import ReleaseBlock from '~/releases/components/release_block.vue';
import ReleaseSkeletonLoader from '~/releases/components/release_skeleton_loader.vue';
import oneReleaseQuery from '~/releases/graphql/queries/one_release.query.graphql';
jest.mock('~/flash');
+jest.mock('~/releases/release_notification_service');
Vue.use(VueApollo);
@@ -88,6 +90,11 @@ describe('Release show component', () => {
createComponent({ apolloProvider });
});
+ it('shows info notification on mount', () => {
+ expect(popCreateReleaseNotification).toHaveBeenCalledTimes(1);
+ expect(popCreateReleaseNotification).toHaveBeenCalledWith(MOCK_FULL_PATH);
+ });
+
it('builds a GraphQL with the expected variables', () => {
expect(queryHandler).toHaveBeenCalledTimes(1);
expect(queryHandler).toHaveBeenCalledWith({
diff --git a/spec/frontend/releases/components/evidence_block_spec.js b/spec/frontend/releases/components/evidence_block_spec.js
index 6f935215dd7..69443cb7a11 100644
--- a/spec/frontend/releases/components/evidence_block_spec.js
+++ b/spec/frontend/releases/components/evidence_block_spec.js
@@ -40,13 +40,11 @@ describe('Evidence Block', () => {
});
it('renders the correct hover text for the download', () => {
- expect(wrapper.findComponent(GlLink).attributes('title')).toBe('Download evidence JSON');
+ expect(wrapper.findComponent(GlLink).attributes('title')).toBe('Open evidence JSON in new tab');
});
- it('renders the correct file link for download', () => {
- expect(wrapper.findComponent(GlLink).attributes().download).toMatch(
- /v1\.1-evidences-[0-9]+\.json/,
- );
+ it('renders a link that opens in a new tab', () => {
+ expect(wrapper.findComponent(GlLink).attributes().target).toBe('_blank');
});
describe('sha text', () => {
diff --git a/spec/frontend/releases/components/release_block_assets_spec.js b/spec/frontend/releases/components/release_block_assets_spec.js
index 4f94e4dfd55..6d53bf5a49e 100644
--- a/spec/frontend/releases/components/release_block_assets_spec.js
+++ b/spec/frontend/releases/components/release_block_assets_spec.js
@@ -123,42 +123,14 @@ describe('Release block assets', () => {
});
});
- describe('external vs internal links', () => {
+ describe('links', () => {
const containsExternalSourceIndicator = () =>
wrapper.find('[data-testid="external-link-indicator"]').exists();
- describe('when a link is external', () => {
- beforeEach(() => {
- defaultProps.assets.sources = [];
- defaultProps.assets.links = [
- {
- ...defaultProps.assets.links[0],
- external: true,
- },
- ];
- createComponent(defaultProps);
- });
-
- it('renders the link with an "external source" indicator', () => {
- expect(containsExternalSourceIndicator()).toBe(true);
- });
- });
+ beforeEach(() => createComponent(defaultProps));
- describe('when a link is internal', () => {
- beforeEach(() => {
- defaultProps.assets.sources = [];
- defaultProps.assets.links = [
- {
- ...defaultProps.assets.links[0],
- external: false,
- },
- ];
- createComponent(defaultProps);
- });
-
- it('renders the link without the "external source" indicator', () => {
- expect(containsExternalSourceIndicator()).toBe(false);
- });
+ it('renders with an external source indicator (except for sections with no title)', () => {
+ expect(containsExternalSourceIndicator()).toBe(true);
});
});
});
diff --git a/spec/frontend/releases/components/releases_empty_state_spec.js b/spec/frontend/releases/components/releases_empty_state_spec.js
index 495e6d863f7..f0db7d16bd7 100644
--- a/spec/frontend/releases/components/releases_empty_state_spec.js
+++ b/spec/frontend/releases/components/releases_empty_state_spec.js
@@ -4,6 +4,7 @@ import ReleasesEmptyState from '~/releases/components/releases_empty_state.vue';
describe('releases_empty_state.vue', () => {
const documentationPath = 'path/to/releases/documentation';
+ const newReleasePath = 'path/to/releases/new-release';
const illustrationPath = 'path/to/releases/empty/state/illustration';
let wrapper;
@@ -12,6 +13,7 @@ describe('releases_empty_state.vue', () => {
wrapper = shallowMountExtended(ReleasesEmptyState, {
provide: {
documentationPath,
+ newReleasePath,
illustrationPath,
},
});
@@ -21,36 +23,17 @@ describe('releases_empty_state.vue', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('renders a GlEmptyState and provides it with the correct props', () => {
const emptyStateProps = wrapper.findComponent(GlEmptyState).props();
- expect(emptyStateProps).toEqual(
- expect.objectContaining({
- title: ReleasesEmptyState.i18n.emptyStateTitle,
- svgPath: illustrationPath,
- }),
- );
- });
-
- it('renders the empty state text', () => {
- expect(wrapper.findByText(ReleasesEmptyState.i18n.emptyStateText).exists()).toBe(true);
- });
-
- it('renders a link to the documentation', () => {
- const documentationLink = wrapper.findByText(ReleasesEmptyState.i18n.moreInformation);
-
- expect(documentationLink.exists()).toBe(true);
-
- expect(documentationLink.attributes()).toEqual(
- expect.objectContaining({
- 'aria-label': ReleasesEmptyState.i18n.releasesDocumentation,
- href: documentationPath,
- target: '_blank',
- }),
- );
+ expect(emptyStateProps).toMatchObject({
+ title: ReleasesEmptyState.i18n.emptyStateTitle,
+ svgPath: illustrationPath,
+ description: ReleasesEmptyState.i18n.emptyStateText,
+ primaryButtonLink: newReleasePath,
+ primaryButtonText: ReleasesEmptyState.i18n.newRelease,
+ secondaryButtonLink: documentationPath,
+ secondaryButtonText: ReleasesEmptyState.i18n.releasesDocumentation,
+ });
});
});
diff --git a/spec/frontend/releases/release_notification_service_spec.js b/spec/frontend/releases/release_notification_service_spec.js
new file mode 100644
index 00000000000..2344d4b929a
--- /dev/null
+++ b/spec/frontend/releases/release_notification_service_spec.js
@@ -0,0 +1,57 @@
+import {
+ popCreateReleaseNotification,
+ putCreateReleaseNotification,
+} from '~/releases/release_notification_service';
+import { createAlert, VARIANT_SUCCESS } from '~/flash';
+
+jest.mock('~/flash');
+
+describe('~/releases/release_notification_service', () => {
+ const projectPath = 'test-project-path';
+ const releaseName = 'test-release-name';
+
+ const storageKey = `createRelease:${projectPath}`;
+
+ describe('prepareCreateReleaseFlash', () => {
+ it('should set the session storage with project path key and release name value', () => {
+ putCreateReleaseNotification(projectPath, releaseName);
+
+ const item = window.sessionStorage.getItem(storageKey);
+
+ expect(item).toBe(releaseName);
+ });
+ });
+
+ describe('showNotificationsIfPresent', () => {
+ describe('if notification is prepared', () => {
+ beforeEach(() => {
+ window.sessionStorage.setItem(storageKey, releaseName);
+ popCreateReleaseNotification(projectPath);
+ });
+
+ it('should remove storage key', () => {
+ const item = window.sessionStorage.getItem(storageKey);
+
+ expect(item).toBe(null);
+ });
+
+ it('should create a flash message', () => {
+ expect(createAlert).toHaveBeenCalledTimes(1);
+ expect(createAlert).toHaveBeenCalledWith({
+ message: `Release ${releaseName} has been successfully created.`,
+ variant: VARIANT_SUCCESS,
+ });
+ });
+ });
+
+ describe('if notification is not prepared', () => {
+ beforeEach(() => {
+ popCreateReleaseNotification(projectPath);
+ });
+
+ it('should not create a flash message', () => {
+ expect(createAlert).toHaveBeenCalledTimes(0);
+ });
+ });
+ });
+});
diff --git a/spec/frontend/releases/stores/modules/detail/actions_spec.js b/spec/frontend/releases/stores/modules/detail/actions_spec.js
index eeee6747349..ca3b2d5f734 100644
--- a/spec/frontend/releases/stores/modules/detail/actions_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/actions_spec.js
@@ -23,6 +23,8 @@ jest.mock('~/api/tags_api');
jest.mock('~/flash');
+jest.mock('~/releases/release_notification_service');
+
jest.mock('~/lib/utils/url_utility', () => ({
redirectTo: jest.fn(),
joinPaths: jest.requireActual('~/lib/utils/url_utility').joinPaths,
@@ -41,9 +43,12 @@ describe('Release edit/new actions', () => {
let releaseResponse;
let error;
+ const projectPath = 'test/project-path';
+
const setupState = (updates = {}) => {
state = {
...createState({
+ projectPath,
projectId: '18',
isExistingRelease: true,
tagName: releaseResponse.tag_name,
diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
index 944769d22cc..bf40af9a897 100644
--- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
@@ -89,6 +89,15 @@ describe('Release edit/new mutations', () => {
expect(state.release.tagName).toBe(newTag);
});
+
+ it('nulls out existing release', () => {
+ state.release = release;
+ state.existingRelease = release;
+ const newTag = 'updated-tag-name';
+ mutations[types.UPDATE_RELEASE_TAG_NAME](state, newTag);
+
+ expect(state.existingRelease).toBe(null);
+ });
});
describe(`${types.UPDATE_RELEASE_TAG_MESSAGE}`, () => {
@@ -304,6 +313,17 @@ describe('Release edit/new mutations', () => {
expect(state.tagNotes).toBe('');
expect(state.isFetchingTagNotes).toBe(false);
});
+
+ it('nulls out existing release', () => {
+ state.existingRelease = release;
+ const message = 'there was an error';
+ state.isFetchingTagNotes = true;
+ state.tagNotes = 'tag notes';
+
+ mutations[types.RECEIVE_TAG_NOTES_ERROR](state, { message });
+
+ expect(state.existingRelease).toBe(null);
+ });
});
describe(`${types.UPDATE_INCLUDE_TAG_NOTES}`, () => {
it('sets whether or not to include the tag notes', () => {
diff --git a/spec/frontend/repository/components/blob_content_viewer_spec.js b/spec/frontend/repository/components/blob_content_viewer_spec.js
index 2e8860f67ef..03a8ee6ac5d 100644
--- a/spec/frontend/repository/components/blob_content_viewer_spec.js
+++ b/spec/frontend/repository/components/blob_content_viewer_spec.js
@@ -16,7 +16,7 @@ import ForkSuggestion from '~/repository/components/fork_suggestion.vue';
import { loadViewer } from '~/repository/components/blob_viewers';
import DownloadViewer from '~/repository/components/blob_viewers/download_viewer.vue';
import EmptyViewer from '~/repository/components/blob_viewers/empty_viewer.vue';
-import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
+import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_deprecated.vue';
import blobInfoQuery from 'shared_queries/repository/blob_info.query.graphql';
import projectInfoQuery from '~/repository/queries/project_info.query.graphql';
import userInfoQuery from '~/repository/queries/user_info.query.graphql';
diff --git a/spec/frontend/repository/components/fork_info_spec.js b/spec/frontend/repository/components/fork_info_spec.js
index c23d5ae5823..f327a8cfae7 100644
--- a/spec/frontend/repository/components/fork_info_spec.js
+++ b/spec/frontend/repository/components/fork_info_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import VueApollo from 'vue-apollo';
-import { GlSkeletonLoader, GlIcon, GlLink } from '@gitlab/ui';
+import { GlSkeletonLoader, GlIcon, GlLink, GlSprintf } 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';
@@ -16,13 +16,14 @@ describe('ForkInfo component', () => {
let wrapper;
let mockResolver;
const forkInfoError = new Error('Something went wrong');
+ const projectId = 'gid://gitlab/Project/1';
Vue.use(VueApollo);
const createCommitData = ({ ahead = 3, behind = 7 }) => {
return {
data: {
- project: { id: '1', forkDetails: { ahead, behind, __typename: 'ForkDetails' } },
+ project: { id: projectId, forkDetails: { ahead, behind, __typename: 'ForkDetails' } },
},
};
};
@@ -35,6 +36,7 @@ describe('ForkInfo component', () => {
wrapper = shallowMountExtended(ForkInfo, {
apolloProvider: createMockApollo([[forkDetailsQuery, mockResolver]]),
propsData: { ...propsForkInfo, ...props },
+ stubs: { GlSprintf },
});
return waitForPromises();
};
@@ -42,8 +44,10 @@ describe('ForkInfo component', () => {
const findLink = () => wrapper.findComponent(GlLink);
const findSkeleton = () => wrapper.findComponent(GlSkeletonLoader);
const findIcon = () => wrapper.findComponent(GlIcon);
- const findDivergenceMessage = () => wrapper.find('.gl-text-secondary');
+ const findDivergenceMessage = () => wrapper.findByTestId('divergence-message');
const findInaccessibleMessage = () => wrapper.findByTestId('inaccessible-project');
+ const findCompareLinks = () => findDivergenceMessage().findAllComponents(GlLink);
+
it('displays a skeleton while loading data', async () => {
createComponent();
expect(findSkeleton().exists()).toBe(true);
@@ -88,28 +92,54 @@ describe('ForkInfo component', () => {
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.');
- });
+ describe.each([
+ {
+ ahead: 7,
+ behind: 3,
+ message: '3 commits behind, 7 commits ahead of the upstream repository.',
+ firstLink: propsForkInfo.behindComparePath,
+ secondLink: propsForkInfo.aheadComparePath,
+ },
+ {
+ ahead: 7,
+ behind: 0,
+ message: '7 commits ahead of the upstream repository.',
+ firstLink: propsForkInfo.aheadComparePath,
+ secondLink: '',
+ },
+ {
+ ahead: 0,
+ behind: 3,
+ message: '3 commits behind the upstream repository.',
+ firstLink: propsForkInfo.behindComparePath,
+ secondLink: '',
+ },
+ ])(
+ 'renders correct divergence message for ahead: $ahead, behind: $behind divergence commits',
+ ({ ahead, behind, message, firstLink, secondLink }) => {
+ beforeEach(async () => {
+ await createComponent({}, { ahead, behind });
+ });
+
+ it('displays correct text', () => {
+ expect(findDivergenceMessage().text()).toBe(message);
+ });
+
+ it('adds correct links', () => {
+ const links = findCompareLinks();
+ expect(links.at(0).attributes('href')).toBe(firstLink);
+
+ if (secondLink) {
+ expect(links.at(1).attributes('href')).toBe(secondLink);
+ }
+ });
+ },
+ );
it('renders alert with error message when request fails', async () => {
await createComponent({}, {}, true);
diff --git a/spec/frontend/repository/components/last_commit_spec.js b/spec/frontend/repository/components/last_commit_spec.js
index 964b135bee3..7226e7baa36 100644
--- a/spec/frontend/repository/components/last_commit_spec.js
+++ b/spec/frontend/repository/components/last_commit_spec.js
@@ -20,7 +20,7 @@ const findUserAvatarLink = () => wrapper.findComponent(UserAvatarLink);
const findLastCommitLabel = () => wrapper.findByTestId('last-commit-id-label');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findCommitRowDescription = () => wrapper.find('.commit-row-description');
-const findStatusBox = () => wrapper.find('.gpg-status-box');
+const findStatusBox = () => wrapper.find('.signature-badge');
const findItemTitle = () => wrapper.find('.item-title');
const defaultPipelineEdges = [
@@ -206,7 +206,7 @@ describe('Repository last commit component', () => {
it('renders the signature HTML as returned by the backend', async () => {
createComponent({
signatureHtml: `<a
- class="btn gpg-status-box valid"
+ class="btn signature-badge"
data-content="signature-content"
data-html="true"
data-placement="top"
@@ -214,12 +214,12 @@ describe('Repository last commit component', () => {
data-toggle="popover"
role="button"
tabindex="0"
- >Verified</a>`,
+ ><span class="gl-badge badge badge-pill badge-success md">Verified</span></a>`,
});
await waitForPromises();
expect(findStatusBox().html()).toBe(
- `<a class="btn gpg-status-box valid" data-content="signature-content" data-html="true" data-placement="top" data-title="signature-title" data-toggle="popover" role="button" tabindex="0">Verified</a>`,
+ `<a class="btn signature-badge" data-content="signature-content" data-html="true" data-placement="top" data-title="signature-title" data-toggle="popover" role="button" tabindex="0"><span class="gl-badge badge badge-pill badge-success md">Verified</span></a>`,
);
});
diff --git a/spec/frontend/repository/components/preview/index_spec.js b/spec/frontend/repository/components/preview/index_spec.js
index e4eba65795e..d4c746b67d6 100644
--- a/spec/frontend/repository/components/preview/index_spec.js
+++ b/spec/frontend/repository/components/preview/index_spec.js
@@ -9,9 +9,14 @@ jest.mock('~/lib/utils/common_utils');
let vm;
let $apollo;
-function factory(blob) {
+function factory(blob, loading) {
$apollo = {
- query: jest.fn().mockReturnValue(Promise.resolve({})),
+ queries: {
+ readme: {
+ query: jest.fn().mockReturnValue(Promise.resolve({})),
+ loading,
+ },
+ },
};
vm = shallowMount(Preview, {
@@ -58,14 +63,13 @@ describe('Repository file preview component', () => {
});
it('renders loading icon', async () => {
- factory({
- webPath: 'http://test.com',
- name: 'README.md',
- });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- vm.setData({ loading: 1 });
+ factory(
+ {
+ webPath: 'http://test.com',
+ name: 'README.md',
+ },
+ true,
+ );
await nextTick();
expect(vm.findComponent(GlLoadingIcon).exists()).toBe(true);
diff --git a/spec/frontend/repository/log_tree_spec.js b/spec/frontend/repository/log_tree_spec.js
index c1309539b6d..a2e86c86add 100644
--- a/spec/frontend/repository/log_tree_spec.js
+++ b/spec/frontend/repository/log_tree_spec.js
@@ -1,6 +1,7 @@
import MockAdapter from 'axios-mock-adapter';
import { createMockClient } from 'helpers/mock_apollo_helper';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { resolveCommit, fetchLogsTree } from '~/repository/log_tree';
import commitsQuery from '~/repository/queries/commits.query.graphql';
import projectPathQuery from '~/repository/queries/project_path.query.graphql';
@@ -47,7 +48,7 @@ describe('fetchLogsTree', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(/(.*)/).reply(200, mockData, {});
+ mock.onGet(/(.*)/).reply(HTTP_STATUS_OK, mockData, {});
jest.spyOn(axios, 'get');
diff --git a/spec/frontend/repository/mixins/highlight_mixin_spec.js b/spec/frontend/repository/mixins/highlight_mixin_spec.js
new file mode 100644
index 00000000000..7c48fe440d2
--- /dev/null
+++ b/spec/frontend/repository/mixins/highlight_mixin_spec.js
@@ -0,0 +1,106 @@
+import { shallowMount } from '@vue/test-utils';
+import { splitIntoChunks } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
+import highlightMixin from '~/repository/mixins/highlight_mixin';
+import LineHighlighter from '~/blob/line_highlighter';
+import Tracking from '~/tracking';
+import { TEXT_FILE_TYPE } from '~/repository/constants';
+import {
+ EVENT_ACTION,
+ EVENT_LABEL_FALLBACK,
+ LINES_PER_CHUNK,
+} from '~/vue_shared/components/source_viewer/constants';
+
+const lineHighlighter = new LineHighlighter();
+jest.mock('~/blob/line_highlighter', () => jest.fn().mockReturnValue({ highlightHash: jest.fn() }));
+jest.mock('~/vue_shared/components/source_viewer/workers/highlight_utils', () => ({
+ splitIntoChunks: jest.fn().mockResolvedValue([]),
+}));
+
+const workerMock = { postMessage: jest.fn() };
+const onErrorMock = jest.fn();
+
+describe('HighlightMixin', () => {
+ let wrapper;
+ const hash = '#L50';
+ const contentArray = Array.from({ length: 140 }, () => 'newline'); // simulate 140 lines of code
+ const rawTextBlob = contentArray.join('\n');
+ const languageMock = 'javascript';
+
+ const createComponent = ({ fileType = TEXT_FILE_TYPE, language = languageMock } = {}) => {
+ const simpleViewer = { fileType };
+
+ const dummyComponent = {
+ mixins: [highlightMixin],
+ inject: { highlightWorker: { default: workerMock } },
+ template: '<div>{{chunks[0]?.highlightedContent}}</div>',
+ created() {
+ this.initHighlightWorker({ rawTextBlob, simpleViewer, language });
+ },
+ methods: { onError: onErrorMock },
+ };
+
+ wrapper = shallowMount(dummyComponent, { mocks: { $route: { hash } } });
+ };
+
+ beforeEach(() => createComponent());
+
+ afterEach(() => wrapper.destroy());
+
+ describe('initHighlightWorker', () => {
+ const firstSeventyLines = contentArray.slice(0, LINES_PER_CHUNK).join('\n');
+
+ it('does not instruct worker if file is not a text file', () => {
+ workerMock.postMessage.mockClear();
+ createComponent({ fileType: 'markdown' });
+
+ expect(workerMock.postMessage).not.toHaveBeenCalled();
+ });
+
+ it('tracks event if a language is not supported and does not instruct worker', () => {
+ const unsupportedLanguage = 'some_unsupported_language';
+ const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
+
+ jest.spyOn(Tracking, 'event');
+ workerMock.postMessage.mockClear();
+ createComponent({ language: unsupportedLanguage });
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ expect(onErrorMock).toHaveBeenCalled();
+ expect(workerMock.postMessage).not.toHaveBeenCalled();
+ });
+
+ it('generates a chunk for the first 70 lines of raw text', () => {
+ expect(splitIntoChunks).toHaveBeenCalledWith(languageMock, firstSeventyLines);
+ });
+
+ it('calls postMessage on the worker', () => {
+ expect(workerMock.postMessage.mock.calls.length).toBe(2);
+
+ // first call instructs worker to highlight the first 70 lines
+ expect(workerMock.postMessage.mock.calls[0][0]).toMatchObject({
+ content: firstSeventyLines,
+ language: languageMock,
+ });
+
+ // second call instructs worker to highlight all of the lines
+ expect(workerMock.postMessage.mock.calls[1][0]).toMatchObject({
+ content: rawTextBlob,
+ language: languageMock,
+ });
+ });
+ });
+
+ describe('worker message handling', () => {
+ const CHUNK_MOCK = { startingFrom: 0, totalLines: 70, highlightedContent: 'some content' };
+
+ beforeEach(() => workerMock.onmessage({ data: [CHUNK_MOCK] }));
+
+ it('updates the chunks data', () => {
+ expect(wrapper.text()).toBe(CHUNK_MOCK.highlightedContent);
+ });
+
+ it('highlights hash', () => {
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
+ });
+ });
+});
diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js
index d85434a9148..04ffe52bc3f 100644
--- a/spec/frontend/repository/mock_data.js
+++ b/spec/frontend/repository/mock_data.js
@@ -120,7 +120,9 @@ export const graphQLErrors = [
export const propsForkInfo = {
projectPath: 'nataliia/myGitLab',
- selectedRef: 'main',
+ selectedBranch: 'main',
sourceName: 'gitLab',
sourcePath: 'gitlab-org/gitlab',
+ aheadComparePath: '/nataliia/myGitLab/-/compare/main...ref?from_project_id=1',
+ behindComparePath: 'gitlab-org/gitlab/-/compare/ref...main?from_project_id=2',
};
diff --git a/spec/frontend/repository/utils/ref_switcher_utils_spec.js b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
index 4d0250fffbf..7f708f13eaa 100644
--- a/spec/frontend/repository/utils/ref_switcher_utils_spec.js
+++ b/spec/frontend/repository/utils/ref_switcher_utils_spec.js
@@ -18,12 +18,14 @@ describe('generateRefDestinationPath', () => {
${`${projectRootPath}/-/blob/${currentRef}/dir1/dir2/test.js#L123`} | ${`${projectRootPath}/-/blob/${selectedRef}/dir1/dir2/test.js#L123`}
`('generates the correct destination path for $currentPath', ({ currentPath, result }) => {
setWindowLocation(currentPath);
- expect(generateRefDestinationPath(projectRootPath, selectedRef)).toBe(result);
+ expect(generateRefDestinationPath(projectRootPath, currentRef, selectedRef)).toBe(result);
});
it('encodes the selected ref', () => {
const result = `${projectRootPath}/-/tree/${encodedRefWithSpecialCharMock}`;
- expect(generateRefDestinationPath(projectRootPath, refWithSpecialCharMock)).toBe(result);
+ expect(generateRefDestinationPath(projectRootPath, currentRef, refWithSpecialCharMock)).toBe(
+ result,
+ );
});
});
diff --git a/spec/frontend/right_sidebar_spec.js b/spec/frontend/right_sidebar_spec.js
index 3b220ba8351..f51d51ee182 100644
--- a/spec/frontend/right_sidebar_spec.js
+++ b/spec/frontend/right_sidebar_spec.js
@@ -69,6 +69,9 @@ describe('RightSidebar', () => {
});
it('should not hide collapsed icons', () => {
+ $toggle.click();
+ assertSidebarState('collapsed');
+
[].forEach.call(document.querySelectorAll('.sidebar-collapsed-icon'), (el) => {
expect(el.querySelector('.fa, svg').classList.contains('hidden')).toBe(false);
});
diff --git a/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap
new file mode 100644
index 00000000000..3abdfcdaf20
--- /dev/null
+++ b/spec/frontend/saved_replies/components/__snapshots__/list_item_spec.js.snap
@@ -0,0 +1,21 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Saved replies list item component renders list item 1`] = `
+<li
+ class="gl-mb-5"
+>
+ <div
+ class="gl-display-flex gl-align-items-center"
+ >
+ <strong>
+ test
+ </strong>
+ </div>
+
+ <div
+ class="gl-mt-3 gl-font-monospace"
+ >
+ /assign_reviewer
+ </div>
+</li>
+`;
diff --git a/spec/frontend/saved_replies/components/list_item_spec.js b/spec/frontend/saved_replies/components/list_item_spec.js
new file mode 100644
index 00000000000..cad1000473b
--- /dev/null
+++ b/spec/frontend/saved_replies/components/list_item_spec.js
@@ -0,0 +1,22 @@
+import { shallowMount } from '@vue/test-utils';
+import ListItem from '~/saved_replies/components/list_item.vue';
+
+let wrapper;
+
+function createComponent(propsData = {}) {
+ return shallowMount(ListItem, {
+ propsData,
+ });
+}
+
+describe('Saved replies list item component', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('renders list item', async () => {
+ wrapper = createComponent({ reply: { name: 'test', content: '/assign_reviewer' } });
+
+ expect(wrapper.element).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/saved_replies/components/list_spec.js b/spec/frontend/saved_replies/components/list_spec.js
new file mode 100644
index 00000000000..66e9ddfe148
--- /dev/null
+++ b/spec/frontend/saved_replies/components/list_spec.js
@@ -0,0 +1,68 @@
+import Vue from 'vue';
+import { mount } from '@vue/test-utils';
+import VueApollo from 'vue-apollo';
+import noSavedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies_empty.query.graphql.json';
+import savedRepliesResponse from 'test_fixtures/graphql/saved_replies/saved_replies.query.graphql.json';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import List from '~/saved_replies/components/list.vue';
+import ListItem from '~/saved_replies/components/list_item.vue';
+import savedRepliesQuery from '~/saved_replies/queries/saved_replies.query.graphql';
+
+let wrapper;
+
+function createMockApolloProvider(response) {
+ Vue.use(VueApollo);
+
+ const requestHandlers = [[savedRepliesQuery, jest.fn().mockResolvedValue(response)]];
+
+ return createMockApollo(requestHandlers);
+}
+
+function createComponent(options = {}) {
+ const { mockApollo } = options;
+
+ return mount(List, {
+ apolloProvider: mockApollo,
+ });
+}
+
+describe('Saved replies list component', () => {
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ it('does not render any list items when response is empty', async () => {
+ const mockApollo = createMockApolloProvider(noSavedRepliesResponse);
+ wrapper = createComponent({ mockApollo });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ListItem).length).toBe(0);
+ });
+
+ it('render saved replies count', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ wrapper = createComponent({ mockApollo });
+
+ await waitForPromises();
+
+ expect(wrapper.find('[data-testid="title"]').text()).toEqual('My saved replies (2)');
+ });
+
+ it('renders list of saved replies', async () => {
+ const mockApollo = createMockApolloProvider(savedRepliesResponse);
+ const savedReplies = savedRepliesResponse.data.currentUser.savedReplies.nodes;
+ wrapper = createComponent({ mockApollo });
+
+ await waitForPromises();
+
+ expect(wrapper.findAllComponents(ListItem).length).toBe(2);
+ expect(wrapper.findAllComponents(ListItem).at(0).props('reply')).toEqual(
+ expect.objectContaining(savedReplies[0]),
+ );
+ expect(wrapper.findAllComponents(ListItem).at(1).props('reply')).toEqual(
+ expect.objectContaining(savedReplies[1]),
+ );
+ });
+});
diff --git a/spec/frontend/search/mock_data.js b/spec/frontend/search/mock_data.js
index e02d3b0eab8..fb9c0a93907 100644
--- a/spec/frontend/search/mock_data.js
+++ b/spec/frontend/search/mock_data.js
@@ -192,3 +192,464 @@ export const MOCK_NAVIGATION_ACTION_MUTATION = {
type: types.RECEIVE_NAVIGATION_COUNT,
payload: { key: 'projects', count: '13' },
};
+
+export const MOCK_AGGREGATIONS = [
+ {
+ name: 'language',
+ buckets: [
+ { key: 'random-label-edumingos0', count: 1 },
+ { key: 'random-label-rbourgourd1', count: 2 },
+ { key: 'random-label-dfearnside2', count: 3 },
+ { key: 'random-label-gewins3', count: 4 },
+ { key: 'random-label-telverstone4', count: 5 },
+ { key: 'random-label-ygerriets5', count: 6 },
+ { key: 'random-label-lmoffet6', count: 7 },
+ { key: 'random-label-ehinnerk7', count: 8 },
+ { key: 'random-label-flanceley8', count: 9 },
+ { key: 'random-label-adoyle9', count: 10 },
+ { key: 'random-label-rmcgirla', count: 11 },
+ { key: 'random-label-dwhellansb', count: 12 },
+ { key: 'random-label-apitkethlyc', count: 13 },
+ { key: 'random-label-senevoldsend', count: 14 },
+ { key: 'random-label-tlardnare', count: 15 },
+ { key: 'random-label-fcoilsf', count: 16 },
+ { key: 'random-label-qgeckg', count: 17 },
+ { key: 'random-label-rgrabenh', count: 18 },
+ { key: 'random-label-lashardi', count: 19 },
+ { key: 'random-label-sadamovitchj', count: 20 },
+ { key: 'random-label-rlyddiardk', count: 21 },
+ { key: 'random-label-jpoell', count: 22 },
+ { key: 'random-label-kcharitym', count: 23 },
+ { key: 'random-label-cbertenshawn', count: 24 },
+ { key: 'random-label-jsturgeso', count: 25 },
+ { key: 'random-label-ohouldcroftp', count: 26 },
+ { key: 'random-label-rheijnenq', count: 27 },
+ { key: 'random-label-snortheyr', count: 28 },
+ { key: 'random-label-vpairpoints', count: 29 },
+ { key: 'random-label-odavidovicit', count: 30 },
+ { key: 'random-label-fmccartu', count: 31 },
+ { key: 'random-label-cwansburyv', count: 32 },
+ { key: 'random-label-bdimontw', count: 33 },
+ { key: 'random-label-adocketx', count: 34 },
+ { key: 'random-label-obavridgey', count: 35 },
+ { key: 'random-label-jperezz', count: 36 },
+ { key: 'random-label-gdeneve10', count: 37 },
+ { key: 'random-label-rmckeand11', count: 38 },
+ { key: 'random-label-kwestmerland12', count: 39 },
+ { key: 'random-label-mpryer13', count: 40 },
+ { key: 'random-label-rmcneil14', count: 41 },
+ { key: 'random-label-ablondel15', count: 42 },
+ { key: 'random-label-wbalducci16', count: 43 },
+ { key: 'random-label-swigley17', count: 44 },
+ { key: 'random-label-gferroni18', count: 45 },
+ { key: 'random-label-icollings19', count: 46 },
+ { key: 'random-label-wszymanski1a', count: 47 },
+ { key: 'random-label-jelson1b', count: 48 },
+ { key: 'random-label-fsambrook1c', count: 49 },
+ { key: 'random-label-kconey1d', count: 50 },
+ { key: 'random-label-agoodread1e', count: 51 },
+ { key: 'random-label-nmewton1f', count: 52 },
+ { key: 'random-label-gcodman1g', count: 53 },
+ { key: 'random-label-rpoplee1h', count: 54 },
+ { key: 'random-label-mhug1i', count: 55 },
+ { key: 'random-label-ggowrie1j', count: 56 },
+ { key: 'random-label-ctonepohl1k', count: 57 },
+ { key: 'random-label-cstillman1l', count: 58 },
+ { key: 'random-label-dcollyer1m', count: 59 },
+ { key: 'random-label-idimelow1n', count: 60 },
+ { key: 'random-label-djarley1o', count: 61 },
+ { key: 'random-label-omclleese1p', count: 62 },
+ { key: 'random-label-dstivers1q', count: 63 },
+ { key: 'random-label-svose1r', count: 64 },
+ { key: 'random-label-clanfare1s', count: 65 },
+ { key: 'random-label-aport1t', count: 66 },
+ { key: 'random-label-hcarlett1u', count: 67 },
+ { key: 'random-label-dstillmann1v', count: 68 },
+ { key: 'random-label-ncorpe1w', count: 69 },
+ { key: 'random-label-mjacobsohn1x', count: 70 },
+ { key: 'random-label-ycleiment1y', count: 71 },
+ { key: 'random-label-owherton1z', count: 72 },
+ { key: 'random-label-anowaczyk20', count: 73 },
+ { key: 'random-label-rmckennan21', count: 74 },
+ { key: 'random-label-cmoulding22', count: 75 },
+ { key: 'random-label-sswate23', count: 76 },
+ { key: 'random-label-cbarge24', count: 77 },
+ { key: 'random-label-agrainger25', count: 78 },
+ { key: 'random-label-ncosin26', count: 79 },
+ { key: 'random-label-pkears27', count: 80 },
+ { key: 'random-label-cmcarthur28', count: 81 },
+ { key: 'random-label-jmantripp29', count: 82 },
+ { key: 'random-label-cjekel2a', count: 83 },
+ { key: 'random-label-hdilleway2b', count: 84 },
+ { key: 'random-label-lbovaird2c', count: 85 },
+ { key: 'random-label-mweld2d', count: 86 },
+ { key: 'random-label-marnowitz2e', count: 87 },
+ { key: 'random-label-nbertomieu2f', count: 88 },
+ { key: 'random-label-mledward2g', count: 89 },
+ { key: 'random-label-mhince2h', count: 90 },
+ { key: 'random-label-baarons2i', count: 91 },
+ { key: 'random-label-kfrancie2j', count: 92 },
+ { key: 'random-label-ishooter2k', count: 93 },
+ { key: 'random-label-glowmass2l', count: 94 },
+ { key: 'random-label-rgeorgi2m', count: 95 },
+ { key: 'random-label-bproby2n', count: 96 },
+ { key: 'random-label-hsteffan2o', count: 97 },
+ { key: 'random-label-doruane2p', count: 98 },
+ { key: 'random-label-rlunny2q', count: 99 },
+ { key: 'random-label-geles2r', count: 100 },
+ { key: 'random-label-nmaggiore2s', count: 101 },
+ { key: 'random-label-aboocock2t', count: 102 },
+ { key: 'random-label-eguilbert2u', count: 103 },
+ { key: 'random-label-emccutcheon2v', count: 104 },
+ { key: 'random-label-hcowser2w', count: 105 },
+ { key: 'random-label-dspeeding2x', count: 106 },
+ { key: 'random-label-oseebright2y', count: 107 },
+ { key: 'random-label-hpresdee2z', count: 108 },
+ { key: 'random-label-pesseby30', count: 109 },
+ { key: 'random-label-hpusey31', count: 110 },
+ { key: 'random-label-dmanthorpe32', count: 111 },
+ { key: 'random-label-natley33', count: 112 },
+ { key: 'random-label-iferentz34', count: 113 },
+ { key: 'random-label-adyble35', count: 114 },
+ { key: 'random-label-dlockitt36', count: 115 },
+ { key: 'random-label-acoxwell37', count: 116 },
+ { key: 'random-label-amcgarvey38', count: 117 },
+ { key: 'random-label-rmcgougan39', count: 118 },
+ { key: 'random-label-mscole3a', count: 119 },
+ { key: 'random-label-lmalim3b', count: 120 },
+ { key: 'random-label-cends3c', count: 121 },
+ { key: 'random-label-dmannie3d', count: 122 },
+ { key: 'random-label-lgoodricke3e', count: 123 },
+ { key: 'random-label-rcaghy3f', count: 124 },
+ { key: 'random-label-mprozillo3g', count: 125 },
+ { key: 'random-label-mcardnell3h', count: 126 },
+ { key: 'random-label-gericssen3i', count: 127 },
+ { key: 'random-label-fspooner3j', count: 128 },
+ { key: 'random-label-achadney3k', count: 129 },
+ { key: 'random-label-corchard3l', count: 130 },
+ { key: 'random-label-lyerill3m', count: 131 },
+ { key: 'random-label-jrusk3n', count: 132 },
+ { key: 'random-label-lbonelle3o', count: 133 },
+ { key: 'random-label-eduny3p', count: 134 },
+ { key: 'random-label-mhutchence3q', count: 135 },
+ { key: 'random-label-rmargeram3r', count: 136 },
+ { key: 'random-label-smaudlin3s', count: 137 },
+ { key: 'random-label-sfarrance3t', count: 138 },
+ { key: 'random-label-eclendennen3u', count: 139 },
+ { key: 'random-label-cyabsley3v', count: 140 },
+ { key: 'random-label-ahensmans3w', count: 141 },
+ { key: 'random-label-tsenchenko3x', count: 142 },
+ { key: 'random-label-ryurchishin3y', count: 143 },
+ { key: 'random-label-teby3z', count: 144 },
+ { key: 'random-label-dvaillant40', count: 145 },
+ { key: 'random-label-kpetyakov41', count: 146 },
+ { key: 'random-label-cmorrison42', count: 147 },
+ { key: 'random-label-ltwiddy43', count: 148 },
+ { key: 'random-label-ineame44', count: 149 },
+ { key: 'random-label-blucock45', count: 150 },
+ { key: 'random-label-kdunsford46', count: 151 },
+ { key: 'random-label-dducham47', count: 152 },
+ { key: 'random-label-javramovitz48', count: 153 },
+ { key: 'random-label-mascraft49', count: 154 },
+ { key: 'random-label-bloughead4a', count: 155 },
+ { key: 'random-label-sduckit4b', count: 156 },
+ { key: 'random-label-hhardman4c', count: 157 },
+ { key: 'random-label-cstaniforth4d', count: 158 },
+ { key: 'random-label-jedney4e', count: 159 },
+ { key: 'random-label-bobbard4f', count: 160 },
+ { key: 'random-label-cgiraux4g', count: 161 },
+ { key: 'random-label-tkiln4h', count: 162 },
+ { key: 'random-label-jwansbury4i', count: 163 },
+ { key: 'random-label-dquinlan4j', count: 164 },
+ { key: 'random-label-hgindghill4k', count: 165 },
+ { key: 'random-label-jjowle4l', count: 166 },
+ { key: 'random-label-egambrell4m', count: 167 },
+ { key: 'random-label-jmcgloughlin4n', count: 168 },
+ { key: 'random-label-bbabb4o', count: 169 },
+ { key: 'random-label-achuck4p', count: 170 },
+ { key: 'random-label-tsyers4q', count: 171 },
+ { key: 'random-label-jlandon4r', count: 172 },
+ { key: 'random-label-wteather4s', count: 173 },
+ { key: 'random-label-dfoskin4t', count: 174 },
+ { key: 'random-label-gmorlon4u', count: 175 },
+ { key: 'random-label-jseely4v', count: 176 },
+ { key: 'random-label-cbrass4w', count: 177 },
+ { key: 'random-label-fmanilo4x', count: 178 },
+ { key: 'random-label-bfrangleton4y', count: 179 },
+ { key: 'random-label-vbartkiewicz4z', count: 180 },
+ { key: 'random-label-tclymer50', count: 181 },
+ { key: 'random-label-pqueen51', count: 182 },
+ { key: 'random-label-bpol52', count: 183 },
+ { key: 'random-label-jclaeskens53', count: 184 },
+ { key: 'random-label-cstranieri54', count: 185 },
+ { key: 'random-label-drumbelow55', count: 186 },
+ { key: 'random-label-wbrumham56', count: 187 },
+ { key: 'random-label-azeal57', count: 188 },
+ { key: 'random-label-msnooks58', count: 189 },
+ { key: 'random-label-blapre59', count: 190 },
+ { key: 'random-label-cduckers5a', count: 191 },
+ { key: 'random-label-mgumary5b', count: 192 },
+ { key: 'random-label-rtebbs5c', count: 193 },
+ { key: 'random-label-eroe5d', count: 194 },
+ { key: 'random-label-rconfait5e', count: 195 },
+ { key: 'random-label-fsinderland5f', count: 196 },
+ { key: 'random-label-tdallywater5g', count: 197 },
+ { key: 'random-label-glindenman5h', count: 198 },
+ { key: 'random-label-fbauser5i', count: 199 },
+ { key: 'random-label-bdownton5j', count: 200 },
+ ],
+ },
+];
+
+export const MOCK_LANGUAGE_AGGREGATIONS_BUCKETS = [
+ { key: 'random-label-edumingos0', count: 1 },
+ { key: 'random-label-rbourgourd1', count: 2 },
+ { key: 'random-label-dfearnside2', count: 3 },
+ { key: 'random-label-gewins3', count: 4 },
+ { key: 'random-label-telverstone4', count: 5 },
+ { key: 'random-label-ygerriets5', count: 6 },
+ { key: 'random-label-lmoffet6', count: 7 },
+ { key: 'random-label-ehinnerk7', count: 8 },
+ { key: 'random-label-flanceley8', count: 9 },
+ { key: 'random-label-adoyle9', count: 10 },
+ { key: 'random-label-rmcgirla', count: 11 },
+ { key: 'random-label-dwhellansb', count: 12 },
+ { key: 'random-label-apitkethlyc', count: 13 },
+ { key: 'random-label-senevoldsend', count: 14 },
+ { key: 'random-label-tlardnare', count: 15 },
+ { key: 'random-label-fcoilsf', count: 16 },
+ { key: 'random-label-qgeckg', count: 17 },
+ { key: 'random-label-rgrabenh', count: 18 },
+ { key: 'random-label-lashardi', count: 19 },
+ { key: 'random-label-sadamovitchj', count: 20 },
+ { key: 'random-label-rlyddiardk', count: 21 },
+ { key: 'random-label-jpoell', count: 22 },
+ { key: 'random-label-kcharitym', count: 23 },
+ { key: 'random-label-cbertenshawn', count: 24 },
+ { key: 'random-label-jsturgeso', count: 25 },
+ { key: 'random-label-ohouldcroftp', count: 26 },
+ { key: 'random-label-rheijnenq', count: 27 },
+ { key: 'random-label-snortheyr', count: 28 },
+ { key: 'random-label-vpairpoints', count: 29 },
+ { key: 'random-label-odavidovicit', count: 30 },
+ { key: 'random-label-fmccartu', count: 31 },
+ { key: 'random-label-cwansburyv', count: 32 },
+ { key: 'random-label-bdimontw', count: 33 },
+ { key: 'random-label-adocketx', count: 34 },
+ { key: 'random-label-obavridgey', count: 35 },
+ { key: 'random-label-jperezz', count: 36 },
+ { key: 'random-label-gdeneve10', count: 37 },
+ { key: 'random-label-rmckeand11', count: 38 },
+ { key: 'random-label-kwestmerland12', count: 39 },
+ { key: 'random-label-mpryer13', count: 40 },
+ { key: 'random-label-rmcneil14', count: 41 },
+ { key: 'random-label-ablondel15', count: 42 },
+ { key: 'random-label-wbalducci16', count: 43 },
+ { key: 'random-label-swigley17', count: 44 },
+ { key: 'random-label-gferroni18', count: 45 },
+ { key: 'random-label-icollings19', count: 46 },
+ { key: 'random-label-wszymanski1a', count: 47 },
+ { key: 'random-label-jelson1b', count: 48 },
+ { key: 'random-label-fsambrook1c', count: 49 },
+ { key: 'random-label-kconey1d', count: 50 },
+ { key: 'random-label-agoodread1e', count: 51 },
+ { key: 'random-label-nmewton1f', count: 52 },
+ { key: 'random-label-gcodman1g', count: 53 },
+ { key: 'random-label-rpoplee1h', count: 54 },
+ { key: 'random-label-mhug1i', count: 55 },
+ { key: 'random-label-ggowrie1j', count: 56 },
+ { key: 'random-label-ctonepohl1k', count: 57 },
+ { key: 'random-label-cstillman1l', count: 58 },
+ { key: 'random-label-dcollyer1m', count: 59 },
+ { key: 'random-label-idimelow1n', count: 60 },
+ { key: 'random-label-djarley1o', count: 61 },
+ { key: 'random-label-omclleese1p', count: 62 },
+ { key: 'random-label-dstivers1q', count: 63 },
+ { key: 'random-label-svose1r', count: 64 },
+ { key: 'random-label-clanfare1s', count: 65 },
+ { key: 'random-label-aport1t', count: 66 },
+ { key: 'random-label-hcarlett1u', count: 67 },
+ { key: 'random-label-dstillmann1v', count: 68 },
+ { key: 'random-label-ncorpe1w', count: 69 },
+ { key: 'random-label-mjacobsohn1x', count: 70 },
+ { key: 'random-label-ycleiment1y', count: 71 },
+ { key: 'random-label-owherton1z', count: 72 },
+ { key: 'random-label-anowaczyk20', count: 73 },
+ { key: 'random-label-rmckennan21', count: 74 },
+ { key: 'random-label-cmoulding22', count: 75 },
+ { key: 'random-label-sswate23', count: 76 },
+ { key: 'random-label-cbarge24', count: 77 },
+ { key: 'random-label-agrainger25', count: 78 },
+ { key: 'random-label-ncosin26', count: 79 },
+ { key: 'random-label-pkears27', count: 80 },
+ { key: 'random-label-cmcarthur28', count: 81 },
+ { key: 'random-label-jmantripp29', count: 82 },
+ { key: 'random-label-cjekel2a', count: 83 },
+ { key: 'random-label-hdilleway2b', count: 84 },
+ { key: 'random-label-lbovaird2c', count: 85 },
+ { key: 'random-label-mweld2d', count: 86 },
+ { key: 'random-label-marnowitz2e', count: 87 },
+ { key: 'random-label-nbertomieu2f', count: 88 },
+ { key: 'random-label-mledward2g', count: 89 },
+ { key: 'random-label-mhince2h', count: 90 },
+ { key: 'random-label-baarons2i', count: 91 },
+ { key: 'random-label-kfrancie2j', count: 92 },
+ { key: 'random-label-ishooter2k', count: 93 },
+ { key: 'random-label-glowmass2l', count: 94 },
+ { key: 'random-label-rgeorgi2m', count: 95 },
+ { key: 'random-label-bproby2n', count: 96 },
+ { key: 'random-label-hsteffan2o', count: 97 },
+ { key: 'random-label-doruane2p', count: 98 },
+ { key: 'random-label-rlunny2q', count: 99 },
+ { key: 'random-label-geles2r', count: 100 },
+ { key: 'random-label-nmaggiore2s', count: 101 },
+ { key: 'random-label-aboocock2t', count: 102 },
+ { key: 'random-label-eguilbert2u', count: 103 },
+ { key: 'random-label-emccutcheon2v', count: 104 },
+ { key: 'random-label-hcowser2w', count: 105 },
+ { key: 'random-label-dspeeding2x', count: 106 },
+ { key: 'random-label-oseebright2y', count: 107 },
+ { key: 'random-label-hpresdee2z', count: 108 },
+ { key: 'random-label-pesseby30', count: 109 },
+ { key: 'random-label-hpusey31', count: 110 },
+ { key: 'random-label-dmanthorpe32', count: 111 },
+ { key: 'random-label-natley33', count: 112 },
+ { key: 'random-label-iferentz34', count: 113 },
+ { key: 'random-label-adyble35', count: 114 },
+ { key: 'random-label-dlockitt36', count: 115 },
+ { key: 'random-label-acoxwell37', count: 116 },
+ { key: 'random-label-amcgarvey38', count: 117 },
+ { key: 'random-label-rmcgougan39', count: 118 },
+ { key: 'random-label-mscole3a', count: 119 },
+ { key: 'random-label-lmalim3b', count: 120 },
+ { key: 'random-label-cends3c', count: 121 },
+ { key: 'random-label-dmannie3d', count: 122 },
+ { key: 'random-label-lgoodricke3e', count: 123 },
+ { key: 'random-label-rcaghy3f', count: 124 },
+ { key: 'random-label-mprozillo3g', count: 125 },
+ { key: 'random-label-mcardnell3h', count: 126 },
+ { key: 'random-label-gericssen3i', count: 127 },
+ { key: 'random-label-fspooner3j', count: 128 },
+ { key: 'random-label-achadney3k', count: 129 },
+ { key: 'random-label-corchard3l', count: 130 },
+ { key: 'random-label-lyerill3m', count: 131 },
+ { key: 'random-label-jrusk3n', count: 132 },
+ { key: 'random-label-lbonelle3o', count: 133 },
+ { key: 'random-label-eduny3p', count: 134 },
+ { key: 'random-label-mhutchence3q', count: 135 },
+ { key: 'random-label-rmargeram3r', count: 136 },
+ { key: 'random-label-smaudlin3s', count: 137 },
+ { key: 'random-label-sfarrance3t', count: 138 },
+ { key: 'random-label-eclendennen3u', count: 139 },
+ { key: 'random-label-cyabsley3v', count: 140 },
+ { key: 'random-label-ahensmans3w', count: 141 },
+ { key: 'random-label-tsenchenko3x', count: 142 },
+ { key: 'random-label-ryurchishin3y', count: 143 },
+ { key: 'random-label-teby3z', count: 144 },
+ { key: 'random-label-dvaillant40', count: 145 },
+ { key: 'random-label-kpetyakov41', count: 146 },
+ { key: 'random-label-cmorrison42', count: 147 },
+ { key: 'random-label-ltwiddy43', count: 148 },
+ { key: 'random-label-ineame44', count: 149 },
+ { key: 'random-label-blucock45', count: 150 },
+ { key: 'random-label-kdunsford46', count: 151 },
+ { key: 'random-label-dducham47', count: 152 },
+ { key: 'random-label-javramovitz48', count: 153 },
+ { key: 'random-label-mascraft49', count: 154 },
+ { key: 'random-label-bloughead4a', count: 155 },
+ { key: 'random-label-sduckit4b', count: 156 },
+ { key: 'random-label-hhardman4c', count: 157 },
+ { key: 'random-label-cstaniforth4d', count: 158 },
+ { key: 'random-label-jedney4e', count: 159 },
+ { key: 'random-label-bobbard4f', count: 160 },
+ { key: 'random-label-cgiraux4g', count: 161 },
+ { key: 'random-label-tkiln4h', count: 162 },
+ { key: 'random-label-jwansbury4i', count: 163 },
+ { key: 'random-label-dquinlan4j', count: 164 },
+ { key: 'random-label-hgindghill4k', count: 165 },
+ { key: 'random-label-jjowle4l', count: 166 },
+ { key: 'random-label-egambrell4m', count: 167 },
+ { key: 'random-label-jmcgloughlin4n', count: 168 },
+ { key: 'random-label-bbabb4o', count: 169 },
+ { key: 'random-label-achuck4p', count: 170 },
+ { key: 'random-label-tsyers4q', count: 171 },
+ { key: 'random-label-jlandon4r', count: 172 },
+ { key: 'random-label-wteather4s', count: 173 },
+ { key: 'random-label-dfoskin4t', count: 174 },
+ { key: 'random-label-gmorlon4u', count: 175 },
+ { key: 'random-label-jseely4v', count: 176 },
+ { key: 'random-label-cbrass4w', count: 177 },
+ { key: 'random-label-fmanilo4x', count: 178 },
+ { key: 'random-label-bfrangleton4y', count: 179 },
+ { key: 'random-label-vbartkiewicz4z', count: 180 },
+ { key: 'random-label-tclymer50', count: 181 },
+ { key: 'random-label-pqueen51', count: 182 },
+ { key: 'random-label-bpol52', count: 183 },
+ { key: 'random-label-jclaeskens53', count: 184 },
+ { key: 'random-label-cstranieri54', count: 185 },
+ { key: 'random-label-drumbelow55', count: 186 },
+ { key: 'random-label-wbrumham56', count: 187 },
+ { key: 'random-label-azeal57', count: 188 },
+ { key: 'random-label-msnooks58', count: 189 },
+ { key: 'random-label-blapre59', count: 190 },
+ { key: 'random-label-cduckers5a', count: 191 },
+ { key: 'random-label-mgumary5b', count: 192 },
+ { key: 'random-label-rtebbs5c', count: 193 },
+ { key: 'random-label-eroe5d', count: 194 },
+ { key: 'random-label-rconfait5e', count: 195 },
+ { key: 'random-label-fsinderland5f', count: 196 },
+ { key: 'random-label-tdallywater5g', count: 197 },
+ { key: 'random-label-glindenman5h', count: 198 },
+ { key: 'random-label-fbauser5i', count: 199 },
+ { key: 'random-label-bdownton5j', count: 200 },
+];
+
+export const MOCK_RECEIVE_AGGREGATIONS_SUCCESS_MUTATION = [
+ {
+ type: types.REQUEST_AGGREGATIONS,
+ },
+ {
+ type: types.RECEIVE_AGGREGATIONS_SUCCESS,
+ payload: MOCK_AGGREGATIONS,
+ },
+];
+
+export const MOCK_RECEIVE_AGGREGATIONS_ERROR_MUTATION = [
+ {
+ type: types.REQUEST_AGGREGATIONS,
+ },
+ {
+ type: types.RECEIVE_AGGREGATIONS_ERROR,
+ },
+];
+
+export const TEST_RAW_BUCKETS = [
+ { key: 'Go', count: 350 },
+ { key: 'C', count: 298 },
+ { key: 'JavaScript', count: 128 },
+ { key: 'YAML', count: 58 },
+ { key: 'Text', count: 46 },
+ { key: 'Markdown', count: 37 },
+ { key: 'HTML', count: 34 },
+ { key: 'Shell', count: 34 },
+ { key: 'Makefile', count: 21 },
+ { key: 'JSON', count: 15 },
+];
+
+export const TEST_FILTER_DATA = {
+ header: 'Language',
+ scopes: { BLOBS: 'blobs' },
+ filterParam: 'language',
+ filters: {
+ GO: { label: 'Go', value: 'Go', count: 350 },
+ C: { label: 'C', value: 'C', count: 298 },
+ JAVASCRIPT: { label: 'JavaScript', value: 'JavaScript', count: 128 },
+ YAML: { label: 'YAML', value: 'YAML', count: 58 },
+ TEXT: { label: 'Text', value: 'Text', count: 46 },
+ MARKDOWN: { label: 'Markdown', value: 'Markdown', count: 37 },
+ HTML: { label: 'HTML', value: 'HTML', count: 34 },
+ SHELL: { label: 'Shell', value: 'Shell', count: 34 },
+ MAKEFILE: { label: 'Makefile', value: 'Makefile', count: 21 },
+ JSON: { label: 'JSON', value: 'JSON', count: 15 },
+ },
+};
diff --git a/spec/frontend/search/sidebar/components/app_spec.js b/spec/frontend/search/sidebar/components/app_spec.js
index e87217950cd..83302b90233 100644
--- a/spec/frontend/search/sidebar/components/app_spec.js
+++ b/spec/frontend/search/sidebar/components/app_spec.js
@@ -5,6 +5,7 @@ import { MOCK_QUERY } from 'jest/search/mock_data';
import GlobalSearchSidebar from '~/search/sidebar/components/app.vue';
import ResultsFilters from '~/search/sidebar/components/results_filters.vue';
import ScopeNavigation from '~/search/sidebar/components/scope_navigation.vue';
+import LanguageFilter from '~/search/sidebar/components/language_filter.vue';
Vue.use(Vuex);
@@ -35,72 +36,66 @@ describe('GlobalSearchSidebar', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findSidebarSection = () => wrapper.find('section');
const findFilters = () => wrapper.findComponent(ResultsFilters);
const findSidebarNavigation = () => wrapper.findComponent(ScopeNavigation);
+ const findLanguageAggregation = () => wrapper.findComponent(LanguageFilter);
describe('renders properly', () => {
- describe('scope=projects', () => {
+ describe('always', () => {
beforeEach(() => {
- createComponent({ urlQuery: { ...MOCK_QUERY, scope: 'projects' } });
+ createComponent({});
});
-
- it('shows section', () => {
+ it(`shows section`, () => {
expect(findSidebarSection().exists()).toBe(true);
});
-
- it("doesn't shows filters", () => {
- expect(findFilters().exists()).toBe(false);
- });
});
- describe('scope=merge_requests', () => {
+ describe.each`
+ scope | showFilters | ShowsLanguage
+ ${'issues'} | ${true} | ${false}
+ ${'merge_requests'} | ${true} | ${false}
+ ${'projects'} | ${false} | ${false}
+ ${'blobs'} | ${false} | ${true}
+ `('sidebar scope: $scope', ({ scope, showFilters, ShowsLanguage }) => {
beforeEach(() => {
- createComponent({ urlQuery: { ...MOCK_QUERY, scope: 'merge_requests' } });
+ createComponent({ urlQuery: { scope } }, { searchBlobsLanguageAggregation: true });
});
- it('shows section', () => {
- expect(findSidebarSection().exists()).toBe(true);
+ it(`${!showFilters ? "doesn't" : ''} shows filters`, () => {
+ expect(findFilters().exists()).toBe(showFilters);
});
- it('shows filters', () => {
- expect(findFilters().exists()).toBe(true);
+ it(`${!ShowsLanguage ? "doesn't" : ''} shows language filters`, () => {
+ expect(findLanguageAggregation().exists()).toBe(ShowsLanguage);
});
});
- describe('scope=issues', () => {
+ describe('renders navigation', () => {
beforeEach(() => {
- createComponent({ urlQuery: MOCK_QUERY });
- });
- it('shows section', () => {
- expect(findSidebarSection().exists()).toBe(true);
+ createComponent({});
});
-
- it('shows filters', () => {
- expect(findFilters().exists()).toBe(true);
+ it('shows the vertical navigation', () => {
+ expect(findSidebarNavigation().exists()).toBe(true);
});
});
});
- describe('when search_page_vertical_nav is enabled', () => {
+ describe('when search_blobs_language_aggregation is enabled', () => {
beforeEach(() => {
- createComponent({}, { searchPageVerticalNav: true });
+ createComponent({ urlQuery: { scope: 'blobs' } }, { searchBlobsLanguageAggregation: true });
});
- it('shows the vertical navigation', () => {
- expect(findSidebarNavigation().exists()).toBe(true);
+ it('shows the language filter', () => {
+ expect(findLanguageAggregation().exists()).toBe(true);
});
});
- describe('when search_page_vertical_nav is disabled', () => {
+ describe('when search_blobs_language_aggregation is disabled', () => {
beforeEach(() => {
- createComponent({}, { searchPageVerticalNav: false });
+ createComponent({ urlQuery: { scope: 'blobs' } }, { searchBlobsLanguageAggregation: false });
});
- it('hides the vertical navigation', () => {
- expect(findSidebarNavigation().exists()).toBe(false);
+ it('hides the language filter', () => {
+ expect(findLanguageAggregation().exists()).toBe(false);
});
});
});
diff --git a/spec/frontend/search/sidebar/components/checkbox_filter_spec.js b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
new file mode 100644
index 00000000000..82017754b23
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/checkbox_filter_spec.js
@@ -0,0 +1,85 @@
+import { GlFormCheckboxGroup, GlFormCheckbox } from '@gitlab/ui';
+import Vue from 'vue';
+import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { MOCK_QUERY, MOCK_LANGUAGE_AGGREGATIONS_BUCKETS } from 'jest/search/mock_data';
+import CheckboxFilter from '~/search/sidebar/components/checkbox_filter.vue';
+
+import { languageFilterData } from '~/search/sidebar/constants/language_filter_data';
+import { convertFiltersData } from '~/search/sidebar/utils';
+
+Vue.use(Vuex);
+
+describe('CheckboxFilter', () => {
+ let wrapper;
+
+ const actionSpies = {
+ setQuery: jest.fn(),
+ };
+
+ const defaultProps = {
+ filterData: convertFiltersData(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
+ };
+
+ const createComponent = () => {
+ const store = new Vuex.Store({
+ state: {
+ query: MOCK_QUERY,
+ },
+ actions: actionSpies,
+ });
+
+ wrapper = shallowMountExtended(CheckboxFilter, {
+ store,
+ propsData: {
+ ...defaultProps,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ const findFormCheckboxGroup = () => wrapper.findComponent(GlFormCheckboxGroup);
+ const findAllCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
+ const fintAllCheckboxLabels = () => wrapper.findAllByTestId('label');
+ const fintAllCheckboxLabelCounts = () => wrapper.findAllByTestId('labelCount');
+
+ describe('Renders correctly', () => {
+ it('renders form', () => {
+ expect(findFormCheckboxGroup().exists()).toBe(true);
+ });
+
+ it('renders checkbox-filter', () => {
+ expect(findAllCheckboxes().exists()).toBe(true);
+ });
+
+ it('renders all checkbox-filter checkboxes', () => {
+ expect(findAllCheckboxes()).toHaveLength(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS.length);
+ });
+
+ it('renders correctly label for the element', () => {
+ expect(fintAllCheckboxLabels().at(0).text()).toBe(MOCK_LANGUAGE_AGGREGATIONS_BUCKETS[0].key);
+ });
+
+ it('renders correctly count for the element', () => {
+ expect(fintAllCheckboxLabelCounts().at(0).text()).toBe(
+ MOCK_LANGUAGE_AGGREGATIONS_BUCKETS[0].count.toString(),
+ );
+ });
+ });
+
+ describe('actions', () => {
+ it('triggers setQuery', () => {
+ const filter =
+ defaultProps.filterData.filters[Object.keys(defaultProps.filterData.filters)[0]].value;
+ findFormCheckboxGroup().vm.$emit('input', filter);
+
+ expect(actionSpies.setQuery).toHaveBeenCalledWith(expect.any(Object), {
+ key: languageFilterData.filterParam,
+ value: filter,
+ });
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
index d5ecca4636c..4f146757454 100644
--- a/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/confidentiality_filter_spec.js
@@ -22,24 +22,4 @@ describe('ConfidentialityFilter', () => {
expect(findRadioFilter().exists()).toBe(true);
});
});
-
- describe.each`
- hasFeatureFlagEnabled | paddingClass
- ${true} | ${'gl-px-5'}
- ${false} | ${'gl-px-0'}
- `(`RadioFilter`, ({ hasFeatureFlagEnabled, paddingClass }) => {
- beforeEach(() => {
- createComponent({
- provide: {
- glFeatures: {
- searchPageVerticalNav: hasFeatureFlagEnabled,
- },
- },
- });
- });
-
- it(`has ${paddingClass} class`, () => {
- expect(findRadioFilter().classes(paddingClass)).toBe(true);
- });
- });
});
diff --git a/spec/frontend/search/sidebar/components/language_filters_spec.js b/spec/frontend/search/sidebar/components/language_filters_spec.js
new file mode 100644
index 00000000000..e297d1c33b0
--- /dev/null
+++ b/spec/frontend/search/sidebar/components/language_filters_spec.js
@@ -0,0 +1,152 @@
+import { GlAlert, GlFormCheckbox, GlForm } from '@gitlab/ui';
+import Vue, { nextTick } from 'vue';
+import Vuex from 'vuex';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import {
+ MOCK_QUERY,
+ MOCK_AGGREGATIONS,
+ MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
+} from 'jest/search/mock_data';
+import LanguageFilter from '~/search/sidebar/components/language_filter.vue';
+import CheckboxFilter from '~/search/sidebar/components/checkbox_filter.vue';
+import { MAX_ITEM_LENGTH } from '~/search/sidebar/constants/language_filter_data';
+
+Vue.use(Vuex);
+
+describe('GlobalSearchSidebarLanguageFilter', () => {
+ let wrapper;
+
+ const actionSpies = {
+ fetchLanguageAggregation: jest.fn(),
+ applyQuery: jest.fn(),
+ };
+
+ const getterSpies = {
+ langugageAggregationBuckets: jest.fn(() => MOCK_LANGUAGE_AGGREGATIONS_BUCKETS),
+ };
+
+ const createComponent = (initialState) => {
+ const store = new Vuex.Store({
+ state: {
+ query: MOCK_QUERY,
+ urlQuery: MOCK_QUERY,
+ aggregations: MOCK_AGGREGATIONS,
+ ...initialState,
+ },
+ actions: actionSpies,
+ getters: getterSpies,
+ });
+
+ wrapper = shallowMountExtended(LanguageFilter, {
+ store,
+ stubs: {
+ CheckboxFilter,
+ },
+ });
+ };
+
+ const findForm = () => wrapper.findComponent(GlForm);
+ const findCheckboxFilter = () => wrapper.findComponent(CheckboxFilter);
+ const findApplyButton = () => wrapper.findByTestId('apply-button');
+ const findShowMoreButton = () => wrapper.findByTestId('show-more-button');
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findAllCheckboxes = () => wrapper.findAllComponents(GlFormCheckbox);
+ const findHasOverMax = () => wrapper.findByTestId('has-over-max-text');
+
+ describe('Renders correctly', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders form', () => {
+ expect(findForm().exists()).toBe(true);
+ });
+
+ it('renders checkbox-filter', () => {
+ expect(findCheckboxFilter().exists()).toBe(true);
+ });
+
+ it('renders all checkbox-filter checkboxes', () => {
+ // 11th checkbox is hidden
+ expect(findAllCheckboxes()).toHaveLength(10);
+ });
+
+ it('renders ApplyButton', () => {
+ expect(findApplyButton().exists()).toBe(true);
+ });
+
+ it('renders Show More button', () => {
+ expect(findShowMoreButton().exists()).toBe(true);
+ });
+
+ it("doesn't render Alert", () => {
+ expect(findAlert().exists()).toBe(false);
+ });
+ });
+
+ describe('ApplyButton', () => {
+ describe('when sidebarDirty is false', () => {
+ beforeEach(() => {
+ createComponent({ sidebarDirty: false });
+ });
+
+ it('disables the button', () => {
+ expect(findApplyButton().attributes('disabled')).toBe('true');
+ });
+ });
+
+ describe('when sidebarDirty is true', () => {
+ beforeEach(() => {
+ createComponent({ sidebarDirty: true });
+ });
+
+ it('enables the button', () => {
+ expect(findApplyButton().attributes('disabled')).toBe(undefined);
+ });
+ });
+ });
+
+ describe('Show All button works', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it(`renders ${MAX_ITEM_LENGTH} amount of items`, async () => {
+ findShowMoreButton().vm.$emit('click');
+ await nextTick();
+ expect(findAllCheckboxes()).toHaveLength(MAX_ITEM_LENGTH);
+ });
+
+ it(`renders more then ${MAX_ITEM_LENGTH} text`, async () => {
+ findShowMoreButton().vm.$emit('click');
+ await nextTick();
+ expect(findHasOverMax().exists()).toBe(true);
+ });
+
+ it(`doesn't render show more button after click`, async () => {
+ findShowMoreButton().vm.$emit('click');
+ await nextTick();
+ expect(findShowMoreButton().exists()).toBe(false);
+ });
+ });
+
+ describe('actions', () => {
+ beforeEach(() => {
+ createComponent({});
+ });
+
+ it('uses getter langugageAggregationBuckets', () => {
+ expect(getterSpies.langugageAggregationBuckets).toHaveBeenCalled();
+ });
+
+ it('uses action fetchLanguageAggregation', () => {
+ expect(actionSpies.fetchLanguageAggregation).toHaveBeenCalled();
+ });
+
+ it('clicking ApplyButton calls applyQuery', () => {
+ findForm().vm.$emit('submit', { preventDefault: () => {} });
+
+ expect(actionSpies.applyQuery).toHaveBeenCalled();
+ });
+ });
+});
diff --git a/spec/frontend/search/sidebar/components/status_filter_spec.js b/spec/frontend/search/sidebar/components/status_filter_spec.js
index 2ed199469e6..6704634ef36 100644
--- a/spec/frontend/search/sidebar/components/status_filter_spec.js
+++ b/spec/frontend/search/sidebar/components/status_filter_spec.js
@@ -22,24 +22,4 @@ describe('StatusFilter', () => {
expect(findRadioFilter().exists()).toBe(true);
});
});
-
- describe.each`
- hasFeatureFlagEnabled | paddingClass
- ${true} | ${'gl-px-5'}
- ${false} | ${'gl-px-0'}
- `(`RadioFilter`, ({ hasFeatureFlagEnabled, paddingClass }) => {
- beforeEach(() => {
- createComponent({
- provide: {
- glFeatures: {
- searchPageVerticalNav: hasFeatureFlagEnabled,
- },
- },
- });
- });
-
- it(`has ${paddingClass} class`, () => {
- expect(findRadioFilter().classes(paddingClass)).toBe(true);
- });
- });
});
diff --git a/spec/frontend/search/sidebar/utils_spec.js b/spec/frontend/search/sidebar/utils_spec.js
new file mode 100644
index 00000000000..652d32c836e
--- /dev/null
+++ b/spec/frontend/search/sidebar/utils_spec.js
@@ -0,0 +1,10 @@
+import { convertFiltersData } from '~/search/sidebar/utils';
+import { TEST_RAW_BUCKETS, TEST_FILTER_DATA } from '../mock_data';
+
+describe('Global Search sidebar utils', () => {
+ describe('convertFiltersData', () => {
+ it('converts raw buckets to array', () => {
+ expect(convertFiltersData(TEST_RAW_BUCKETS)).toStrictEqual(TEST_FILTER_DATA);
+ });
+ });
+});
diff --git a/spec/frontend/search/store/actions_spec.js b/spec/frontend/search/store/actions_spec.js
index 3d19b27ff86..2f87802dfe6 100644
--- a/spec/frontend/search/store/actions_spec.js
+++ b/spec/frontend/search/store/actions_spec.js
@@ -4,6 +4,7 @@ import Api from '~/api';
import { createAlert } from '~/flash';
import * as logger from '~/lib/logger';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as urlUtils from '~/lib/utils/url_utility';
import * as actions from '~/search/store/actions';
import {
@@ -27,6 +28,9 @@ import {
MOCK_NAVIGATION_DATA,
MOCK_NAVIGATION_ACTION_MUTATION,
MOCK_ENDPOINT_RESPONSE,
+ MOCK_RECEIVE_AGGREGATIONS_SUCCESS_MUTATION,
+ MOCK_RECEIVE_AGGREGATIONS_ERROR_MUTATION,
+ MOCK_AGGREGATIONS,
} from '../mock_data';
jest.mock('~/flash');
@@ -59,11 +63,11 @@ describe('Global Search Store Actions', () => {
});
describe.each`
- action | axiosMock | type | expectedMutations | flashCallCount
- ${actions.fetchGroups} | ${{ method: 'onGet', code: 200, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${0}
- ${actions.fetchGroups} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${1}
- ${actions.fetchProjects} | ${{ method: 'onGet', code: 200, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${0}
- ${actions.fetchProjects} | ${{ method: 'onGet', code: 500, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${1}
+ action | axiosMock | type | expectedMutations | flashCallCount
+ ${actions.fetchGroups} | ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_GROUPS }} | ${'success'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_SUCCESS, payload: MOCK_GROUPS }]} | ${0}
+ ${actions.fetchGroups} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_GROUPS }, { type: types.RECEIVE_GROUPS_ERROR }]} | ${1}
+ ${actions.fetchProjects} | ${{ method: 'onGet', code: HTTP_STATUS_OK, res: MOCK_PROJECTS }} | ${'success'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_SUCCESS, payload: MOCK_PROJECTS }]} | ${0}
+ ${actions.fetchProjects} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${[{ type: types.REQUEST_PROJECTS }, { type: types.RECEIVE_PROJECTS_ERROR }]} | ${1}
`(`axios calls`, ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
@@ -80,11 +84,11 @@ describe('Global Search Store Actions', () => {
});
describe.each`
- action | axiosMock | type | expectedMutations | flashCallCount
- ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resGroups]} | ${0}
- ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[]} | ${1}
- ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resProjects]} | ${0}
- ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${[]} | ${1}
+ action | axiosMock | type | expectedMutations | flashCallCount
+ ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resGroups]} | ${0}
+ ${actions.loadFrequentGroups} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${[]} | ${1}
+ ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${[PROMISE_ALL_EXPECTED_MUTATIONS.resProjects]} | ${0}
+ ${actions.loadFrequentProjects} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${[]} | ${1}
`('Promise.all calls', ({ action, axiosMock, type, expectedMutations, flashCallCount }) => {
describe(action.name, () => {
describe(`on ${type}`, () => {
@@ -269,10 +273,10 @@ describe('Global Search Store Actions', () => {
});
describe.each`
- action | axiosMock | type | scope | expectedMutations | errorLogs
- ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: 200 }} | ${'success'} | ${'issues'} | ${[MOCK_NAVIGATION_ACTION_MUTATION]} | ${0}
- ${actions.fetchSidebarCount} | ${{ method: null, code: 0 }} | ${'success'} | ${'projects'} | ${[]} | ${0}
- ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: 500 }} | ${'error'} | ${'issues'} | ${[]} | ${1}
+ action | axiosMock | type | scope | expectedMutations | errorLogs
+ ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${'issues'} | ${[MOCK_NAVIGATION_ACTION_MUTATION]} | ${0}
+ ${actions.fetchSidebarCount} | ${{ method: null, code: 0 }} | ${'success'} | ${'projects'} | ${[]} | ${0}
+ ${actions.fetchSidebarCount} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${'issues'} | ${[]} | ${1}
`('fetchSidebarCount', ({ action, axiosMock, type, expectedMutations, scope, errorLogs }) => {
describe(`on ${type}`, () => {
beforeEach(() => {
@@ -295,4 +299,30 @@ describe('Global Search Store Actions', () => {
});
});
});
+
+ describe.each`
+ action | axiosMock | type | expectedMutations | errorLogs
+ ${actions.fetchLanguageAggregation} | ${{ method: 'onGet', code: HTTP_STATUS_OK }} | ${'success'} | ${MOCK_RECEIVE_AGGREGATIONS_SUCCESS_MUTATION} | ${0}
+ ${actions.fetchLanguageAggregation} | ${{ method: 'onPut', code: 0 }} | ${'error'} | ${MOCK_RECEIVE_AGGREGATIONS_ERROR_MUTATION} | ${1}
+ ${actions.fetchLanguageAggregation} | ${{ method: 'onGet', code: HTTP_STATUS_INTERNAL_SERVER_ERROR }} | ${'error'} | ${MOCK_RECEIVE_AGGREGATIONS_ERROR_MUTATION} | ${1}
+ `('fetchLanguageAggregation', ({ action, axiosMock, type, expectedMutations, errorLogs }) => {
+ describe(`on ${type}`, () => {
+ beforeEach(() => {
+ if (axiosMock.method) {
+ mock[axiosMock.method]().reply(
+ axiosMock.code,
+ axiosMock.code === HTTP_STATUS_OK ? MOCK_AGGREGATIONS : [],
+ );
+ }
+ });
+
+ it(`should ${type === 'error' ? 'NOT ' : ''}dispatch ${
+ type === 'error' ? '' : 'the correct '
+ }mutations`, () => {
+ return testAction({ action, state, expectedMutations }).then(() => {
+ expect(logger.logError).toHaveBeenCalledTimes(errorLogs);
+ });
+ });
+ });
+ });
});
diff --git a/spec/frontend/search/store/getters_spec.js b/spec/frontend/search/store/getters_spec.js
index 081e6a986eb..818902ee720 100644
--- a/spec/frontend/search/store/getters_spec.js
+++ b/spec/frontend/search/store/getters_spec.js
@@ -1,7 +1,13 @@
import { GROUPS_LOCAL_STORAGE_KEY, PROJECTS_LOCAL_STORAGE_KEY } from '~/search/store/constants';
import * as getters from '~/search/store/getters';
import createState from '~/search/store/state';
-import { MOCK_QUERY, MOCK_GROUPS, MOCK_PROJECTS } from '../mock_data';
+import {
+ MOCK_QUERY,
+ MOCK_GROUPS,
+ MOCK_PROJECTS,
+ MOCK_AGGREGATIONS,
+ MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
+} from '../mock_data';
describe('Global Search Store Getters', () => {
let state;
@@ -29,4 +35,16 @@ describe('Global Search Store Getters', () => {
expect(getters.frequentProjects(state)).toStrictEqual(MOCK_PROJECTS);
});
});
+
+ describe('langugageAggregationBuckets', () => {
+ beforeEach(() => {
+ state.aggregations.data = MOCK_AGGREGATIONS;
+ });
+
+ it('returns the correct data', () => {
+ expect(getters.langugageAggregationBuckets(state)).toStrictEqual(
+ MOCK_LANGUAGE_AGGREGATIONS_BUCKETS,
+ );
+ });
+ });
});
diff --git a/spec/frontend/search/store/mutations_spec.js b/spec/frontend/search/store/mutations_spec.js
index a79ec8f70b0..d604cf38f8f 100644
--- a/spec/frontend/search/store/mutations_spec.js
+++ b/spec/frontend/search/store/mutations_spec.js
@@ -8,6 +8,7 @@ import {
MOCK_NAVIGATION_DATA,
MOCK_NAVIGATION_ACTION_MUTATION,
MOCK_DATA_FOR_NAVIGATION_ACTION_MUTATION,
+ MOCK_AGGREGATIONS,
} from '../mock_data';
describe('Global Search Store Mutations', () => {
@@ -108,4 +109,17 @@ describe('Global Search Store Mutations', () => {
);
});
});
+
+ describe.each`
+ mutation | data | result
+ ${types.REQUEST_AGGREGATIONS} | ${[]} | ${{ fetching: true, error: false, data: [] }}
+ ${types.RECEIVE_AGGREGATIONS_SUCCESS} | ${MOCK_AGGREGATIONS} | ${{ fetching: false, error: false, data: MOCK_AGGREGATIONS }}
+ ${types.RECEIVE_AGGREGATIONS_ERROR} | ${[]} | ${{ fetching: false, error: true, data: [] }}
+ `('$mutation', ({ mutation, data, result }) => {
+ it('sets correct object content', () => {
+ mutations[mutation](state, data);
+
+ expect(state.aggregations).toStrictEqual(result);
+ });
+ });
});
diff --git a/spec/frontend/search_autocomplete_spec.js b/spec/frontend/search_autocomplete_spec.js
index 266f047e9dc..a3098fb81ea 100644
--- a/spec/frontend/search_autocomplete_spec.js
+++ b/spec/frontend/search_autocomplete_spec.js
@@ -5,6 +5,7 @@ import { mockTracking, unmockTracking } from 'helpers/tracking_helper';
import axios from '~/lib/utils/axios_utils';
import initSearchAutocomplete from '~/search_autocomplete';
import '~/lib/utils/common_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
describe('Search autocomplete dropdown', () => {
let widget = null;
@@ -191,7 +192,7 @@ describe('Search autocomplete dropdown', () => {
const axiosMock = new AxiosMockAdapter(axios);
const autocompleteUrl = new RegExp(autocompletePath);
- axiosMock.onGet(autocompleteUrl).reply(200, [
+ axiosMock.onGet(autocompleteUrl).reply(HTTP_STATUS_OK, [
{
category: 'Projects',
id: 1,
diff --git a/spec/frontend/self_monitor/store/actions_spec.js b/spec/frontend/self_monitor/store/actions_spec.js
index 4c266fabea6..0e28e330009 100644
--- a/spec/frontend/self_monitor/store/actions_spec.js
+++ b/spec/frontend/self_monitor/store/actions_spec.js
@@ -1,7 +1,11 @@
import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
-import { HTTP_STATUS_ACCEPTED, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import {
+ HTTP_STATUS_ACCEPTED,
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ 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';
@@ -91,7 +95,7 @@ describe('self-monitor actions', () => {
describe('error', () => {
beforeEach(() => {
state.createProjectEndpoint = '/create';
- mock.onPost(state.createProjectEndpoint).reply(500);
+ mock.onPost(state.createProjectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
@@ -198,7 +202,7 @@ describe('self-monitor actions', () => {
describe('error', () => {
beforeEach(() => {
state.deleteProjectEndpoint = '/delete';
- mock.onDelete(state.deleteProjectEndpoint).reply(500);
+ mock.onDelete(state.deleteProjectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches error', () => {
diff --git a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
index 4764f3607bc..60edab8766a 100644
--- a/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
+++ b/spec/frontend/sidebar/components/assignees/assignee_avatar_link_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { GlLink } from '@gitlab/ui';
import { TEST_HOST } from 'helpers/test_constants';
-import { TYPE_USER } from '~/graphql_shared/constants';
+import { TYPENAME_USER } from '~/graphql_shared/constants';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import AssigneeAvatar from '~/sidebar/components/assignees/assignee_avatar.vue';
import AssigneeAvatarLink from '~/sidebar/components/assignees/assignee_avatar_link.vue';
@@ -133,7 +133,7 @@ describe('AssigneeAvatarLink component', () => {
createComponent({
tooltipHasName: true,
issuableType: 'issue',
- user: { ...userDataMock(), id: convertToGraphQLId(TYPE_USER, userId) },
+ user: { ...userDataMock(), id: convertToGraphQLId(TYPENAME_USER, userId) },
});
expect(findUserLink().attributes('data-user-id')).toBe(String(userId));
diff --git a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
index 71424aaead3..be0b14fa997 100644
--- a/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
+++ b/spec/frontend/sidebar/components/assignees/sidebar_participant_spec.js
@@ -1,6 +1,6 @@
import { GlAvatarLabeled, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { IssuableType } from '~/issues/constants';
+import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
import SidebarParticipant from '~/sidebar/components/assignees/sidebar_participant.vue';
const user = {
@@ -16,11 +16,7 @@ describe('Sidebar participant component', () => {
const findAvatar = () => wrapper.findComponent(GlAvatarLabeled);
const findIcon = () => wrapper.findComponent(GlIcon);
- const createComponent = ({
- status = null,
- issuableType = IssuableType.Issue,
- canMerge = false,
- } = {}) => {
+ const createComponent = ({ status = null, issuableType = TYPE_ISSUE, canMerge = false } = {}) => {
wrapper = shallowMount(SidebarParticipant, {
propsData: {
user: {
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 40f14d581dc..c3de076d6aa 100644
--- a/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
+++ b/spec/frontend/sidebar/components/copy/sidebar_reference_widget_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
-import { IssuableType } from '~/issues/constants';
+import { IssuableType, TYPE_ISSUE } from '~/issues/constants';
import SidebarReferenceWidget from '~/sidebar/components/copy/sidebar_reference_widget.vue';
import issueReferenceQuery from '~/sidebar/queries/issue_reference.query.graphql';
import mergeRequestReferenceQuery from '~/sidebar/queries/merge_request_reference.query.graphql';
@@ -19,7 +19,7 @@ describe('Sidebar Reference Widget', () => {
const findCopyableField = () => wrapper.findComponent(CopyableField);
const createComponent = ({
- issuableType = IssuableType.Issue,
+ issuableType = TYPE_ISSUE,
referenceQuery = issueReferenceQuery,
referenceQueryHandler = jest.fn().mockResolvedValue(issueReferenceResponse(mockReferenceValue)),
} = {}) => {
@@ -51,7 +51,7 @@ describe('Sidebar Reference Widget', () => {
});
describe.each([
- [IssuableType.Issue, issueReferenceQuery],
+ [TYPE_ISSUE, issueReferenceQuery],
[IssuableType.MergeRequest, mergeRequestReferenceQuery],
])('when issuableType is %s', (issuableType, referenceQuery) => {
it('sets CopyableField `value` prop to reference value', async () => {
diff --git a/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
index 0e0024aa6c2..55651bccaa8 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_vue/store/actions_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/sidebar/components/labels/labels_select_vue/store/actions';
import * as types from '~/sidebar/components/labels/labels_select_vue/store/mutation_types';
import defaultState from '~/sidebar/components/labels/labels_select_vue/store/state';
@@ -121,7 +122,7 @@ describe('LabelsSelect Actions', () => {
describe('on success', () => {
it('dispatches `requestLabels` & `receiveLabelsSuccess` actions', () => {
const labels = [{ id: 1 }, { id: 2 }, { id: 3 }, { id: 4 }];
- mock.onGet(/labels.json/).replyOnce(200, labels);
+ mock.onGet(/labels.json/).replyOnce(HTTP_STATUS_OK, labels);
return testAction(
actions.fetchLabels,
@@ -135,7 +136,7 @@ describe('LabelsSelect Actions', () => {
describe('on failure', () => {
it('dispatches `requestLabels` & `receiveLabelsFailure` actions', () => {
- mock.onGet(/labels.json/).replyOnce(500, {});
+ mock.onGet(/labels.json/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
return testAction(
actions.fetchLabels,
@@ -205,7 +206,7 @@ describe('LabelsSelect Actions', () => {
describe('on success', () => {
it('dispatches `requestCreateLabel`, `fetchLabels` & `receiveCreateLabelSuccess` & `toggleDropdownContentsCreateView` actions', () => {
const label = { id: 1 };
- mock.onPost(/labels.json/).replyOnce(200, label);
+ mock.onPost(/labels.json/).replyOnce(HTTP_STATUS_OK, label);
return testAction(
actions.createLabel,
@@ -224,7 +225,7 @@ describe('LabelsSelect Actions', () => {
describe('on failure', () => {
it('dispatches `requestCreateLabel` & `receiveCreateLabelFailure` actions', () => {
- mock.onPost(/labels.json/).replyOnce(500, {});
+ mock.onPost(/labels.json/).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {});
return testAction(
actions.createLabel,
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
index 2995c268966..fd8e72bac49 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/labels_select_root_spec.js
@@ -4,7 +4,7 @@ import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import { IssuableType } from '~/issues/constants';
+import { IssuableType, TYPE_EPIC, TYPE_ISSUE } from '~/issues/constants';
import SidebarEditableItem from '~/sidebar/components/sidebar_editable_item.vue';
import DropdownContents from '~/sidebar/components/labels/labels_select_widget/dropdown_contents.vue';
import DropdownValue from '~/sidebar/components/labels/labels_select_widget/dropdown_value.vue';
@@ -14,6 +14,7 @@ import updateIssueLabelsMutation from '~/boards/graphql/issue_set_labels.mutatio
import updateMergeRequestLabelsMutation from '~/sidebar/queries/update_merge_request_labels.mutation.graphql';
import issuableLabelsSubscription from 'ee_else_ce/sidebar/queries/issuable_labels.subscription.graphql';
import updateEpicLabelsMutation from '~/sidebar/components/labels/labels_select_widget/graphql/epic_update_labels.mutation.graphql';
+import updateTestCaseLabelsMutation from '~/sidebar/components/labels/labels_select_widget/graphql/update_test_case_labels.mutation.graphql';
import LabelsSelectRoot from '~/sidebar/components/labels/labels_select_widget/labels_select_root.vue';
import {
mockConfig,
@@ -34,9 +35,10 @@ const subscriptionHandler = jest.fn().mockResolvedValue(issuableLabelsSubscripti
const errorQueryHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const updateLabelsMutation = {
- [IssuableType.Issue]: updateIssueLabelsMutation,
+ [TYPE_ISSUE]: updateIssueLabelsMutation,
[IssuableType.MergeRequest]: updateMergeRequestLabelsMutation,
- [IssuableType.Epic]: updateEpicLabelsMutation,
+ [TYPE_EPIC]: updateEpicLabelsMutation,
+ [IssuableType.TestCase]: updateTestCaseLabelsMutation,
};
describe('LabelsSelectRoot', () => {
@@ -50,7 +52,7 @@ describe('LabelsSelectRoot', () => {
const createComponent = ({
config = mockConfig,
slots = {},
- issuableType = IssuableType.Issue,
+ issuableType = TYPE_ISSUE,
queryHandler = successfulQueryHandler,
mutationHandler = successfulMutationHandler,
} = {}) => {
@@ -211,9 +213,10 @@ describe('LabelsSelectRoot', () => {
describe.each`
issuableType
- ${IssuableType.Issue}
+ ${TYPE_ISSUE}
${IssuableType.MergeRequest}
- ${IssuableType.Epic}
+ ${TYPE_EPIC}
+ ${IssuableType.TestCase}
`('when updating labels for $issuableType', ({ issuableType }) => {
const label = { id: 'gid://gitlab/ProjectLabel/2' };
@@ -228,6 +231,7 @@ describe('LabelsSelectRoot', () => {
it('updates labels correctly after successful mutation', async () => {
createComponent({ issuableType });
+
await nextTick();
findDropdownContents().vm.$emit('setLabels', [label]);
await waitForPromises();
diff --git a/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
index 48530a0261f..5d5a7e9a200 100644
--- a/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
+++ b/spec/frontend/sidebar/components/labels/labels_select_widget/mock_data.js
@@ -174,6 +174,15 @@ export const updateLabelsMutationResponse = {
updateIssuableLabels: {
errors: [],
issuable: {
+ updatedAt: '2023-02-10T22:26:49Z',
+ updatedBy: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl: 'avatar/url',
+ name: 'John Smith',
+ username: 'jsmith',
+ webUrl: 'http://gdk.test:3000/jsmith',
+ __typename: 'UserCore',
+ },
__typename: 'Issue',
id: '1',
labels: {
diff --git a/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
index 843ac1da4bb..b492753867b 100644
--- a/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/milestone/milestone_dropdown_spec.js
@@ -1,7 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import { nextTick } from 'vue';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { IssuableType, WorkspaceType } from '~/issues/constants';
+import { TYPE_ISSUE, WorkspaceType } from '~/issues/constants';
import { __ } from '~/locale';
import MilestoneDropdown from '~/sidebar/components/milestone/milestone_dropdown.vue';
import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
@@ -11,7 +11,7 @@ describe('MilestoneDropdown component', () => {
const propsData = {
attrWorkspacePath: 'full/path',
- issuableType: IssuableType.Issue,
+ issuableType: TYPE_ISSUE,
workspaceType: WorkspaceType.project,
};
diff --git a/spec/frontend/sidebar/components/move/move_issue_button_spec.js b/spec/frontend/sidebar/components/move/move_issue_button_spec.js
new file mode 100644
index 00000000000..acd6b23c1f5
--- /dev/null
+++ b/spec/frontend/sidebar/components/move/move_issue_button_spec.js
@@ -0,0 +1,157 @@
+import { shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { visitUrl } from '~/lib/utils/url_utility';
+import { createAlert } from '~/flash';
+import ProjectSelect from '~/sidebar/components/move/issuable_move_dropdown.vue';
+import MoveIssueButton from '~/sidebar/components/move/move_issue_button.vue';
+import moveIssueMutation from '~/sidebar/queries/move_issue.mutation.graphql';
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+jest.mock('~/lib/utils/url_utility', () => ({
+ visitUrl: jest.fn(),
+}));
+
+const projectFullPath = 'flight/FlightJS';
+const projectsAutocompleteEndpoint = '/-/autocomplete/projects?project_id=1';
+const issueIid = '15';
+
+const mockDestinationProject = {
+ full_path: 'gitlab-org/GitLabTest',
+};
+
+const mockWebUrl = `${mockDestinationProject.full_path}/issues/${issueIid}`;
+
+const mockMutationErrorMessage = 'Example error message';
+
+const resolvedMutationWithoutErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ issueMove: {
+ issue: {
+ id: issueIid,
+ webUrl: mockWebUrl,
+ },
+ errors: [],
+ },
+ },
+});
+
+const resolvedMutationWithErrorsMock = jest.fn().mockResolvedValue({
+ data: {
+ issueMove: {
+ errors: [{ message: mockMutationErrorMessage }],
+ },
+ },
+});
+
+const rejectedMutationMock = jest.fn().mockRejectedValue({});
+
+describe('MoveIssueButton', () => {
+ let wrapper;
+ let fakeApollo;
+
+ const findProjectSelect = () => wrapper.findComponent(ProjectSelect);
+ const emitProjectSelectEvent = () => {
+ findProjectSelect().vm.$emit('move-issuable', mockDestinationProject);
+ };
+ const createComponent = (mutationResolverMock = rejectedMutationMock) => {
+ fakeApollo = createMockApollo([[moveIssueMutation, mutationResolverMock]]);
+
+ wrapper = shallowMount(MoveIssueButton, {
+ provide: {
+ projectFullPath,
+ projectsAutocompleteEndpoint,
+ issueIid,
+ },
+ apolloProvider: fakeApollo,
+ });
+ };
+
+ afterEach(() => {
+ fakeApollo = null;
+ });
+
+ it('renders the project select dropdown', () => {
+ createComponent();
+
+ expect(findProjectSelect().props()).toMatchObject({
+ projectsFetchPath: projectsAutocompleteEndpoint,
+ dropdownButtonTitle: MoveIssueButton.i18n.title,
+ dropdownHeaderTitle: MoveIssueButton.i18n.title,
+ moveInProgress: false,
+ });
+ });
+
+ describe('when the project is selected', () => {
+ it('sets loading state and dropdown button text when issue is moving', async () => {
+ createComponent();
+ expect(findProjectSelect().props()).toMatchObject({
+ dropdownButtonTitle: MoveIssueButton.i18n.title,
+ moveInProgress: false,
+ });
+
+ emitProjectSelectEvent();
+ await nextTick();
+
+ expect(findProjectSelect().props()).toMatchObject({
+ dropdownButtonTitle: MoveIssueButton.i18n.titleInProgress,
+ moveInProgress: true,
+ });
+ });
+
+ it.each`
+ condition | mutation
+ ${'a mutation returns errors'} | ${resolvedMutationWithErrorsMock}
+ ${'a mutation is rejected'} | ${rejectedMutationMock}
+ `('sets loading state to false when $condition', async ({ mutation }) => {
+ createComponent(mutation);
+ emitProjectSelectEvent();
+
+ await nextTick();
+ expect(findProjectSelect().props('moveInProgress')).toBe(true);
+
+ await waitForPromises();
+ expect(findProjectSelect().props('moveInProgress')).toBe(false);
+ });
+
+ it('creates a flash and logs errors when a mutation returns errors', async () => {
+ createComponent(resolvedMutationWithErrorsMock);
+ emitProjectSelectEvent();
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: MoveIssueButton.i18n.moveErrorMessage,
+ captureError: true,
+ error: expect.any(Object),
+ });
+ });
+
+ it('calls a mutation for the selected issue', async () => {
+ createComponent(resolvedMutationWithoutErrorsMock);
+ emitProjectSelectEvent();
+
+ await waitForPromises();
+
+ expect(resolvedMutationWithoutErrorsMock).toHaveBeenCalledWith({
+ moveIssueInput: {
+ projectPath: projectFullPath,
+ iid: issueIid,
+ targetProjectPath: mockDestinationProject.full_path,
+ },
+ });
+ });
+
+ it('redirects to the correct page when the mutation succeeds', async () => {
+ createComponent(resolvedMutationWithoutErrorsMock);
+ emitProjectSelectEvent();
+ await waitForPromises();
+
+ expect(visitUrl).toHaveBeenCalledWith(mockWebUrl);
+ });
+ });
+});
diff --git a/spec/frontend/sidebar/components/move/move_issues_button_spec.js b/spec/frontend/sidebar/components/move/move_issues_button_spec.js
index 999340da27c..c65bad642a0 100644
--- a/spec/frontend/sidebar/components/move/move_issues_button_spec.js
+++ b/spec/frontend/sidebar/components/move/move_issues_button_spec.js
@@ -75,9 +75,15 @@ if (IS_EE) {
getIssuesQueryCompleteResponse.data.project.issues.nodes[0].weight = 5;
}
+const mockIssueResult = {
+ id: mockIssue.iid,
+ webUrl: `${mockDestinationProject.full_path}/issues/${mockIssue.iid}`,
+};
+
const resolvedMutationWithoutErrorsMock = jest.fn().mockResolvedValue({
data: {
issueMove: {
+ issue: mockIssueResult,
errors: [],
},
},
@@ -86,6 +92,7 @@ const resolvedMutationWithoutErrorsMock = jest.fn().mockResolvedValue({
const resolvedMutationWithErrorsMock = jest.fn().mockResolvedValue({
data: {
issueMove: {
+ issue: mockIssueResult,
errors: [{ message: mockMutationErrorMessage }],
},
},
diff --git a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
index 8f936240b7a..71c6c259c32 100644
--- a/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
+++ b/spec/frontend/sidebar/components/severity/sidebar_severity_spec.js
@@ -1,12 +1,12 @@
import { GlDropdown, GlDropdownItem, GlLoadingIcon, GlTooltip, GlSprintf } from '@gitlab/ui';
import { nextTick } from 'vue';
-import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { INCIDENT_SEVERITY, ISSUABLE_TYPES } from '~/sidebar/constants';
import updateIssuableSeverity from '~/sidebar/queries/update_issuable_severity.mutation.graphql';
import SeverityToken from '~/sidebar/components/severity/severity.vue';
-import SidebarSeverity from '~/sidebar/components/severity/sidebar_severity.vue';
+import SidebarSeverityWidget from '~/sidebar/components/severity/sidebar_severity_widget.vue';
jest.mock('~/flash');
@@ -27,7 +27,7 @@ describe('SidebarSeverity', () => {
...props,
};
mutate = jest.fn();
- wrapper = shallowMountExtended(SidebarSeverity, {
+ wrapper = mountExtended(SidebarSeverityWidget, {
propsData,
provide: {
canUpdate,
@@ -48,13 +48,11 @@ describe('SidebarSeverity', () => {
});
afterEach(() => {
- if (wrapper) {
- wrapper.destroy();
- }
+ wrapper.destroy();
});
const findSeverityToken = () => wrapper.findAllComponents(SeverityToken);
- const findEditBtn = () => wrapper.findByTestId('editButton');
+ const findEditBtn = () => wrapper.findByTestId('edit-button');
const findDropdown = () => wrapper.findComponent(GlDropdown);
const findCriticalSeverityDropdownItem = () => wrapper.findComponent(GlDropdownItem);
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
@@ -127,30 +125,15 @@ describe('SidebarSeverity', () => {
});
describe('Switch between collapsed/expanded view of the sidebar', () => {
- const HIDDDEN_CLASS = 'gl-display-none';
- const SHOWN_CLASS = 'show';
-
describe('collapsed', () => {
it('should have collapsed icon class', () => {
expect(findCollapsedSeverity().classes('sidebar-collapsed-icon')).toBe(true);
});
it('should display only icon with a tooltip', () => {
- expect(findSeverityToken().at(0).attributes('icononly')).toBe('true');
- expect(findSeverityToken().at(0).attributes('iconsize')).toBe('14');
- expect(findTooltip().text().replace(/\s+/g, ' ')).toContain(
- `Severity: ${INCIDENT_SEVERITY[severity].label}`,
- );
- });
-
- it('should expand the dropdown on collapsed icon click', async () => {
- wrapper.vm.isDropdownShowing = false;
- await nextTick();
- expect(findDropdown().classes(HIDDDEN_CLASS)).toBe(true);
-
- findCollapsedSeverity().trigger('click');
- await nextTick();
- expect(findDropdown().classes(SHOWN_CLASS)).toBe(true);
+ expect(findSeverityToken().exists()).toBe(true);
+ expect(findTooltip().text()).toContain(INCIDENT_SEVERITY[severity].label);
+ expect(findEditBtn().exists()).toBe(false);
});
});
@@ -158,17 +141,16 @@ describe('SidebarSeverity', () => {
it('toggles dropdown with edit button', async () => {
canUpdate = true;
createComponent();
- wrapper.vm.isDropdownShowing = false;
await nextTick();
- expect(findDropdown().classes(HIDDDEN_CLASS)).toBe(true);
+ expect(findDropdown().isVisible()).toBe(false);
findEditBtn().vm.$emit('click');
await nextTick();
- expect(findDropdown().classes(SHOWN_CLASS)).toBe(true);
+ expect(findDropdown().isVisible()).toBe(true);
findEditBtn().vm.$emit('click');
await nextTick();
- expect(findDropdown().classes(HIDDDEN_CLASS)).toBe(true);
+ expect(findDropdown().isVisible()).toBe(false);
});
});
});
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
index 83bc8cf7002..9f3d689edee 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_spec.js
@@ -11,7 +11,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import { IssuableType } from '~/issues/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
import { IssuableAttributeType } from '~/sidebar/constants';
import projectIssueMilestoneQuery from '~/sidebar/queries/project_issue_milestone.query.graphql';
@@ -66,7 +66,7 @@ describe('SidebarDropdown component', () => {
propsData: {
attrWorkspacePath: mockIssue.projectPath,
currentAttribute: {},
- issuableType: IssuableType.Issue,
+ issuableType: TYPE_ISSUE,
issuableAttribute: IssuableAttributeType.Milestone,
},
attachTo: document.body,
@@ -83,7 +83,7 @@ describe('SidebarDropdown component', () => {
propsData: {
attrWorkspacePath: mockIssue.projectPath,
currentAttribute: {},
- issuableType: IssuableType.Issue,
+ issuableType: TYPE_ISSUE,
issuableAttribute: IssuableAttributeType.Milestone,
...props,
},
diff --git a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
index cf5e220a705..060a2873e04 100644
--- a/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
+++ b/spec/frontend/sidebar/components/sidebar_dropdown_widget_spec.js
@@ -9,7 +9,7 @@ import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
import { getIdFromGraphQLId } from '~/graphql_shared/utils';
-import { IssuableType } from '~/issues/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import { timeFor } from '~/lib/utils/datetime_utility';
import SidebarDropdown from '~/sidebar/components/sidebar_dropdown.vue';
import SidebarDropdownWidget from '~/sidebar/components/sidebar_dropdown_widget.vue';
@@ -105,7 +105,7 @@ describe('SidebarDropdownWidget', () => {
workspacePath: mockIssue.projectPath,
attrWorkspacePath: mockIssue.projectPath,
iid: mockIssue.iid,
- issuableType: IssuableType.Issue,
+ issuableType: TYPE_ISSUE,
issuableAttribute: IssuableAttributeType.Milestone,
},
attachTo: document.body,
@@ -126,7 +126,7 @@ describe('SidebarDropdownWidget', () => {
workspacePath: '',
attrWorkspacePath: '',
iid: '',
- issuableType: IssuableType.Issue,
+ issuableType: TYPE_ISSUE,
issuableAttribute: IssuableAttributeType.Milestone,
},
mocks: {
diff --git a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
index cb3bb7a4538..715f66d305a 100644
--- a/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
+++ b/spec/frontend/sidebar/components/time_tracking/create_timelog_form_spec.js
@@ -7,7 +7,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { convertToGraphQLId } from '~/graphql_shared/utils';
import CreateTimelogForm from '~/sidebar/components/time_tracking/create_timelog_form.vue';
import createTimelogMutation from '~/sidebar/queries/create_timelog.mutation.graphql';
-import { TYPE_ISSUE, TYPE_MERGE_REQUEST } from '~/graphql_shared/constants';
+import { TYPENAME_ISSUE, TYPENAME_MERGE_REQUEST } from '~/graphql_shared/constants';
const mockMutationErrorMessage = 'Example error message';
@@ -157,8 +157,8 @@ describe('Create Timelog Form', () => {
it.each`
issuableType | typeConstant
- ${'issue'} | ${TYPE_ISSUE}
- ${'merge_request'} | ${TYPE_MERGE_REQUEST}
+ ${'issue'} | ${TYPENAME_ISSUE}
+ ${'merge_request'} | ${TYPENAME_MERGE_REQUEST}
`(
'calls the mutation with all the fields when the the form is submitted and issuable type is $issuableType',
async ({ issuableType, typeConstant }) => {
diff --git a/spec/frontend/sidebar/lib/sidebar_move_issue_spec.js b/spec/frontend/sidebar/lib/sidebar_move_issue_spec.js
deleted file mode 100644
index 6e365df329b..00000000000
--- a/spec/frontend/sidebar/lib/sidebar_move_issue_spec.js
+++ /dev/null
@@ -1,162 +0,0 @@
-import MockAdapter from 'axios-mock-adapter';
-import $ from 'jquery';
-import waitForPromises from 'helpers/wait_for_promises';
-import { createAlert } from '~/flash';
-import axios from '~/lib/utils/axios_utils';
-import SidebarMoveIssue from '~/sidebar/lib/sidebar_move_issue';
-import SidebarService from '~/sidebar/services/sidebar_service';
-import SidebarMediator from '~/sidebar/sidebar_mediator';
-import SidebarStore from '~/sidebar/stores/sidebar_store';
-import { GitLabDropdown } from '~/deprecated_jquery_dropdown/gl_dropdown';
-import Mock from '../mock_data';
-
-jest.mock('~/flash');
-
-describe('SidebarMoveIssue', () => {
- let mock;
- const test = {};
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- const mockData = Mock.responseMap.GET['/autocomplete/projects?project_id=15'];
- mock.onGet('/autocomplete/projects?project_id=15').reply(200, mockData);
- test.mediator = new SidebarMediator(Mock.mediator);
- test.$content = $(`
- <div class="dropdown">
- <div class="js-toggle"></div>
- <div class="dropdown-menu">
- <div class="dropdown-content"></div>
- </div>
- <div class="js-confirm-button"></div>
- </div>
- `);
- test.$toggleButton = test.$content.find('.js-toggle');
- test.$confirmButton = test.$content.find('.js-confirm-button');
-
- test.sidebarMoveIssue = new SidebarMoveIssue(
- test.mediator,
- test.$toggleButton,
- test.$confirmButton,
- );
- test.sidebarMoveIssue.init();
- });
-
- afterEach(() => {
- SidebarService.singleton = null;
- SidebarStore.singleton = null;
- SidebarMediator.singleton = null;
-
- test.sidebarMoveIssue.destroy();
- mock.restore();
- });
-
- describe('init', () => {
- it('should initialize the dropdown and listeners', () => {
- jest.spyOn(test.sidebarMoveIssue, 'initDropdown').mockImplementation(() => {});
- jest.spyOn(test.sidebarMoveIssue, 'addEventListeners').mockImplementation(() => {});
-
- test.sidebarMoveIssue.init();
-
- expect(test.sidebarMoveIssue.initDropdown).toHaveBeenCalled();
- expect(test.sidebarMoveIssue.addEventListeners).toHaveBeenCalled();
- });
- });
-
- describe('destroy', () => {
- it('should remove the listeners', () => {
- jest.spyOn(test.sidebarMoveIssue, 'removeEventListeners').mockImplementation(() => {});
-
- test.sidebarMoveIssue.destroy();
-
- expect(test.sidebarMoveIssue.removeEventListeners).toHaveBeenCalled();
- });
- });
-
- describe('initDropdown', () => {
- it('should initialize the deprecatedJQueryDropdown', () => {
- test.sidebarMoveIssue.initDropdown();
-
- expect(test.sidebarMoveIssue.$dropdownToggle.data('deprecatedJQueryDropdown')).toBeInstanceOf(
- GitLabDropdown,
- );
- });
-
- it('escapes html from project name', async () => {
- test.$toggleButton.dropdown('toggle');
-
- await waitForPromises();
-
- expect(test.$content.find('.js-move-issue-dropdown-item')[1].innerHTML.trim()).toEqual(
- '&lt;img src=x onerror=alert(document.domain)&gt; foo / bar',
- );
- });
- });
-
- describe('onConfirmClicked', () => {
- it('should move the issue with valid project ID', () => {
- jest.spyOn(test.mediator, 'moveIssue').mockReturnValue(Promise.resolve());
- test.mediator.setMoveToProjectId(7);
-
- test.sidebarMoveIssue.onConfirmClicked();
-
- expect(test.mediator.moveIssue).toHaveBeenCalled();
- expect(test.$confirmButton.prop('disabled')).toBe(true);
- expect(test.$confirmButton.hasClass('is-loading')).toBe(true);
- });
-
- it('should remove loading state from confirm button on failure', async () => {
- jest.spyOn(test.mediator, 'moveIssue').mockReturnValue(Promise.reject());
- test.mediator.setMoveToProjectId(7);
-
- test.sidebarMoveIssue.onConfirmClicked();
-
- expect(test.mediator.moveIssue).toHaveBeenCalled();
-
- // Wait for the move issue request to fail
- await waitForPromises();
-
- expect(createAlert).toHaveBeenCalled();
- expect(test.$confirmButton.prop('disabled')).toBe(false);
- expect(test.$confirmButton.hasClass('is-loading')).toBe(false);
- });
-
- it('should not move the issue with id=0', () => {
- jest.spyOn(test.mediator, 'moveIssue').mockImplementation(() => {});
- test.mediator.setMoveToProjectId(0);
-
- test.sidebarMoveIssue.onConfirmClicked();
-
- expect(test.mediator.moveIssue).not.toHaveBeenCalled();
- });
- });
-
- it('should set moveToProjectId on dropdown item "No project" click', async () => {
- jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {});
-
- // Open the dropdown
- test.$toggleButton.dropdown('toggle');
-
- // Wait for the autocomplete request to finish
- await waitForPromises();
-
- test.$content.find('.js-move-issue-dropdown-item').eq(0).trigger('click');
-
- expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(0);
- expect(test.$confirmButton.prop('disabled')).toBe(true);
- });
-
- it('should set moveToProjectId on dropdown item click', async () => {
- jest.spyOn(test.mediator, 'setMoveToProjectId').mockImplementation(() => {});
-
- // Open the dropdown
- test.$toggleButton.dropdown('toggle');
-
- // Wait for the autocomplete request to finish
- await waitForPromises();
-
- test.$content.find('.js-move-issue-dropdown-item').eq(1).trigger('click');
-
- expect(test.mediator.setMoveToProjectId).toHaveBeenCalledWith(20);
- expect(test.$confirmButton.attr('disabled')).toBe(undefined);
- });
-});
diff --git a/spec/frontend/sidebar/sidebar_mediator_spec.js b/spec/frontend/sidebar/sidebar_mediator_spec.js
index cdb9ced70b8..77b1ccb4f9a 100644
--- a/spec/frontend/sidebar/sidebar_mediator_spec.js
+++ b/spec/frontend/sidebar/sidebar_mediator_spec.js
@@ -1,5 +1,6 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as urlUtility from '~/lib/utils/url_utility';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarMediator from '~/sidebar/sidebar_mediator';
@@ -36,10 +37,10 @@ describe('Sidebar mediator', () => {
});
it('saves assignees', () => {
- mock.onPut(mediatorMockData.endpoint).reply(200, {});
+ mock.onPut(mediatorMockData.endpoint).reply(HTTP_STATUS_OK, {});
return mediator.saveAssignees('issue[assignee_ids]').then((resp) => {
- expect(resp.status).toEqual(200);
+ expect(resp.status).toEqual(HTTP_STATUS_OK);
});
});
@@ -91,7 +92,7 @@ describe('Sidebar mediator', () => {
it('fetches the data', async () => {
const mockData = Mock.responseMap.GET[mediatorMockData.endpoint];
- mock.onGet(mediatorMockData.endpoint).reply(200, mockData);
+ mock.onGet(mediatorMockData.endpoint).reply(HTTP_STATUS_OK, mockData);
const spy = jest.spyOn(mediator, 'processFetchedData').mockReturnValue(Promise.resolve());
await mediator.fetch();
@@ -120,7 +121,7 @@ describe('Sidebar mediator', () => {
it('fetches autocomplete projects', () => {
const searchTerm = 'foo';
- mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(200, {});
+ mock.onGet(mediatorMockData.projectsAutocompleteEndpoint).reply(HTTP_STATUS_OK, {});
const getterSpy = jest
.spyOn(mediator.service, 'getProjectsAutocomplete')
.mockReturnValue(Promise.resolve({ data: {} }));
@@ -137,7 +138,7 @@ describe('Sidebar mediator', () => {
it('moves issue', () => {
const mockData = Mock.responseMap.POST[mediatorMockData.moveIssueEndpoint];
const moveToProjectId = 7;
- mock.onPost(mediatorMockData.moveIssueEndpoint).reply(200, mockData);
+ mock.onPost(mediatorMockData.moveIssueEndpoint).reply(HTTP_STATUS_OK, mockData);
mediator.store.setMoveToProjectId(moveToProjectId);
const moveIssueSpy = jest
.spyOn(mediator.service, 'moveIssue')
diff --git a/spec/frontend/single_file_diff_spec.js b/spec/frontend/single_file_diff_spec.js
index 6f42ec47458..ff2a4e31e0b 100644
--- a/spec/frontend/single_file_diff_spec.js
+++ b/spec/frontend/single_file_diff_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import $ from 'jquery';
import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_BAD_REQUEST, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import SingleFileDiff from '~/single_file_diff';
describe('SingleFileDiff', () => {
@@ -10,7 +11,9 @@ describe('SingleFileDiff', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(blobDiffPath).replyOnce(200, { html: `<div class="diff-content">MOCKED</div>` });
+ mock
+ .onGet(blobDiffPath)
+ .replyOnce(HTTP_STATUS_OK, { html: `<div class="diff-content">MOCKED</div>` });
});
afterEach(() => {
@@ -54,7 +57,7 @@ describe('SingleFileDiff', () => {
expect(diff.isOpen).toBe(false);
expect(diff.content).not.toBeNull();
- mock.onGet(blobDiffPath).replyOnce(400, '');
+ mock.onGet(blobDiffPath).replyOnce(HTTP_STATUS_BAD_REQUEST, '');
// Opening again
await diff.toggleDiff($(document.querySelector('.js-file-title')));
diff --git a/spec/frontend/snippets/components/snippet_blob_edit_spec.js b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
index 33b8e2be969..82c4a37ccc9 100644
--- a/spec/frontend/snippets/components/snippet_blob_edit_spec.js
+++ b/spec/frontend/snippets/components/snippet_blob_edit_spec.js
@@ -6,6 +6,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import BlobHeaderEdit from '~/blob/components/blob_edit_header.vue';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { joinPaths } from '~/lib/utils/url_utility';
import SnippetBlobEdit from '~/snippets/components/snippet_blob_edit.vue';
import SourceEditor from '~/vue_shared/components/source_editor.vue';
@@ -57,7 +58,7 @@ describe('Snippet Blob Edit component', () => {
beforeEach(() => {
axiosMock = new AxiosMockAdapter(axios);
- axiosMock.onGet(TEST_FULL_PATH).reply(200, TEST_CONTENT);
+ axiosMock.onGet(TEST_FULL_PATH).reply(HTTP_STATUS_OK, TEST_CONTENT);
});
afterEach(() => {
@@ -103,7 +104,7 @@ describe('Snippet Blob Edit component', () => {
describe('with unloaded blob and JSON content', () => {
beforeEach(() => {
- axiosMock.onGet(TEST_FULL_PATH).reply(200, TEST_JSON_CONTENT);
+ axiosMock.onGet(TEST_FULL_PATH).reply(HTTP_STATUS_OK, TEST_JSON_CONTENT);
createComponent();
});
@@ -118,7 +119,7 @@ describe('Snippet Blob Edit component', () => {
describe('with error', () => {
beforeEach(() => {
axiosMock.reset();
- axiosMock.onGet(TEST_FULL_PATH).replyOnce(500);
+ axiosMock.onGet(TEST_FULL_PATH).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponent();
});
diff --git a/spec/frontend/super_sidebar/components/counter_spec.js b/spec/frontend/super_sidebar/components/counter_spec.js
index 1150b0a3aa8..8f514540413 100644
--- a/spec/frontend/super_sidebar/components/counter_spec.js
+++ b/spec/frontend/super_sidebar/components/counter_spec.js
@@ -13,10 +13,6 @@ describe('Counter component', () => {
label: __('Issues'),
};
- afterEach(() => {
- wrapper.destroy();
- });
-
const findButton = () => wrapper.find('button');
const findIcon = () => wrapper.getComponent(GlIcon);
const findLink = () => wrapper.find('a');
diff --git a/spec/frontend/super_sidebar/components/create_menu_spec.js b/spec/frontend/super_sidebar/components/create_menu_spec.js
new file mode 100644
index 00000000000..b24c6b8de7f
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/create_menu_spec.js
@@ -0,0 +1,39 @@
+import { GlDisclosureDropdown, GlTooltip } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { __ } from '~/locale';
+import CreateMenu from '~/super_sidebar/components/create_menu.vue';
+import { createNewMenuGroups } from '../mock_data';
+
+describe('CreateMenu component', () => {
+ let wrapper;
+
+ const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findGlTooltip = () => wrapper.findComponent(GlTooltip);
+
+ const createWrapper = () => {
+ wrapper = shallowMountExtended(CreateMenu, {
+ propsData: {
+ groups: createNewMenuGroups,
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it("sets the toggle's label", () => {
+ expect(findGlDisclosureDropdown().props('toggleText')).toBe(__('Create new...'));
+ });
+
+ it('passes the groups to the disclosure dropdown', () => {
+ expect(findGlDisclosureDropdown().props('items')).toBe(createNewMenuGroups);
+ });
+
+ it("sets the toggle ID and tooltip's target", () => {
+ expect(findGlDisclosureDropdown().props('toggleId')).toBe(wrapper.vm.$options.toggleId);
+ expect(findGlTooltip().props('target')).toBe(`#${wrapper.vm.$options.toggleId}`);
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/help_center_spec.js b/spec/frontend/super_sidebar/components/help_center_spec.js
new file mode 100644
index 00000000000..bc847a3e159
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/help_center_spec.js
@@ -0,0 +1,152 @@
+import { GlDisclosureDropdownGroup } from '@gitlab/ui';
+import { within } from '@testing-library/dom';
+import toggleWhatsNewDrawer from '~/whats_new';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import HelpCenter from '~/super_sidebar/components/help_center.vue';
+import { helpPagePath } from '~/helpers/help_page_helper';
+import { PROMO_URL } from 'jh_else_ce/lib/utils/url_utility';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import { STORAGE_KEY } from '~/whats_new/utils/notification';
+import { sidebarData } from '../mock_data';
+
+jest.mock('~/whats_new');
+
+describe('HelpCenter component', () => {
+ let wrapper;
+
+ const GlEmoji = { template: '<img/>' };
+
+ const findDropdownGroup = (i = 0) => {
+ return wrapper.findAllComponents(GlDisclosureDropdownGroup).at(i);
+ };
+ const withinComponent = () => within(wrapper.element);
+ const findButton = (name) => withinComponent().getByRole('button', { name });
+
+ // eslint-disable-next-line no-shadow
+ const createWrapper = (sidebarData) => {
+ wrapper = mountExtended(HelpCenter, {
+ propsData: { sidebarData },
+ stubs: { GlEmoji },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper(sidebarData);
+ });
+
+ it('renders menu items', () => {
+ expect(findDropdownGroup(0).props('group').items).toEqual([
+ { text: HelpCenter.i18n.help, href: helpPagePath() },
+ { text: HelpCenter.i18n.support, href: sidebarData.support_path },
+ { text: HelpCenter.i18n.docs, href: 'https://docs.gitlab.com' },
+ { text: HelpCenter.i18n.plans, href: `${PROMO_URL}/pricing` },
+ { text: HelpCenter.i18n.forum, href: 'https://forum.gitlab.com/' },
+ {
+ text: HelpCenter.i18n.contribute,
+ href: helpPagePath('', { anchor: 'contributing-to-gitlab' }),
+ },
+ { text: HelpCenter.i18n.feedback, href: 'https://about.gitlab.com/submit-feedback' },
+ ]);
+
+ expect(findDropdownGroup(1).props('group').items).toEqual([
+ expect.objectContaining({ text: HelpCenter.i18n.shortcuts }),
+ expect.objectContaining({ text: HelpCenter.i18n.whatsnew }),
+ ]);
+ });
+
+ describe('with Gitlab version check feature enabled', () => {
+ beforeEach(() => {
+ createWrapper({ ...sidebarData, show_version_check: true });
+ });
+
+ it('shows version information as first item', () => {
+ expect(findDropdownGroup(0).props('group').items).toEqual([
+ { text: HelpCenter.i18n.version, href: helpPagePath('update/index'), version: '16.0' },
+ ]);
+ });
+ });
+
+ describe('showKeyboardShortcuts', () => {
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm.$refs.dropdown, 'close');
+ window.toggleShortcutsHelp = jest.fn();
+ findButton('Keyboard shortcuts ?').click();
+ });
+
+ it('closes the dropdown', () => {
+ expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled();
+ });
+
+ it('shows the keyboard shortcuts modal', () => {
+ expect(window.toggleShortcutsHelp).toHaveBeenCalled();
+ });
+ });
+
+ describe('showWhatsNew', () => {
+ beforeEach(() => {
+ jest.spyOn(wrapper.vm.$refs.dropdown, 'close');
+ findButton("What's new 5").click();
+ });
+
+ it('closes the dropdown', () => {
+ expect(wrapper.vm.$refs.dropdown.close).toHaveBeenCalled();
+ });
+
+ it('shows the "What\'s new" slideout', () => {
+ expect(toggleWhatsNewDrawer).toHaveBeenCalledWith(expect.any(Object));
+ });
+
+ it('shows the existing "What\'s new" slideout instance on subsequent clicks', () => {
+ findButton("What's new").click();
+ expect(toggleWhatsNewDrawer).toHaveBeenCalledTimes(2);
+ expect(toggleWhatsNewDrawer).toHaveBeenLastCalledWith();
+ });
+ });
+
+ describe('shouldShowWhatsNewNotification', () => {
+ describe('when setting is disabled', () => {
+ beforeEach(() => {
+ createWrapper({ ...sidebarData, display_whats_new: false });
+ });
+
+ it('is false', () => {
+ expect(wrapper.vm.showWhatsNewNotification).toBe(false);
+ });
+ });
+
+ describe('when setting is enabled', () => {
+ useLocalStorageSpy();
+
+ beforeEach(() => {
+ createWrapper({ ...sidebarData, display_whats_new: true });
+ });
+
+ it('is true', () => {
+ expect(wrapper.vm.showWhatsNewNotification).toBe(true);
+ });
+
+ describe('when "What\'s new" drawer got opened', () => {
+ beforeEach(() => {
+ findButton("What's new 5").click();
+ });
+
+ it('is false', () => {
+ expect(wrapper.vm.showWhatsNewNotification).toBe(false);
+ });
+ });
+
+ describe('with matching version digest in local storage', () => {
+ beforeEach(() => {
+ window.localStorage.setItem(STORAGE_KEY, 1);
+ createWrapper({ ...sidebarData, display_whats_new: true });
+ });
+
+ it('is false', () => {
+ expect(wrapper.vm.showWhatsNewNotification).toBe(false);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/merge_request_menu_spec.js b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
new file mode 100644
index 00000000000..fe87c4be9c3
--- /dev/null
+++ b/spec/frontend/super_sidebar/components/merge_request_menu_spec.js
@@ -0,0 +1,46 @@
+import { GlBadge, GlDisclosureDropdown } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue';
+import { mergeRequestMenuGroup } from '../mock_data';
+
+describe('MergeRequestMenu component', () => {
+ let wrapper;
+
+ const findGlBadge = (at) => wrapper.findAllComponents(GlBadge).at(at);
+ const findGlDisclosureDropdown = () => wrapper.findComponent(GlDisclosureDropdown);
+ const findLink = () => wrapper.findByRole('link');
+
+ const createWrapper = () => {
+ wrapper = mountExtended(MergeRequestMenu, {
+ propsData: {
+ items: mergeRequestMenuGroup,
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createWrapper();
+ });
+
+ it('passes the items to the disclosure dropdown', () => {
+ expect(findGlDisclosureDropdown().props('items')).toBe(mergeRequestMenuGroup);
+ });
+
+ it('renders item text and count in link', () => {
+ const { text, href, count } = mergeRequestMenuGroup[0].items[0];
+ expect(findLink().text()).toContain(text);
+ expect(findLink().text()).toContain(String(count));
+ expect(findLink().attributes('href')).toBe(href);
+ });
+
+ it('renders item count string in badge', () => {
+ const { count } = mergeRequestMenuGroup[0].items[0];
+ expect(findGlBadge(0).text()).toBe(String(count));
+ });
+
+ it('renders 0 string when count is empty', () => {
+ expect(findGlBadge(1).text()).toBe(String(0));
+ });
+ });
+});
diff --git a/spec/frontend/super_sidebar/components/super_sidebar_spec.js b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
index d7d2f67dc8a..45fc30c08f0 100644
--- a/spec/frontend/super_sidebar/components/super_sidebar_spec.js
+++ b/spec/frontend/super_sidebar/components/super_sidebar_spec.js
@@ -1,5 +1,6 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SuperSidebar from '~/super_sidebar/components/super_sidebar.vue';
+import HelpCenter from '~/super_sidebar/components/help_center.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
import { sidebarData } from '../mock_data';
@@ -7,10 +8,7 @@ describe('SuperSidebar component', () => {
let wrapper;
const findUserBar = () => wrapper.findComponent(UserBar);
-
- afterEach(() => {
- wrapper.destroy();
- });
+ const findHelpCenter = () => wrapper.findComponent(HelpCenter);
const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(SuperSidebar, {
@@ -29,5 +27,9 @@ describe('SuperSidebar component', () => {
it('renders UserBar with sidebarData', () => {
expect(findUserBar().props('sidebarData')).toBe(sidebarData);
});
+
+ it('renders HelpCenter with sidebarData', () => {
+ expect(findHelpCenter().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
index 6d0186a2749..eceb792c3db 100644
--- a/spec/frontend/super_sidebar/components/user_bar_spec.js
+++ b/spec/frontend/super_sidebar/components/user_bar_spec.js
@@ -1,5 +1,7 @@
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { __ } from '~/locale';
+import CreateMenu from '~/super_sidebar/components/create_menu.vue';
+import MergeRequestMenu from '~/super_sidebar/components/merge_request_menu.vue';
import Counter from '~/super_sidebar/components/counter.vue';
import UserBar from '~/super_sidebar/components/user_bar.vue';
import { sidebarData } from '../mock_data';
@@ -7,11 +9,9 @@ import { sidebarData } from '../mock_data';
describe('UserBar component', () => {
let wrapper;
+ const findCreateMenu = () => wrapper.findComponent(CreateMenu);
const findCounter = (at) => wrapper.findAllComponents(Counter).at(at);
-
- afterEach(() => {
- wrapper.destroy();
- });
+ const findMergeRequestMenu = () => wrapper.findComponent(MergeRequestMenu);
const createWrapper = (props = {}) => {
wrapper = shallowMountExtended(UserBar, {
@@ -31,12 +31,25 @@ describe('UserBar component', () => {
createWrapper();
});
+ it('passes the "Create new..." menu groups to the create-menu component', () => {
+ expect(findCreateMenu().props('groups')).toBe(sidebarData.create_new_menu_groups);
+ });
+
+ it('passes the "Merge request" menu groups to the merge_request_menu component', () => {
+ expect(findMergeRequestMenu().props('items')).toBe(sidebarData.merge_request_menu);
+ });
+
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 merge requests counter', () => {
+ expect(findCounter(1).props('count')).toBe(sidebarData.total_merge_requests_count);
+ expect(findCounter(1).props('label')).toBe(__('Merge requests'));
+ });
+
it('renders todos counter', () => {
expect(findCounter(2).props('count')).toBe(sidebarData.todos_pending_count);
expect(findCounter(2).props('href')).toBe('/dashboard/todos');
diff --git a/spec/frontend/super_sidebar/mock_data.js b/spec/frontend/super_sidebar/mock_data.js
index 7db0d0ea5cc..91a2dc93a47 100644
--- a/spec/frontend/super_sidebar/mock_data.js
+++ b/spec/frontend/super_sidebar/mock_data.js
@@ -1,9 +1,77 @@
+export const createNewMenuGroups = [
+ {
+ name: 'This group',
+ items: [
+ {
+ text: 'New project/repository',
+ href: '/projects/new?namespace_id=22',
+ },
+ {
+ text: 'New subgroup',
+ href: '/groups/new?parent_id=22#create-group-pane',
+ },
+ {
+ text: 'New epic',
+ href: '/groups/gitlab-org/-/epics/new',
+ },
+ {
+ text: 'Invite members',
+ href: '/groups/gitlab-org/-/group_members',
+ },
+ ],
+ },
+ {
+ name: 'GitLab',
+ items: [
+ {
+ text: 'New project/repository',
+ href: '/projects/new',
+ },
+ {
+ text: 'New group',
+ href: '/groups/new',
+ },
+ {
+ text: 'New snippet',
+ href: '/-/snippets/new',
+ },
+ ],
+ },
+];
+
+export const mergeRequestMenuGroup = [
+ {
+ name: 'Merge requests',
+ items: [
+ {
+ text: 'Assigned',
+ href: '/dashboard/merge_requests?assignee_username=root',
+ count: 4,
+ },
+ {
+ text: 'Review requests',
+ href: '/dashboard/merge_requests?reviewer_username=root',
+ count: 0,
+ },
+ ],
+ },
+];
+
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',
+ total_merge_requests_count: 4,
+ create_new_menu_groups: createNewMenuGroups,
+ merge_request_menu: mergeRequestMenuGroup,
+ support_path: '/support',
+ display_whats_new: true,
+ whats_new_most_recent_release_items_count: 5,
+ whats_new_version_digest: 1,
+ show_version_check: false,
+ gitlab_version: { major: 16, minor: 0 },
+ gitlab_version_check: { severity: 'success' },
};
diff --git a/spec/frontend/terms/components/app_spec.js b/spec/frontend/terms/components/app_spec.js
index ce1c126f868..99f61a31dbd 100644
--- a/spec/frontend/terms/components/app_spec.js
+++ b/spec/frontend/terms/components/app_spec.js
@@ -3,7 +3,6 @@ import { GlIntersectionObserver } from '@gitlab/ui';
import { nextTick } from 'vue';
import { mountExtended } from 'helpers/vue_test_utils_helper';
-import { FLASH_TYPES, FLASH_CLOSED_EVENT } from '~/flash';
import { isLoggedIn } from '~/lib/utils/common_utils';
import TermsApp from '~/terms/components/app.vue';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
@@ -129,7 +128,6 @@ describe('TermsApp', () => {
beforeEach(() => {
flashEl = document.createElement('div');
- flashEl.classList.add(`flash-${FLASH_TYPES.ALERT}`);
document.body.appendChild(flashEl);
});
@@ -137,7 +135,7 @@ describe('TermsApp', () => {
document.body.innerHTML = '';
});
- it('recalculates height of scrollable viewport', () => {
+ it('recalculates height of scrollable viewport', async () => {
jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => 800);
jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 600);
@@ -148,7 +146,8 @@ describe('TermsApp', () => {
jest.spyOn(document.documentElement, 'scrollHeight', 'get').mockImplementation(() => 700);
jest.spyOn(document.documentElement, 'clientHeight', 'get').mockImplementation(() => 600);
- flashEl.dispatchEvent(new Event(FLASH_CLOSED_EVENT));
+ flashEl.remove();
+ await nextTick();
expect(findScrollableViewport().attributes('style')).toBe('max-height: calc(100vh - 100px);');
});
diff --git a/spec/frontend/token_access/inbound_token_access_spec.js b/spec/frontend/token_access/inbound_token_access_spec.js
new file mode 100644
index 00000000000..fcd1a33fa68
--- /dev/null
+++ b/spec/frontend/token_access/inbound_token_access_spec.js
@@ -0,0 +1,311 @@
+import { GlAlert, GlFormInput, GlToggle, GlLoadingIcon } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue';
+import inboundAddProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_add_project_ci_job_token_scope.mutation.graphql';
+import inboundRemoveProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_remove_project_ci_job_token_scope.mutation.graphql';
+import inboundUpdateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/inbound_update_ci_job_token_scope.mutation.graphql';
+import inboundGetCIJobTokenScopeQuery from '~/token_access/graphql/queries/inbound_get_ci_job_token_scope.query.graphql';
+import inboundGetProjectsWithCIJobTokenScopeQuery from '~/token_access/graphql/queries/inbound_get_projects_with_ci_job_token_scope.query.graphql';
+import {
+ inboundJobTokenScopeEnabledResponse,
+ inboundJobTokenScopeDisabledResponse,
+ inboundProjectsWithScopeResponse,
+ inboundAddProjectSuccessResponse,
+ inboundRemoveProjectSuccess,
+ inboundUpdateScopeSuccessResponse,
+} from './mock_data';
+
+const projectPath = 'root/my-repo';
+const message = 'An error occurred';
+const error = new Error(message);
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+
+describe('TokenAccess component', () => {
+ let wrapper;
+
+ const inboundJobTokenScopeEnabledResponseHandler = jest
+ .fn()
+ .mockResolvedValue(inboundJobTokenScopeEnabledResponse);
+ const inboundJobTokenScopeDisabledResponseHandler = jest
+ .fn()
+ .mockResolvedValue(inboundJobTokenScopeDisabledResponse);
+ const inboundProjectsWithScopeResponseHandler = jest
+ .fn()
+ .mockResolvedValue(inboundProjectsWithScopeResponse);
+ const inboundAddProjectSuccessResponseHandler = jest
+ .fn()
+ .mockResolvedValue(inboundAddProjectSuccessResponse);
+ const inboundRemoveProjectSuccessHandler = jest
+ .fn()
+ .mockResolvedValue(inboundRemoveProjectSuccess);
+ const inboundUpdateScopeSuccessResponseHandler = jest
+ .fn()
+ .mockResolvedValue(inboundUpdateScopeSuccessResponse);
+ const failureHandler = jest.fn().mockRejectedValue(error);
+
+ const findToggle = () => wrapper.findComponent(GlToggle);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findAddProjectBtn = () => wrapper.findByRole('button', { name: 'Add project' });
+ const findCancelBtn = () => wrapper.findByRole('button', { name: 'Cancel' });
+ const findProjectInput = () => wrapper.findComponent(GlFormInput);
+ const findRemoveProjectBtn = () => wrapper.findByRole('button', { name: 'Remove access' });
+ const findTokenDisabledAlert = () => wrapper.findComponent(GlAlert);
+
+ const createMockApolloProvider = (requestHandlers) => {
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (requestHandlers, mountFn = shallowMountExtended) => {
+ wrapper = mountFn(InboundTokenAccess, {
+ provide: {
+ fullPath: projectPath,
+ },
+ apolloProvider: createMockApolloProvider(requestHandlers),
+ data() {
+ return {
+ targetProjectPath: 'root/test',
+ };
+ },
+ });
+ };
+
+ describe('loading state', () => {
+ it('shows loading state while waiting on query to resolve', async () => {
+ createComponent([
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ ]);
+
+ expect(findLoadingIcon().exists()).toBe(true);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ });
+ });
+
+ describe('fetching projects and scope', () => {
+ it('fetches projects and scope correctly', () => {
+ const expectedVariables = {
+ fullPath: 'root/my-repo',
+ };
+
+ createComponent([
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ ]);
+
+ expect(inboundJobTokenScopeEnabledResponseHandler).toHaveBeenCalledWith(expectedVariables);
+ expect(inboundProjectsWithScopeResponseHandler).toHaveBeenCalledWith(expectedVariables);
+ });
+
+ it('handles fetch projects error correctly', async () => {
+ createComponent([
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, failureHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'There was a problem fetching the projects',
+ });
+ });
+
+ it('handles fetch scope error correctly', async () => {
+ createComponent([
+ [inboundGetCIJobTokenScopeQuery, failureHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'There was a problem fetching the job token scope value',
+ });
+ });
+ });
+
+ describe('toggle', () => {
+ it('the toggle is on and the alert is hidden', async () => {
+ createComponent([
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(true);
+ expect(findTokenDisabledAlert().exists()).toBe(false);
+ });
+
+ it('the toggle is off and the alert is visible', async () => {
+ createComponent([
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeDisabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ ]);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(false);
+ expect(findTokenDisabledAlert().exists()).toBe(true);
+ });
+
+ describe('update ci job token scope', () => {
+ it('calls inboundUpdateCIJobTokenScopeMutation mutation', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundUpdateCIJobTokenScopeMutation, inboundUpdateScopeSuccessResponseHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(true);
+
+ findToggle().vm.$emit('change', false);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(false);
+ expect(inboundUpdateScopeSuccessResponseHandler).toHaveBeenCalledWith({
+ input: {
+ fullPath: 'root/my-repo',
+ inboundJobTokenScopeEnabled: false,
+ },
+ });
+ });
+
+ it('handles update scope error correctly', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeDisabledResponseHandler],
+ [inboundUpdateCIJobTokenScopeMutation, failureHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(false);
+
+ findToggle().vm.$emit('change', true);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(false);
+ expect(createAlert).toHaveBeenCalledWith({ message });
+ });
+ });
+ });
+
+ describe('add project', () => {
+ it('calls add project mutation', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [inboundAddProjectCIJobTokenScopeMutation, inboundAddProjectSuccessResponseHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ findAddProjectBtn().trigger('click');
+
+ expect(inboundAddProjectSuccessResponseHandler).toHaveBeenCalledWith({
+ projectPath,
+ targetProjectPath: 'root/test',
+ });
+ });
+
+ it('add project handles error correctly', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [inboundAddProjectCIJobTokenScopeMutation, failureHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ findAddProjectBtn().trigger('click');
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message });
+ });
+
+ it('clicking cancel clears target path', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ expect(findProjectInput().element.value).toBe('root/test');
+
+ await findCancelBtn().trigger('click');
+
+ expect(findProjectInput().element.value).toBe('');
+ });
+ });
+
+ describe('remove project', () => {
+ it('calls remove project mutation', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [inboundRemoveProjectCIJobTokenScopeMutation, inboundRemoveProjectSuccessHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ findRemoveProjectBtn().trigger('click');
+
+ expect(inboundRemoveProjectSuccessHandler).toHaveBeenCalledWith({
+ projectPath,
+ targetProjectPath: 'root/ci-project',
+ });
+ });
+
+ it('remove project handles error correctly', async () => {
+ createComponent(
+ [
+ [inboundGetCIJobTokenScopeQuery, inboundJobTokenScopeEnabledResponseHandler],
+ [inboundGetProjectsWithCIJobTokenScopeQuery, inboundProjectsWithScopeResponseHandler],
+ [inboundRemoveProjectCIJobTokenScopeMutation, failureHandler],
+ ],
+ mountExtended,
+ );
+
+ await waitForPromises();
+
+ findRemoveProjectBtn().trigger('click');
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({ message });
+ });
+ });
+});
diff --git a/spec/frontend/token_access/mock_data.js b/spec/frontend/token_access/mock_data.js
index 0c8ba266201..ab04735b985 100644
--- a/spec/frontend/token_access/mock_data.js
+++ b/spec/frontend/token_access/mock_data.js
@@ -105,3 +105,125 @@ export const mockProjects = [
__typename: 'Project',
},
];
+
+export const mockFields = [
+ {
+ key: 'project',
+ label: 'Project with access',
+ },
+ {
+ key: 'namespace',
+ label: 'Namespace',
+ },
+ {
+ key: 'actions',
+ label: '',
+ },
+];
+
+export const optInJwtQueryResponse = (optInJwt) => ({
+ data: {
+ project: {
+ id: '1',
+ ciCdSettings: {
+ optInJwt,
+ __typename: 'ProjectCiCdSetting',
+ },
+ __typename: 'Project',
+ },
+ },
+});
+
+export const optInJwtMutationResponse = (optInJwt) => ({
+ data: {
+ ciCdSettingsUpdate: {
+ ciCdSettings: {
+ optInJwt,
+ __typename: 'ProjectCiCdSetting',
+ },
+ errors: [],
+ __typename: 'CiCdSettingsUpdatePayload',
+ },
+ },
+});
+
+export const inboundJobTokenScopeEnabledResponse = {
+ data: {
+ project: {
+ id: '1',
+ ciCdSettings: {
+ inboundJobTokenScopeEnabled: true,
+ __typename: 'ProjectCiCdSetting',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const inboundJobTokenScopeDisabledResponse = {
+ data: {
+ project: {
+ id: '1',
+ ciCdSettings: {
+ inboundJobTokenScopeEnabled: false,
+ __typename: 'ProjectCiCdSetting',
+ },
+ __typename: 'Project',
+ },
+ },
+};
+
+export const inboundProjectsWithScopeResponse = {
+ data: {
+ project: {
+ __typename: 'Project',
+ id: '1',
+ ciJobTokenScope: {
+ __typename: 'CiJobTokenScopeType',
+ inboundAllowlist: {
+ __typename: 'ProjectConnection',
+ nodes: [
+ {
+ __typename: 'Project',
+ fullPath: 'root/ci-project',
+ id: 'gid://gitlab/Project/23',
+ name: 'ci-project',
+ namespace: { id: 'gid://gitlab/Namespaces::UserNamespace/1', fullPath: 'root' },
+ },
+ ],
+ },
+ },
+ },
+ },
+};
+
+export const inboundAddProjectSuccessResponse = {
+ data: {
+ ciJobTokenScopeAddProject: {
+ errors: [],
+ __typename: 'CiJobTokenScopeAddProjectPayload',
+ },
+ },
+};
+
+export const inboundRemoveProjectSuccess = {
+ data: {
+ ciJobTokenScopeRemoveProject: {
+ errors: [],
+ __typename: 'CiJobTokenScopeRemoveProjectPayload',
+ },
+ },
+};
+
+export const inboundUpdateScopeSuccessResponse = {
+ data: {
+ ciCdSettingsUpdate: {
+ ciCdSettings: {
+ inboundJobTokenScopeEnabled: false,
+ __typename: 'ProjectCiCdSetting',
+ },
+ errors: [],
+ __typename: 'CiCdSettingsUpdatePayload',
+ },
+ },
+};
diff --git a/spec/frontend/token_access/opt_in_jwt_spec.js b/spec/frontend/token_access/opt_in_jwt_spec.js
new file mode 100644
index 00000000000..3a68f247aa6
--- /dev/null
+++ b/spec/frontend/token_access/opt_in_jwt_spec.js
@@ -0,0 +1,144 @@
+import { GlLink, GlLoadingIcon, GlToggle, GlSprintf } from '@gitlab/ui';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { createAlert } from '~/flash';
+import { OPT_IN_JWT_HELP_LINK } from '~/token_access/constants';
+import OptInJwt from '~/token_access/components/opt_in_jwt.vue';
+import getOptInJwtSettingQuery from '~/token_access/graphql/queries/get_opt_in_jwt_setting.query.graphql';
+import updateOptInJwtMutation from '~/token_access/graphql/mutations/update_opt_in_jwt.mutation.graphql';
+import { optInJwtMutationResponse, optInJwtQueryResponse } from './mock_data';
+
+const errorMessage = 'An error occurred';
+const error = new Error(errorMessage);
+
+Vue.use(VueApollo);
+
+jest.mock('~/flash');
+
+describe('OptInJwt component', () => {
+ let wrapper;
+
+ const failureHandler = jest.fn().mockRejectedValue(error);
+ const enabledOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtQueryResponse(true));
+ const disabledOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtQueryResponse(false));
+ const updateOptInJwtHandler = jest.fn().mockResolvedValue(optInJwtMutationResponse(true));
+
+ const findHelpLink = () => wrapper.findComponent(GlLink);
+ const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
+ const findToggle = () => wrapper.findComponent(GlToggle);
+ const findOptInJwtExpandedSection = () => wrapper.findByTestId('opt-in-jwt-expanded-section');
+
+ const createMockApolloProvider = (requestHandlers) => {
+ return createMockApollo(requestHandlers);
+ };
+
+ const createComponent = (requestHandlers, mountFn = shallowMountExtended, options = {}) => {
+ wrapper = mountFn(OptInJwt, {
+ provide: {
+ fullPath: 'root/my-repo',
+ },
+ apolloProvider: createMockApolloProvider(requestHandlers),
+ data() {
+ return {
+ targetProjectPath: 'root/test',
+ };
+ },
+ ...options,
+ });
+ };
+
+ const createShallowComponent = (requestHandlers, options = {}) =>
+ createComponent(requestHandlers, shallowMountExtended, options);
+ const createFullComponent = (requestHandlers, options = {}) =>
+ createComponent(requestHandlers, mountExtended, options);
+
+ describe('loading state', () => {
+ it('shows loading state and hides toggle while waiting on query to resolve', async () => {
+ createShallowComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]]);
+
+ expect(findLoadingIcon().exists()).toBe(true);
+ expect(findToggle().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findLoadingIcon().exists()).toBe(false);
+ expect(findToggle().exists()).toBe(true);
+ });
+ });
+
+ describe('template', () => {
+ it('renders help link', async () => {
+ createShallowComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]], {
+ stubs: {
+ GlToggle,
+ GlSprintf,
+ GlLink,
+ },
+ });
+ await waitForPromises();
+
+ expect(findHelpLink().exists()).toBe(true);
+ expect(findHelpLink().attributes('href')).toBe(OPT_IN_JWT_HELP_LINK);
+ });
+ });
+
+ describe('toggle JWT token access', () => {
+ it('code instruction is visible when toggle is enabled', async () => {
+ createShallowComponent([[getOptInJwtSettingQuery, enabledOptInJwtHandler]]);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(true);
+ expect(findOptInJwtExpandedSection().exists()).toBe(true);
+ });
+
+ it('code instruction is hidden when toggle is disabled', async () => {
+ createShallowComponent([[getOptInJwtSettingQuery, disabledOptInJwtHandler]]);
+
+ await waitForPromises();
+
+ expect(findToggle().props('value')).toBe(false);
+ expect(findOptInJwtExpandedSection().exists()).toBe(false);
+ });
+
+ describe('update JWT token access', () => {
+ it('calls updateOptInJwtMutation with correct arguments', async () => {
+ createFullComponent([
+ [getOptInJwtSettingQuery, disabledOptInJwtHandler],
+ [updateOptInJwtMutation, updateOptInJwtHandler],
+ ]);
+
+ await waitForPromises();
+
+ findToggle().vm.$emit('change', true);
+
+ expect(updateOptInJwtHandler).toHaveBeenCalledWith({
+ input: {
+ fullPath: 'root/my-repo',
+ optInJwt: true,
+ },
+ });
+ });
+
+ it('handles update error', async () => {
+ createFullComponent([
+ [getOptInJwtSettingQuery, enabledOptInJwtHandler],
+ [updateOptInJwtMutation, failureHandler],
+ ]);
+
+ await waitForPromises();
+
+ findToggle().vm.$emit('change', false);
+
+ await waitForPromises();
+
+ expect(createAlert).toHaveBeenCalledWith({
+ message: 'An error occurred while update the setting. Please try again.',
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/token_access/token_access_spec.js b/spec/frontend/token_access/outbound_token_access_spec.js
index 6fe94e28548..893a021197f 100644
--- a/spec/frontend/token_access/token_access_spec.js
+++ b/spec/frontend/token_access/outbound_token_access_spec.js
@@ -5,7 +5,7 @@ import createMockApollo from 'helpers/mock_apollo_helper';
import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import { createAlert } from '~/flash';
-import TokenAccess from '~/token_access/components/token_access.vue';
+import OutboundTokenAccess from '~/token_access/components/outbound_token_access.vue';
import addProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/add_project_ci_job_token_scope.mutation.graphql';
import removeProjectCIJobTokenScopeMutation from '~/token_access/graphql/mutations/remove_project_ci_job_token_scope.mutation.graphql';
import updateCIJobTokenScopeMutation from '~/token_access/graphql/mutations/update_ci_job_token_scope.mutation.graphql';
@@ -50,7 +50,7 @@ describe('TokenAccess component', () => {
};
const createComponent = (requestHandlers, mountFn = shallowMountExtended) => {
- wrapper = mountFn(TokenAccess, {
+ wrapper = mountFn(OutboundTokenAccess, {
provide: {
fullPath: projectPath,
},
@@ -63,10 +63,6 @@ describe('TokenAccess component', () => {
});
};
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('loading state', () => {
it('shows loading state while waiting on query to resolve', async () => {
createComponent([
diff --git a/spec/frontend/token_access/token_access_app_spec.js b/spec/frontend/token_access/token_access_app_spec.js
new file mode 100644
index 00000000000..7f269ee5fda
--- /dev/null
+++ b/spec/frontend/token_access/token_access_app_spec.js
@@ -0,0 +1,47 @@
+import { shallowMount } from '@vue/test-utils';
+import OutboundTokenAccess from '~/token_access/components/outbound_token_access.vue';
+import InboundTokenAccess from '~/token_access/components/inbound_token_access.vue';
+import OptInJwt from '~/token_access/components/opt_in_jwt.vue';
+import TokenAccessApp from '~/token_access/components/token_access_app.vue';
+
+describe('TokenAccessApp component', () => {
+ let wrapper;
+
+ const findOutboundTokenAccess = () => wrapper.findComponent(OutboundTokenAccess);
+ const findInboundTokenAccess = () => wrapper.findComponent(InboundTokenAccess);
+ const findOptInJwt = () => wrapper.findComponent(OptInJwt);
+
+ const createComponent = (flagState = false) => {
+ wrapper = shallowMount(TokenAccessApp, {
+ provide: {
+ glFeatures: { ciInboundJobTokenScope: flagState },
+ },
+ });
+ };
+
+ describe('default', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('renders the opt in jwt component', () => {
+ expect(findOptInJwt().exists()).toBe(true);
+ });
+
+ it('renders the outbound token access component', () => {
+ expect(findOutboundTokenAccess().exists()).toBe(true);
+ });
+
+ it('does not render the inbound token access component', () => {
+ expect(findInboundTokenAccess().exists()).toBe(false);
+ });
+ });
+
+ describe('with feature flag enabled', () => {
+ it('renders the inbound token access component', () => {
+ createComponent(true);
+
+ expect(findInboundTokenAccess().exists()).toBe(true);
+ });
+ });
+});
diff --git a/spec/frontend/token_access/token_projects_table_spec.js b/spec/frontend/token_access/token_projects_table_spec.js
index 0fa1a2453f7..b51d8b3ccea 100644
--- a/spec/frontend/token_access/token_projects_table_spec.js
+++ b/spec/frontend/token_access/token_projects_table_spec.js
@@ -1,7 +1,7 @@
import { GlTable, GlButton } from '@gitlab/ui';
import { mountExtended } from 'helpers/vue_test_utils_helper';
import TokenProjectsTable from '~/token_access/components/token_projects_table.vue';
-import { mockProjects } from './mock_data';
+import { mockProjects, mockFields } from './mock_data';
describe('Token projects table', () => {
let wrapper;
@@ -12,6 +12,7 @@ describe('Token projects table', () => {
fullPath: 'root/ci-project',
},
propsData: {
+ tableFields: mockFields,
projects: mockProjects,
},
});
@@ -28,10 +29,6 @@ describe('Token projects table', () => {
createComponent();
});
- afterEach(() => {
- wrapper.destroy();
- });
-
it('displays a table', () => {
expect(findTable().exists()).toBe(true);
});
diff --git a/spec/frontend/tracking/get_standard_context_spec.js b/spec/frontend/tracking/get_standard_context_spec.js
index ada914b586c..ae452aeaeb3 100644
--- a/spec/frontend/tracking/get_standard_context_spec.js
+++ b/spec/frontend/tracking/get_standard_context_spec.js
@@ -52,7 +52,7 @@ describe('~/tracking/get_standard_context', () => {
it('accepts optional `extra` property', () => {
const extra = { foo: 'bar' };
- expect(getStandardContext({ extra }).data.extra).toBe(extra);
+ expect(getStandardContext({ extra }).data.extra).toStrictEqual(extra);
});
describe('with Google Analytics cookie present', () => {
diff --git a/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js b/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js
new file mode 100644
index 00000000000..cb70ea4e72d
--- /dev/null
+++ b/spec/frontend/usage_quotas/components/usage_quotas_app_spec.js
@@ -0,0 +1,39 @@
+import { GlSprintf } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import UsageQuotasApp from '~/usage_quotas/components/usage_quotas_app.vue';
+import { USAGE_QUOTAS_TITLE } from '~/usage_quotas/constants';
+import { defaultProvide } from '../mock_data';
+
+describe('UsageQuotasApp', () => {
+ let wrapper;
+
+ const createComponent = ({ provide = {} } = {}) => {
+ wrapper = shallowMountExtended(UsageQuotasApp, {
+ provide: {
+ ...defaultProvide,
+ ...provide,
+ },
+ stubs: {
+ GlSprintf,
+ },
+ });
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findSubTitle = () => wrapper.findByTestId('usage-quotas-page-subtitle');
+
+ it('renders the view title', () => {
+ expect(wrapper.text()).toContain(USAGE_QUOTAS_TITLE);
+ });
+
+ it('renders the view subtitle', () => {
+ expect(findSubTitle().text()).toContain(defaultProvide.namespaceName);
+ });
+});
diff --git a/spec/frontend/usage_quotas/mock_data.js b/spec/frontend/usage_quotas/mock_data.js
new file mode 100644
index 00000000000..a9d2a7ad1db
--- /dev/null
+++ b/spec/frontend/usage_quotas/mock_data.js
@@ -0,0 +1,3 @@
+export const defaultProvide = {
+ namespaceName: 'Group 1',
+};
diff --git a/spec/frontend/users/profile/components/report_abuse_button_spec.js b/spec/frontend/users/profile/components/report_abuse_button_spec.js
index 7ad28566f49..1ef856c9849 100644
--- a/spec/frontend/users/profile/components/report_abuse_button_spec.js
+++ b/spec/frontend/users/profile/components/report_abuse_button_spec.js
@@ -9,7 +9,7 @@ describe('ReportAbuseButton', () => {
let wrapper;
const ACTION_PATH = '/abuse_reports/add_category';
- const USER_ID = '1';
+ const USER_ID = 1;
const REPORTED_FROM_URL = 'http://example.com';
const createComponent = (props) => {
diff --git a/spec/frontend/users_select/test_helper.js b/spec/frontend/users_select/test_helper.js
index 9231e38ea90..6fb3436100f 100644
--- a/spec/frontend/users_select/test_helper.js
+++ b/spec/frontend/users_select/test_helper.js
@@ -4,6 +4,7 @@ import usersFixture from 'test_fixtures/autocomplete/users.json';
import { getFixture } from 'helpers/fixtures';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import UsersSelect from '~/users_select';
// fixtures -------------------------------------------------------------------
@@ -31,7 +32,7 @@ export const createTestContext = ({ fixturePath }) => {
document.body.appendChild(rootEl);
mock = new MockAdapter(axios);
- mock.onGet('/-/autocomplete/users.json').reply(200, cloneDeep(getUsersFixture()));
+ mock.onGet('/-/autocomplete/users.json').reply(HTTP_STATUS_OK, cloneDeep(getUsersFixture()));
};
const teardown = () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
index 1f3b6dce620..bf208f16d18 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_spec.js
@@ -13,7 +13,12 @@ import {
} from '~/vue_merge_request_widget/components/approvals/messages';
import eventHub from '~/vue_merge_request_widget/event_hub';
-jest.mock('~/flash');
+const mockAlertDismiss = jest.fn();
+jest.mock('~/flash', () => ({
+ createAlert: jest.fn().mockImplementation(() => ({
+ dismiss: mockAlertDismiss,
+ })),
+}));
const RULE_NAME = 'first_rule';
const TEST_HELP_PATH = 'help/path';
@@ -87,6 +92,8 @@ describe('MRWidget approvals', () => {
approvalRules: [],
isOpen: true,
state: 'open',
+ targetProjectFullPath: 'gitlab-org/gitlab',
+ iid: '1',
};
jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
@@ -98,21 +105,18 @@ describe('MRWidget approvals', () => {
});
describe('when created', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('shows loading message', () => {
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ fetchingApprovals: true });
+ it('shows loading message', async () => {
+ service = {
+ fetchApprovals: jest.fn().mockReturnValue(new Promise(() => {})),
+ };
- return nextTick().then(() => {
- expect(wrapper.text()).toContain(FETCH_LOADING);
- });
+ createComponent();
+ await nextTick();
+ expect(wrapper.text()).toContain(FETCH_LOADING);
});
it('fetches approvals', () => {
+ createComponent();
expect(service.fetchApprovals).toHaveBeenCalled();
});
});
@@ -267,9 +271,16 @@ describe('MRWidget approvals', () => {
return nextTick();
});
- it('flashes error message', () => {
+ it('shows an alert with error message', () => {
expect(createAlert).toHaveBeenCalledWith({ message: APPROVE_ERROR });
});
+
+ it('clears the previous alert', () => {
+ expect(mockAlertDismiss).toHaveBeenCalledTimes(0);
+
+ findAction().vm.$emit('click');
+ expect(mockAlertDismiss).toHaveBeenCalledTimes(1);
+ });
});
});
});
@@ -377,15 +388,14 @@ describe('MRWidget approvals', () => {
});
it('is rendered with props', () => {
- const expected = testApprovals();
const summary = findSummary();
expect(findOptionalSummary().exists()).toBe(false);
expect(summary.exists()).toBe(true);
expect(summary.props()).toMatchObject({
- approvalsLeft: expected.approvals_left,
- rulesLeft: expected.approval_rules_left,
- approvers: testApprovedBy(),
+ projectPath: 'gitlab-org/gitlab',
+ iid: '1',
+ updatedCount: 0,
});
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
index f4234083346..e75ce7c60c9 100644
--- a/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/approvals/approvals_summary_spec.js
@@ -1,5 +1,12 @@
-import { shallowMount } from '@vue/test-utils';
-import { toNounSeriesText } from '~/lib/utils/grammar';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import { mount } from '@vue/test-utils';
+import approvedByMultipleUsers from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql_multiple_users.json';
+import noApprovalsResponse from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql_no_approvals.json';
+import approvedByCurrentUser from 'test_fixtures/graphql/merge_requests/approvals/approved_by.query.graphql.json';
+import waitForPromises from 'helpers/wait_for_promises';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import { getIdFromGraphQLId } from '~/graphql_shared/utils';
import ApprovalsSummary from '~/vue_merge_request_widget/components/approvals/approvals_summary.vue';
import {
APPROVED_BY_OTHERS,
@@ -7,25 +14,21 @@ import {
APPROVED_BY_YOU_AND_OTHERS,
} from '~/vue_merge_request_widget/components/approvals/messages';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
+import approvedByQuery from 'ee_else_ce/vue_merge_request_widget/components/approvals/queries/approved_by.query.graphql';
-const exampleUserId = 1;
-const testApprovers = () => Array.from({ length: 5 }, (_, i) => i).map((id) => ({ id }));
-const testRulesLeft = () => ['Lorem', 'Ipsum', 'dolar & sit'];
-const TEST_APPROVALS_LEFT = 3;
+Vue.use(VueApollo);
describe('MRWidget approvals summary', () => {
const originalUserId = gon.current_user_id;
let wrapper;
- const createComponent = (props = {}) => {
- wrapper = shallowMount(ApprovalsSummary, {
+ const createComponent = (response = approvedByCurrentUser) => {
+ wrapper = mount(ApprovalsSummary, {
propsData: {
- approved: false,
- approvers: testApprovers(),
- approvalsLeft: TEST_APPROVALS_LEFT,
- rulesLeft: testRulesLeft(),
- ...props,
+ projectPath: 'gitlab-org/gitlab',
+ iid: '1',
},
+ apolloProvider: createMockApollo([[approvedByQuery, jest.fn().mockResolvedValue(response)]]),
});
};
@@ -38,10 +41,10 @@ describe('MRWidget approvals summary', () => {
});
describe('when approved', () => {
- beforeEach(() => {
- createComponent({
- approved: true,
- });
+ beforeEach(async () => {
+ createComponent();
+
+ await waitForPromises();
});
it('shows approved message', () => {
@@ -54,18 +57,19 @@ describe('MRWidget approvals summary', () => {
expect(avatars.exists()).toBe(true);
expect(avatars.props()).toEqual(
expect.objectContaining({
- items: testApprovers(),
+ items: approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes,
}),
);
});
describe('by the current user', () => {
- beforeEach(() => {
- gon.current_user_id = exampleUserId;
- createComponent({
- approvers: [{ id: exampleUserId }],
- approved: true,
- });
+ beforeEach(async () => {
+ gon.current_user_id = getIdFromGraphQLId(
+ approvedByCurrentUser.data.project.mergeRequest.approvedBy.nodes[0].id,
+ );
+ createComponent();
+
+ await waitForPromises();
});
it('shows "Approved by you" message', () => {
@@ -74,12 +78,13 @@ describe('MRWidget approvals summary', () => {
});
describe('by the current user and others', () => {
- beforeEach(() => {
- gon.current_user_id = exampleUserId;
- createComponent({
- approvers: [{ id: exampleUserId }, { id: exampleUserId + 1 }],
- approved: true,
- });
+ beforeEach(async () => {
+ gon.current_user_id = getIdFromGraphQLId(
+ approvedByMultipleUsers.data.project.mergeRequest.approvedBy.nodes[0].id,
+ );
+ createComponent(approvedByMultipleUsers);
+
+ await waitForPromises();
});
it('shows "Approved by you and others" message', () => {
@@ -88,12 +93,10 @@ describe('MRWidget approvals summary', () => {
});
describe('by other users than the current user', () => {
- beforeEach(() => {
- gon.current_user_id = exampleUserId;
- createComponent({
- approvers: [{ id: exampleUserId + 1 }],
- approved: true,
- });
+ beforeEach(async () => {
+ createComponent(approvedByMultipleUsers);
+
+ await waitForPromises();
});
it('shows "Approved by others" message', () => {
@@ -102,37 +105,11 @@ describe('MRWidget approvals summary', () => {
});
});
- describe('when not approved', () => {
- beforeEach(() => {
- createComponent();
- });
-
- it('render message', () => {
- const names = toNounSeriesText(testRulesLeft());
-
- expect(wrapper.text()).toContain(`Requires ${TEST_APPROVALS_LEFT} approvals from ${names}.`);
- });
- });
-
- describe('when no rulesLeft', () => {
- beforeEach(() => {
- createComponent({
- rulesLeft: [],
- });
- });
-
- it('renders message', () => {
- expect(wrapper.text()).toContain(
- `Requires ${TEST_APPROVALS_LEFT} approvals from eligible users`,
- );
- });
- });
-
describe('when no approvers', () => {
- beforeEach(() => {
- createComponent({
- approvers: [],
- });
+ beforeEach(async () => {
+ createComponent(noApprovalsResponse);
+
+ await waitForPromises();
});
it('does not render avatar list', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
index 73fa4b7b08f..52e2393bf05 100644
--- a/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/artifacts_list_app_spec.js
@@ -5,6 +5,7 @@ import Vue, { nextTick } from 'vue';
import Vuex from 'vuex';
import { TEST_HOST as FAKE_ENDPOINT } from 'helpers/test_constants';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ArtifactsListApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
import { getStoreConfig } from '~/vue_merge_request_widget/stores/artifacts_list';
import { artifacts } from '../mock_data';
@@ -78,10 +79,10 @@ describe('Merge Requests Artifacts list app', () => {
describe('with results', () => {
beforeEach(() => {
createComponent();
- mock.onGet(FAKE_ENDPOINT).reply(200, artifacts, {});
+ mock.onGet(FAKE_ENDPOINT).reply(HTTP_STATUS_OK, artifacts, {});
store.dispatch('receiveArtifactsSuccess', {
data: artifacts,
- status: 200,
+ status: HTTP_STATUS_OK,
});
return nextTick();
});
@@ -109,7 +110,7 @@ describe('Merge Requests Artifacts list app', () => {
describe('with error', () => {
beforeEach(() => {
createComponent();
- mock.onGet(FAKE_ENDPOINT).reply(500, {}, {});
+ mock.onGet(FAKE_ENDPOINT).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR, {}, {});
store.dispatch('receiveArtifactsError');
return nextTick();
});
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js
index 193a16bae8d..4775a0673b5 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_memory_usage_spec.js
@@ -2,6 +2,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import Vue, { nextTick } from 'vue';
import waitForPromises from 'helpers/wait_for_promises';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MemoryUsage from '~/vue_merge_request_widget/components/deployment/memory_usage.vue';
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
@@ -66,7 +67,7 @@ describe('MemoryUsage', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(`${url}.json`).reply(200);
+ mock.onGet(`${url}.json`).reply(HTTP_STATUS_OK);
vm = createComponent();
el = vm.$el;
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
index c3f6331e560..13beb43e10b 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_container_spec.js
@@ -2,6 +2,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ArtifactsApp from '~/vue_merge_request_widget/components/artifacts_list_app.vue';
import DeploymentList from '~/vue_merge_request_widget/components/deployment/deployment_list.vue';
import MrWidgetPipeline from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
@@ -25,7 +26,7 @@ describe('MrWidgetPipelineContainer', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet().reply(200, {});
+ mock.onGet().reply(HTTP_STATUS_OK, {});
});
afterEach(() => {
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
index 144e176b0f0..6a899c00b98 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_pipeline_spec.js
@@ -4,6 +4,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { trimText } from 'helpers/text_helper';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MRWidgetPipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
import PipelineMiniGraph from '~/pipelines/components/pipeline_mini_graph/pipeline_mini_graph.vue';
import { SUCCESS } from '~/vue_merge_request_widget/constants';
@@ -39,7 +40,7 @@ describe('MRWidgetPipeline', () => {
const findMonitoringPipelineMessage = () => wrapper.findByTestId('monitoring-pipeline-message');
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
- const mockArtifactsRequest = () => new MockAdapter(axios).onGet().reply(200, []);
+ const mockArtifactsRequest = () => new MockAdapter(axios).onGet().reply(HTTP_STATUS_OK, []);
const createWrapper = (props = {}, mountFn = shallowMount) => {
wrapper = extendedWrapper(
@@ -110,6 +111,14 @@ describe('MRWidgetPipeline', () => {
expect(findPipelineMiniGraph().props('stages')).toHaveLength(stagesCount);
});
+ it('should render the latest downstream pipelines only', () => {
+ // component receives two downstream pipelines. one of them is already outdated
+ // because we retried the trigger job, so the mini pipeline graph will only
+ // render the newly created downstream pipeline instead
+ expect(mockData.pipeline.triggered).toHaveLength(2);
+ expect(findPipelineMiniGraph().props('downstreamPipelines')).toHaveLength(1);
+ });
+
describe('should render pipeline coverage information', () => {
it('should render coverage percentage', () => {
expect(findPipelineCoverage().text()).toMatch(
@@ -223,7 +232,6 @@ describe('MRWidgetPipeline', () => {
({ pipeline } = JSON.parse(JSON.stringify(mockData)));
pipeline.details.event_type_name = 'Pipeline';
- pipeline.details.name = 'Pipeline';
pipeline.merge_request_event_type = undefined;
pipeline.ref.tag = false;
pipeline.ref.branch = false;
@@ -265,7 +273,6 @@ describe('MRWidgetPipeline', () => {
describe('for a detached merge request pipeline', () => {
it('renders a pipeline widget that reads "Merge request pipeline <ID> <status> for <SHA>"', () => {
pipeline.details.event_type_name = 'Merge request pipeline';
- pipeline.details.name = 'Merge request pipeline';
pipeline.merge_request_event_type = 'detached';
factory();
diff --git a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
index 7b52773e92d..ec047fe0714 100644
--- a/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/mr_widget_rebase_spec.js
@@ -219,18 +219,15 @@ describe('Merge request widget rebase component', () => {
it('renders a message explaining user does not have permissions', () => {
const text = findRebaseMessageText();
- expect(text).toContain(
- 'Merge blocked: the source branch must be rebased onto the target branch.',
- );
+ expect(text).toContain('Merge blocked:');
expect(text).toContain('the source branch must be rebased');
});
it('renders the correct target branch name', () => {
- const elem = findRebaseMessage();
+ const text = findRebaseMessageText();
- expect(elem.text()).toContain(
- 'Merge blocked: the source branch must be rebased onto the target branch.',
- );
+ expect(text).toContain('Merge blocked:');
+ expect(text).toContain('the source branch must be rebased onto the target branch.');
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/report_widget_container_spec.js b/spec/frontend/vue_merge_request_widget/components/report_widget_container_spec.js
new file mode 100644
index 00000000000..436f74d1be2
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/report_widget_container_spec.js
@@ -0,0 +1,33 @@
+import { nextTick } from 'vue';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import ReportWidgetContainer from '~/vue_merge_request_widget/components/report_widget_container.vue';
+
+describe('app/assets/javascripts/vue_merge_request_widget/components/report_widget_container.vue', () => {
+ let wrapper;
+
+ const createComponent = ({ slot } = {}) => {
+ wrapper = mountExtended(ReportWidgetContainer, {
+ slots: {
+ default: slot,
+ },
+ });
+ };
+
+ it('hides the container when children has no content', async () => {
+ createComponent({ slot: `<span><b></b></span>` });
+ await nextTick();
+ expect(wrapper.isVisible()).toBe(false);
+ });
+
+ it('hides the container when children has only empty spaces', async () => {
+ createComponent({ slot: `<span><b>&nbsp;<br/>\t\r\n</b></span>&nbsp;` });
+ await nextTick();
+ expect(wrapper.isVisible()).toBe(false);
+ });
+
+ it('shows the container when a child has content', async () => {
+ createComponent({ slot: `<span><b>test</b></span>` });
+ await nextTick();
+ expect(wrapper.isVisible()).toBe(true);
+ });
+});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
index 8eeba4d6274..e4448346685 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/merge_checks_failed_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import MergeChecksFailed from '~/vue_merge_request_widget/components/states/merge_checks_failed.vue';
import { DETAILED_MERGE_STATUS } from '~/vue_merge_request_widget/constants';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
let wrapper;
@@ -23,6 +24,7 @@ describe('Merge request widget merge checks failed state component', () => {
`('display $displayText text for $mrState', ({ mrState, displayText }) => {
factory({ mr: mrState });
- expect(wrapper.text()).toContain(MergeChecksFailed.i18n[displayText]);
+ const message = wrapper.findComponent(BoldText).props('message');
+ expect(message).toContain(MergeChecksFailed.i18n[displayText]);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
index 5c07f4ce143..08700e834d7 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_archived_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived.vue';
import StateContainer from '~/vue_merge_request_widget/components/state_container.vue';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
describe('MRWidgetArchived', () => {
let wrapper;
@@ -20,8 +21,8 @@ describe('MRWidgetArchived', () => {
});
it('renders information about merging', () => {
- expect(wrapper.text()).toContain(
- 'Merge unavailable: merge requests are read-only on archived projects.',
- );
+ const message = wrapper.findComponent(BoldText).props('message');
+ expect(message).toContain('Merge unavailable:');
+ expect(message).toContain('merge requests are read-only on archived projects.');
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
index 774e2bafed3..a6d3a6286a7 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_commits_header_spec.js
@@ -58,13 +58,8 @@ describe('Commits header component', () => {
expect(findCommitToggle().attributes('aria-label')).toBe('Expand');
});
- it('has a chevron-right icon', async () => {
+ it('has a chevron-right icon', () => {
createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ expanded: false });
-
- await nextTick();
expect(findCommitToggle().props('icon')).toBe('chevron-right');
});
@@ -110,25 +105,21 @@ describe('Commits header component', () => {
});
describe('when expanded', () => {
- beforeEach(() => {
+ beforeEach(async () => {
createComponent();
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ expanded: true });
+ findCommitToggle().trigger('click');
+ await nextTick();
});
- it('toggle has aria-label equal to collapse', async () => {
- await nextTick();
+ it('toggle has aria-label equal to collapse', () => {
expect(findCommitToggle().attributes('aria-label')).toBe('Collapse');
});
- it('has a chevron-down icon', async () => {
- await nextTick();
+ it('has a chevron-down icon', () => {
expect(findCommitToggle().props('icon')).toBe('chevron-down');
});
- it('has a collapse text', async () => {
- await nextTick();
+ it('has a collapse text', () => {
expect(findHeaderWrapper().text()).toBe('Collapse');
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
index a16e4d4a6ea..2ca9dc61745 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_conflicts_spec.js
@@ -12,9 +12,9 @@ describe('MRWidgetConflicts', () => {
const findResolveButton = () => wrapper.findByTestId('resolve-conflicts-button');
const findMergeLocalButton = () => wrapper.findByTestId('merge-locally-button');
- const mergeConflictsText = 'Merge blocked: merge conflicts must be resolved.';
+ const mergeConflictsText = 'merge conflicts must be resolved.';
const fastForwardMergeText =
- 'Merge blocked: fast-forward merge is not possible. To merge this request, first rebase locally.';
+ 'fast-forward merge is not possible. To merge this request, first rebase locally.';
const userCannotMergeText =
'Users who can write to the source or target branches can resolve the conflicts.';
const resolveConflictsBtnText = 'Resolve conflicts';
@@ -76,8 +76,9 @@ describe('MRWidgetConflicts', () => {
});
it('should tell you about conflicts without bothering other people', () => {
- expect(wrapper.text()).toContain(mergeConflictsText);
- expect(wrapper.text()).not.toContain(userCannotMergeText);
+ const text = removeBreakLine(wrapper.text()).trim();
+ expect(text).toContain(mergeConflictsText);
+ expect(text).not.toContain(userCannotMergeText);
});
it('should not allow you to resolve the conflicts', () => {
@@ -102,8 +103,8 @@ describe('MRWidgetConflicts', () => {
});
it('should tell you about conflicts', () => {
- expect(wrapper.text()).toContain(mergeConflictsText);
- expect(wrapper.text()).toContain(userCannotMergeText);
+ const text = removeBreakLine(wrapper.text()).trim();
+ expect(text).toContain(userCannotMergeText);
});
it('should allow you to resolve the conflicts', () => {
@@ -129,8 +130,9 @@ describe('MRWidgetConflicts', () => {
});
it('should tell you about conflicts without bothering other people', () => {
- expect(wrapper.text()).toContain(mergeConflictsText);
- expect(wrapper.text()).not.toContain(userCannotMergeText);
+ const text = removeBreakLine(wrapper.text()).trim();
+ expect(text).toContain(mergeConflictsText);
+ expect(text).not.toContain(userCannotMergeText);
});
it('should allow you to resolve the conflicts', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
index 49bd3739fdb..5408f731b34 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_merging_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import simplePoll from '~/lib/utils/simple_poll';
import MrWidgetMerging from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
jest.mock('~/lib/utils/simple_poll', () =>
jest.fn().mockImplementation(jest.requireActual('~/lib/utils/simple_poll').default),
@@ -33,14 +34,8 @@ describe('MRWidgetMerging', () => {
});
it('renders information about merge request being merged', () => {
- expect(
- wrapper
- .find('.media-body')
- .text()
- .trim()
- .replace(/\s\s+/g, ' ')
- .replace(/[\r\n]+/g, ' '),
- ).toContain('Merging!');
+ const message = wrapper.findComponent(BoldText).props('message');
+ expect(message).toContain('Merging!');
});
describe('initiateMergePolling', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
index c6e7198c678..42515c597c5 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_not_allowed_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
describe('MRWidgetNotAllowed', () => {
let wrapper;
@@ -20,8 +21,9 @@ describe('MRWidgetNotAllowed', () => {
});
it('renders informative text', () => {
- expect(wrapper.text()).toContain('Ready to be merged automatically.');
- expect(wrapper.text()).toContain(
+ const message = wrapper.findComponent(BoldText).props('message');
+ expect(message).toContain('Ready to be merged automatically.');
+ expect(message).toContain(
'Ask someone with write access to this repository to merge this request',
);
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
index 4219ad70b4c..c0197b5e20a 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import PipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
describe('MRWidgetPipelineBlocked', () => {
let wrapper;
@@ -20,8 +21,10 @@ describe('MRWidgetPipelineBlocked', () => {
});
it('renders information text', () => {
- expect(wrapper.text()).toBe(
- "Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.",
+ const message = wrapper.findComponent(BoldText).props('message');
+ expect(message).toContain('Merge blocked:');
+ expect(message).toContain(
+ "pipeline must succeed. It's waiting for a manual action to continue.",
);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
index bd158d59d74..8bae2b62ed1 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_pipeline_failed_spec.js
@@ -1,7 +1,9 @@
import { GlSprintf, GlLink } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
+import { removeBreakLine } from 'helpers/text_helper';
import PipelineFailed from '~/vue_merge_request_widget/components/states/pipeline_failed.vue';
import StatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
+import BoldText from '~/vue_merge_request_widget/components/bold_text.vue';
describe('PipelineFailed', () => {
let wrapper;
@@ -32,16 +34,20 @@ describe('PipelineFailed', () => {
it('should render error message with a disabled merge button', () => {
createComponent();
- expect(wrapper.text()).toContain('Merge blocked: pipeline must succeed.');
- expect(wrapper.text()).toContain('Push a commit that fixes the failure');
+ const text = removeBreakLine(wrapper.text()).trim();
+ expect(text).toContain('Merge blocked:');
+ expect(text).toContain('pipeline must succeed');
+ expect(text).toContain('Push a commit that fixes the failure');
expect(wrapper.findComponent(GlLink).text()).toContain('learn about other solutions');
});
it('should render pipeline blocked message', () => {
createComponent({ isPipelineBlocked: true });
- expect(wrapper.text()).toContain(
- "Merge blocked: pipeline must succeed. It's waiting for a manual action to continue.",
+ const message = wrapper.findComponent(BoldText).props('message');
+ expect(message).toContain('Merge blocked:');
+ expect(message).toContain(
+ "pipeline must succeed. It's waiting for a manual action to continue.",
);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
index d34fc0c1e61..1e4e089e7c1 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_ready_to_merge_spec.js
@@ -79,13 +79,10 @@ const createTestService = () => ({
Vue.use(VueApollo);
+let service;
let wrapper;
let readyToMergeResponseSpy;
-const findMergeButton = () => wrapper.find('[data-testid="merge-button"]');
-const findPipelineFailedConfirmModal = () =>
- wrapper.findComponent(MergeFailedPipelineConfirmationDialog);
-
const createReadyToMergeResponse = (customMr) => {
return produce(readyToMergeResponse, (draft) => {
Object.assign(draft.data.project.mergeRequest, customMr);
@@ -96,7 +93,7 @@ const createComponent = (customConfig = {}, createState = true) => {
wrapper = shallowMount(ReadyToMerge, {
propsData: {
mr: createTestMr(customConfig),
- service: createTestService(),
+ service,
},
data() {
if (createState) {
@@ -119,6 +116,13 @@ const createComponent = (customConfig = {}, createState = true) => {
});
};
+const findMergeButton = () => wrapper.find('[data-testid="merge-button"]');
+const findMergeImmediatelyDropdown = () =>
+ wrapper.find('[data-testid="merge-immediately-dropdown"');
+const findSourceBranchDeletedText = () =>
+ wrapper.find('[data-testid="source-branch-deleted-text"]');
+const findPipelineFailedConfirmModal = () =>
+ wrapper.findComponent(MergeFailedPipelineConfirmationDialog);
const findCheckboxElement = () => wrapper.findComponent(SquashBeforeMerge);
const findCommitEditElements = () => wrapper.findAllComponents(CommitEdit);
const findCommitDropdownElement = () => wrapper.findComponent(CommitMessageDropdown);
@@ -129,33 +133,20 @@ const findCommitEditWithInputId = (inputId) =>
const findMergeCommitMessage = () => findCommitEditWithInputId('merge-message-edit').props('value');
const findSquashCommitMessage = () =>
findCommitEditWithInputId('squash-message-edit').props('value');
+const findDeleteSourceBranchCheckbox = () =>
+ wrapper.find('[data-testid="delete-source-branch-checkbox"]');
const triggerApprovalUpdated = () => eventHub.$emit('ApprovalUpdated');
+const triggerEditCommitInput = () =>
+ wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
describe('ReadyToMerge', () => {
beforeEach(() => {
+ service = createTestService();
readyToMergeResponseSpy = jest.fn().mockResolvedValueOnce(readyToMergeResponse);
});
- afterEach(() => {
- wrapper.destroy();
- });
-
describe('computed', () => {
- describe('isAutoMergeAvailable', () => {
- it('should return true when at least one merge strategy is available', () => {
- createComponent({});
-
- expect(wrapper.vm.isAutoMergeAvailable).toBe(true);
- });
-
- it('should return false when no merge strategies are available', () => {
- createComponent({ mr: { availableAutoMergeStrategies: [] } });
-
- expect(wrapper.vm.isAutoMergeAvailable).toBe(false);
- });
- });
-
describe('status', () => {
it('defaults to success', () => {
createComponent({ mr: { pipeline: true, availableAutoMergeStrategies: [] } });
@@ -190,16 +181,6 @@ describe('ReadyToMerge', () => {
});
});
- describe('Merge Button Variant', () => {
- it('defaults to confirm class', () => {
- createComponent({
- mr: { availableAutoMergeStrategies: [], mergeable: true },
- });
-
- expect(findMergeButton().attributes('variant')).toBe('confirm');
- });
- });
-
describe('status icon', () => {
it('defaults to tick icon', () => {
createComponent({ mr: { mergeable: true } });
@@ -219,334 +200,313 @@ describe('ReadyToMerge', () => {
expect(wrapper.vm.iconClass).toEqual('success');
});
});
+ });
- describe('mergeButtonText', () => {
- it('should return "Merge" when no auto merge strategies are available', () => {
- createComponent({ mr: { availableAutoMergeStrategies: [] } });
+ describe('merge button text', () => {
+ it('should return "Merge" when no auto merge strategies are available', () => {
+ createComponent({ mr: { availableAutoMergeStrategies: [] } });
+
+ expect(findMergeButton().text()).toBe('Merge');
+ });
- expect(wrapper.vm.mergeButtonText).toEqual('Merge');
+ it('should return "Merge when pipeline succeeds" when the MWPS auto merge strategy is available', () => {
+ createComponent({
+ mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY },
});
- it('should return "Merge in progress"', async () => {
- createComponent();
+ expect(findMergeButton().text()).toBe('Merge when pipeline succeeds');
+ });
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ isMergingImmediately: true });
+ it('should return Merge when pipeline succeeds', () => {
+ createComponent({ mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY } });
- await nextTick();
+ expect(findMergeButton().text()).toBe('Merge when pipeline succeeds');
+ });
+ });
- expect(wrapper.vm.mergeButtonText).toEqual('Merge in progress');
+ describe('merge immediately dropdown', () => {
+ it('dropdown should be hidden if no pipeline is active', () => {
+ createComponent({
+ mr: { isPipelineActive: false, onlyAllowMergeIfPipelineSucceeds: false },
});
- it('should return "Merge when pipeline succeeds" when the MWPS auto merge strategy is available', () => {
- createComponent({
- mr: { isMergingImmediately: false, preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY },
- });
+ expect(findMergeImmediatelyDropdown().exists()).toBe(false);
+ });
- expect(wrapper.vm.mergeButtonText).toEqual('Merge when pipeline succeeds');
- });
+ it('dropdown should be hidden if "Pipelines must succeed" is enabled', () => {
+ createComponent({ mr: { isPipelineActive: true, onlyAllowMergeIfPipelineSucceeds: true } });
+
+ expect(findMergeImmediatelyDropdown().exists()).toBe(false);
});
+ });
- describe('autoMergeText', () => {
- it('should return Merge when pipeline succeeds', () => {
- createComponent({ mr: { preferredAutoMergeStrategy: MWPS_MERGE_STRATEGY } });
+ describe('merge button disabled state', () => {
+ it('should not be disabled initally', () => {
+ createComponent();
- expect(wrapper.vm.autoMergeText).toEqual('Merge when pipeline succeeds');
- });
+ expect(findMergeButton().props('disabled')).toBe(false);
});
- describe('shouldShowMergeImmediatelyDropdown', () => {
- it('should return false if no pipeline is active', () => {
- createComponent({
- mr: { isPipelineActive: false, onlyAllowMergeIfPipelineSucceeds: false },
- });
+ it('should be disabled when there is no commit message', () => {
+ createComponent({ mr: { commitMessage: '' } });
- expect(wrapper.vm.shouldShowMergeImmediatelyDropdown).toBe(false);
- });
+ expect(findMergeButton().props('disabled')).toBe(true);
+ });
- it('should return false if "Pipelines must succeed" is enabled for the current project', () => {
- createComponent({ mr: { isPipelineActive: true, onlyAllowMergeIfPipelineSucceeds: true } });
+ it('should be disabled if merge is not allowed', () => {
+ createComponent({ mr: { preventMerge: true } });
- expect(wrapper.vm.shouldShowMergeImmediatelyDropdown).toBe(false);
- });
+ expect(findMergeButton().props('disabled')).toBe(true);
});
- describe('isMergeButtonDisabled', () => {
- it('should return false with initial data', () => {
- createComponent({ mr: { isMergeAllowed: true, mergeable: false } });
+ it('should be disabled when making request', async () => {
+ createComponent({ mr: { isMergeAllowed: true } }, true);
- expect(wrapper.vm.isMergeButtonDisabled).toBe(false);
- });
+ findMergeButton().vm.$emit('click');
- it('should return true when there is no commit message', () => {
- createComponent({ mr: { isMergeAllowed: true, commitMessage: '' } });
+ await nextTick();
- expect(wrapper.vm.isMergeButtonDisabled).toBe(true);
- });
+ expect(findMergeButton().props('disabled')).toBe(true);
+ });
+ });
- it('should return true if merge is not allowed', () => {
+ describe('sourceBranchDeletedText', () => {
+ const should = 'Source branch will be deleted.';
+ const shouldNot = 'Source branch will not be deleted.';
+ const did = 'Deleted the source branch.';
+ const didNot = 'Did not delete the source branch.';
+ const scenarios = [
+ "the MR hasn't merged yet, and the backend-provided value expects to delete the branch",
+ "the MR hasn't merged yet, and the backend-provided value expects to leave the branch",
+ "the MR hasn't merged yet, and the backend-provided value is a non-boolean falsey value",
+ "the MR hasn't merged yet, and the backend-provided value is a non-boolean truthy value",
+ 'the MR has been merged, and the backend reports that the branch has been removed',
+ 'the MR has been merged, and the backend reports that the branch has not been removed',
+ 'the MR has been merged, and the backend reports a non-boolean falsey value',
+ 'the MR has been merged, and the backend reports a non-boolean truthy value',
+ ];
+
+ it.each`
+ describe | premerge | mrShould | mrRemoved | output
+ ${scenarios[0]} | ${true} | ${true} | ${null} | ${should}
+ ${scenarios[1]} | ${true} | ${false} | ${null} | ${shouldNot}
+ ${scenarios[2]} | ${true} | ${null} | ${null} | ${shouldNot}
+ ${scenarios[3]} | ${true} | ${'yeah'} | ${null} | ${should}
+ ${scenarios[4]} | ${false} | ${null} | ${true} | ${did}
+ ${scenarios[5]} | ${false} | ${null} | ${false} | ${didNot}
+ ${scenarios[6]} | ${false} | ${null} | ${null} | ${didNot}
+ ${scenarios[7]} | ${false} | ${null} | ${'yep'} | ${did}
+ `(
+ 'in the case that $describe, returns "$output"',
+ ({ premerge, mrShould, mrRemoved, output }) => {
createComponent({
mr: {
- isMergeAllowed: false,
- availableAutoMergeStrategies: [],
- onlyAllowMergeIfPipelineSucceeds: true,
- mergeable: false,
+ state: !premerge ? 'merged' : 'literally-anything-else',
+ shouldRemoveSourceBranch: mrShould,
+ sourceBranchRemoved: mrRemoved,
+ autoMergeEnabled: true,
},
});
- expect(wrapper.vm.isMergeButtonDisabled).toBe(true);
- });
-
- it('should return true when the vm instance is making request', async () => {
- createComponent({ mr: { isMergeAllowed: true } });
-
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ isMakingRequest: true });
-
- await nextTick();
-
- expect(wrapper.vm.isMergeButtonDisabled).toBe(true);
- });
- });
-
- describe('sourceBranchDeletedText', () => {
- const should = 'Source branch will be deleted.';
- const shouldNot = 'Source branch will not be deleted.';
- const did = 'Deleted the source branch.';
- const didNot = 'Did not delete the source branch.';
- const scenarios = [
- "the MR hasn't merged yet, and the backend-provided value expects to delete the branch",
- "the MR hasn't merged yet, and the backend-provided value expects to leave the branch",
- "the MR hasn't merged yet, and the backend-provided value is a non-boolean falsey value",
- "the MR hasn't merged yet, and the backend-provided value is a non-boolean truthy value",
- 'the MR has been merged, and the backend reports that the branch has been removed',
- 'the MR has been merged, and the backend reports that the branch has not been removed',
- 'the MR has been merged, and the backend reports a non-boolean falsey value',
- 'the MR has been merged, and the backend reports a non-boolean truthy value',
- ];
-
- it.each`
- describe | premerge | mrShould | mrRemoved | output
- ${scenarios[0]} | ${true} | ${true} | ${null} | ${should}
- ${scenarios[1]} | ${true} | ${false} | ${null} | ${shouldNot}
- ${scenarios[2]} | ${true} | ${null} | ${null} | ${shouldNot}
- ${scenarios[3]} | ${true} | ${'yeah'} | ${null} | ${should}
- ${scenarios[4]} | ${false} | ${null} | ${true} | ${did}
- ${scenarios[5]} | ${false} | ${null} | ${false} | ${didNot}
- ${scenarios[6]} | ${false} | ${null} | ${null} | ${didNot}
- ${scenarios[7]} | ${false} | ${null} | ${'yep'} | ${did}
- `(
- 'in the case that $describe, returns "$output"',
- ({ premerge, mrShould, mrRemoved, output }) => {
- createComponent({
- mr: {
- state: !premerge ? 'merged' : 'literally-anything-else',
- shouldRemoveSourceBranch: mrShould,
- sourceBranchRemoved: mrRemoved,
- },
- });
-
- expect(wrapper.vm.sourceBranchDeletedText).toBe(output);
- },
- );
- });
+ expect(findSourceBranchDeletedText().text()).toBe(output);
+ },
+ );
});
- describe('methods', () => {
- describe('handleMergeButtonClick', () => {
- const response = (status) => ({
- data: {
- status,
- },
+ describe('Merge Button Variant', () => {
+ it('defaults to confirm class', () => {
+ createComponent({
+ mr: { availableAutoMergeStrategies: [], mergeable: true },
});
- beforeEach(() => {
- readyToMergeResponseSpy = jest
- .fn()
- .mockResolvedValueOnce(createReadyToMergeResponse({ squash: true, squashOnMerge: true }))
- .mockResolvedValue(
- createReadyToMergeResponse({
- squash: true,
- squashOnMerge: true,
- defaultMergeCommitMessage: '',
- defaultSquashCommitMessage: '',
- }),
- );
- });
+ expect(findMergeButton().attributes('variant')).toBe('confirm');
+ });
+ });
- it('should handle merge when pipeline succeeds', async () => {
- createComponent();
+ describe('Merge button click', () => {
+ const response = (status) => ({
+ data: {
+ status,
+ },
+ });
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest
- .spyOn(wrapper.vm.service, 'merge')
- .mockResolvedValue(response('merge_when_pipeline_succeeds'));
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({ removeSourceBranch: false });
+ beforeEach(() => {
+ readyToMergeResponseSpy = jest
+ .fn()
+ .mockResolvedValueOnce(createReadyToMergeResponse({ squash: true, squashOnMerge: true }))
+ .mockResolvedValue(
+ createReadyToMergeResponse({
+ squash: true,
+ squashOnMerge: true,
+ defaultMergeCommitMessage: '',
+ defaultSquashCommitMessage: '',
+ }),
+ );
+ });
- wrapper.vm.handleMergeButtonClick(true);
+ it('should handle merge when pipeline succeeds', async () => {
+ createComponent({ mr: { shouldRemoveSourceBranch: false } }, true);
- await waitForPromises();
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(service, 'merge').mockResolvedValue(response('merge_when_pipeline_succeeds'));
- expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
- expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
- transition: 'start-auto-merge',
- });
+ findMergeButton().vm.$emit('click');
- const params = wrapper.vm.service.merge.mock.calls[0][0];
+ await waitForPromises();
- expect(params).toEqual(
- expect.objectContaining({
- sha: wrapper.vm.mr.sha,
- commit_message: wrapper.vm.mr.commitMessage,
- should_remove_source_branch: false,
- auto_merge_strategy: 'merge_when_pipeline_succeeds',
- }),
- );
+ expect(eventHub.$emit).toHaveBeenCalledWith('MRWidgetUpdateRequested');
+ expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
+ transition: 'start-auto-merge',
});
- it('should handle merge failed', async () => {
- createComponent();
+ const params = service.merge.mock.calls[0][0];
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'merge').mockResolvedValue(response('failed'));
- wrapper.vm.handleMergeButtonClick(false, true);
+ expect(params).toEqual(
+ expect.objectContaining({
+ sha: '12345678',
+ commit_message: commitMessage,
+ should_remove_source_branch: false,
+ auto_merge_strategy: 'merge_when_pipeline_succeeds',
+ }),
+ );
+ });
- await waitForPromises();
+ it('should handle merge failed', async () => {
+ createComponent({ mr: { availableAutoMergeStrategies: [] } });
- expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(service, 'merge').mockResolvedValue(response('failed'));
- const params = wrapper.vm.service.merge.mock.calls[0][0];
+ findMergeButton().vm.$emit('click');
- expect(params.should_remove_source_branch).toBe(true);
- expect(params.auto_merge_strategy).toBeUndefined();
- });
+ await waitForPromises();
- it('should handle merge action accepted case', async () => {
- createComponent();
+ expect(eventHub.$emit).toHaveBeenCalledWith('FailedToMerge', undefined);
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'merge').mockResolvedValue(response('success'));
- jest.spyOn(wrapper.vm.mr, 'transitionStateMachine');
- wrapper.vm.handleMergeButtonClick();
+ const params = service.merge.mock.calls[0][0];
- expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
- transition: 'start-merge',
- });
+ expect(params.should_remove_source_branch).toBe(true);
+ expect(params.auto_merge_strategy).toBeUndefined();
+ });
- await waitForPromises();
+ it('should handle merge action accepted case', async () => {
+ createComponent({ mr: { availableAutoMergeStrategies: [] } });
- expect(wrapper.vm.mr.transitionStateMachine).toHaveBeenCalledWith({
- transition: 'start-merge',
- });
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(service, 'merge').mockResolvedValue(response('success'));
+ jest.spyOn(wrapper.vm.mr, 'transitionStateMachine');
- const params = wrapper.vm.service.merge.mock.calls[0][0];
+ findMergeButton().vm.$emit('click');
- expect(params.should_remove_source_branch).toBe(true);
- expect(params.auto_merge_strategy).toBeUndefined();
+ expect(eventHub.$emit).toHaveBeenCalledWith('StateMachineValueChanged', {
+ transition: 'start-merge',
});
- it('hides edit commit message', async () => {
- createComponent({}, true, true);
+ await waitForPromises();
- await waitForPromises();
+ expect(wrapper.vm.mr.transitionStateMachine).toHaveBeenCalledWith({
+ transition: 'start-merge',
+ });
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'merge').mockResolvedValue(response('success'));
+ const params = service.merge.mock.calls[0][0];
- await wrapper
- .findComponent('[data-testid="widget_edit_commit_message"]')
- .vm.$emit('input', true);
+ expect(params.should_remove_source_branch).toBe(true);
+ expect(params.auto_merge_strategy).toBeUndefined();
+ });
- expect(wrapper.findComponent('[data-testid="edit_commit_message"]').exists()).toBe(true);
+ it('hides edit commit message', async () => {
+ createComponent();
- wrapper.vm.handleMergeButtonClick();
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(service, 'merge').mockResolvedValue(response('success'));
- await waitForPromises();
+ await triggerEditCommitInput();
- expect(wrapper.findComponent('[data-testid="edit_commit_message"]').exists()).toBe(false);
- });
+ expect(wrapper.findComponent('[data-testid="edit_commit_message"]').exists()).toBe(true);
+
+ findMergeButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(wrapper.findComponent('[data-testid="edit_commit_message"]').exists()).toBe(false);
});
+ });
- describe('initiateRemoveSourceBranchPolling', () => {
- it('should emit event and call simplePoll', () => {
- createComponent();
+ describe('initiateRemoveSourceBranchPolling', () => {
+ it('should emit event and call simplePoll', () => {
+ createComponent();
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- wrapper.vm.initiateRemoveSourceBranchPolling();
+ wrapper.vm.initiateRemoveSourceBranchPolling();
- expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]);
- expect(simplePoll).toHaveBeenCalled();
- });
+ expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [true]);
+ expect(simplePoll).toHaveBeenCalled();
});
+ });
- describe('handleRemoveBranchPolling', () => {
- const response = (state) => ({
- data: {
- source_branch_exists: state,
- },
- });
+ describe('handleRemoveBranchPolling', () => {
+ const response = (state) => ({
+ data: {
+ source_branch_exists: state,
+ },
+ });
- it('should call start and stop polling when MR merged', async () => {
- createComponent();
+ it('should call start and stop polling when MR merged', async () => {
+ createComponent();
- jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
- jest.spyOn(wrapper.vm.service, 'poll').mockResolvedValue(response(false));
+ jest.spyOn(eventHub, '$emit').mockImplementation(() => {});
+ jest.spyOn(service, 'poll').mockResolvedValue(response(false));
- let cpc = false; // continuePollingCalled
- let spc = false; // stopPollingCalled
+ let cpc = false; // continuePollingCalled
+ let spc = false; // stopPollingCalled
- wrapper.vm.handleRemoveBranchPolling(
- () => {
- cpc = true;
- },
- () => {
- spc = true;
- },
- );
+ wrapper.vm.handleRemoveBranchPolling(
+ () => {
+ cpc = true;
+ },
+ () => {
+ spc = true;
+ },
+ );
- await waitForPromises();
+ await waitForPromises();
- expect(wrapper.vm.service.poll).toHaveBeenCalled();
+ expect(service.poll).toHaveBeenCalled();
- const args = eventHub.$emit.mock.calls[0];
+ const args = eventHub.$emit.mock.calls[0];
- expect(args[0]).toEqual('MRWidgetUpdateRequested');
- expect(args[1]).toBeDefined();
- args[1]();
+ expect(args[0]).toEqual('MRWidgetUpdateRequested');
+ expect(args[1]).toBeDefined();
+ args[1]();
- expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [false]);
+ expect(eventHub.$emit).toHaveBeenCalledWith('SetBranchRemoveFlag', [false]);
- expect(cpc).toBe(false);
- expect(spc).toBe(true);
- });
+ expect(cpc).toBe(false);
+ expect(spc).toBe(true);
+ });
- it('should continue polling until MR is merged', async () => {
- createComponent();
+ it('should continue polling until MR is merged', async () => {
+ createComponent();
- jest.spyOn(wrapper.vm.service, 'poll').mockResolvedValue(response(true));
+ jest.spyOn(service, 'poll').mockResolvedValue(response(true));
- let cpc = false; // continuePollingCalled
- let spc = false; // stopPollingCalled
+ let cpc = false; // continuePollingCalled
+ let spc = false; // stopPollingCalled
- wrapper.vm.handleRemoveBranchPolling(
- () => {
- cpc = true;
- },
- () => {
- spc = true;
- },
- );
+ wrapper.vm.handleRemoveBranchPolling(
+ () => {
+ cpc = true;
+ },
+ () => {
+ spc = true;
+ },
+ );
- await waitForPromises();
+ await waitForPromises();
- expect(cpc).toBe(true);
- expect(spc).toBe(false);
- });
+ expect(cpc).toBe(true);
+ expect(spc).toBe(false);
});
});
@@ -563,7 +523,7 @@ describe('ReadyToMerge', () => {
},
});
- expect(wrapper.find('#remove-source-branch-input').exists()).toBe(false);
+ expect(findDeleteSourceBranchCheckbox().exists()).toBe(false);
});
});
@@ -575,7 +535,7 @@ describe('ReadyToMerge', () => {
});
it('isRemoveSourceBranchButtonDisabled should be false', () => {
- expect(wrapper.find('#remove-source-branch-input').props('disabled')).toBe(undefined);
+ expect(findDeleteSourceBranchCheckbox().props('disabled')).toBe(undefined);
});
});
});
@@ -646,7 +606,7 @@ describe('ReadyToMerge', () => {
},
});
- expect(findCommitEditElements().length).toBe(0);
+ expect(findCommitEditElements()).toHaveLength(0);
});
it('should not be rendered if squash before merge is disabled', () => {
@@ -659,7 +619,7 @@ describe('ReadyToMerge', () => {
},
});
- expect(findCommitEditElements().length).toBe(0);
+ expect(findCommitEditElements()).toHaveLength(0);
});
it('should not be rendered if there is only one commit', () => {
@@ -672,7 +632,7 @@ describe('ReadyToMerge', () => {
},
});
- expect(findCommitEditElements().length).toBe(0);
+ expect(findCommitEditElements()).toHaveLength(0);
});
it('should have one edit component if squash is enabled and there is more than 1 commit', async () => {
@@ -686,9 +646,9 @@ describe('ReadyToMerge', () => {
},
});
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
- expect(findCommitEditElements().length).toBe(1);
+ expect(findCommitEditElements()).toHaveLength(1);
expect(findFirstCommitEditLabel()).toBe('Squash commit message');
});
});
@@ -702,16 +662,15 @@ describe('ReadyToMerge', () => {
},
});
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
- expect(findCommitEditElements().length).toBe(2);
+ expect(findCommitEditElements()).toHaveLength(2);
});
- it('should have two edit components when squash is enabled and there is more than 1 commit and mergeRequestWidgetGraphql is enabled', async () => {
+ it('should have two edit components when squash is enabled', async () => {
createComponent(
{
mr: {
- commitsCount: 2,
squashIsSelected: true,
enableSquashBeforeMerge: true,
},
@@ -719,37 +678,9 @@ describe('ReadyToMerge', () => {
true,
);
- // setData usage is discouraged. See https://gitlab.com/groups/gitlab-org/-/epics/7330 for details
- // eslint-disable-next-line no-restricted-syntax
- wrapper.setData({
- loading: false,
- state: {
- ...createTestMr({}),
- userPermissions: {},
- squash: true,
- mergeable: true,
- commitCount: 2,
- commitsWithoutMergeCommits: {},
- },
- });
- await nextTick();
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
-
- expect(findCommitEditElements().length).toBe(2);
- });
-
- it('should have one edit components when squash is enabled and there is 1 commit only', async () => {
- createComponent({
- mr: {
- commitsCount: 1,
- squash: true,
- enableSquashBeforeMerge: true,
- },
- });
+ await triggerEditCommitInput();
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
-
- expect(findCommitEditElements().length).toBe(1);
+ expect(findCommitEditElements()).toHaveLength(2);
});
it('should have correct edit squash commit label', async () => {
@@ -761,7 +692,7 @@ describe('ReadyToMerge', () => {
},
});
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
expect(findFirstCommitEditLabel()).toBe('Squash commit message');
});
@@ -779,7 +710,7 @@ describe('ReadyToMerge', () => {
mr: { enableSquashBeforeMerge: true, squashIsSelected: true, commitsCount: 2 },
});
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
expect(findCommitDropdownElement().exists()).toBe(true);
});
@@ -788,7 +719,7 @@ describe('ReadyToMerge', () => {
it('renders a tip including a link to docs on templates', async () => {
createComponent();
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
expect(findTipLink().exists()).toBe(true);
});
@@ -891,7 +822,8 @@ describe('ReadyToMerge', () => {
createDefaultGqlComponent();
await waitForPromises();
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+
+ await triggerEditCommitInput();
expect(finderFn()).toBe(initialValue);
});
@@ -899,7 +831,7 @@ describe('ReadyToMerge', () => {
it('should have updated value after graphql refetch', async () => {
createDefaultGqlComponent();
await waitForPromises();
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
triggerApprovalUpdated();
await waitForPromises();
@@ -910,7 +842,7 @@ describe('ReadyToMerge', () => {
it('should not update if user has touched', async () => {
createDefaultGqlComponent();
await waitForPromises();
- await wrapper.find('[data-testid="widget_edit_commit_message"]').vm.$emit('input', true);
+ await triggerEditCommitInput();
const input = wrapper.find(inputId);
input.element.value = USER_COMMIT_MESSAGE;
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
index 2a343997cf5..aaa4591d67d 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_sha_mismatch_spec.js
@@ -25,7 +25,7 @@ describe('ShaMismatch', () => {
});
it('should render warning message', () => {
- expect(wrapper.element.innerText).toContain(I18N_SHA_MISMATCH.warningMessage);
+ expect(wrapper.text()).toContain('Merge blocked: new changes were just added.');
});
it('action button should have correct label', () => {
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
index e2d79c61b9b..c97b42f61ac 100644
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_unresolved_discussions_spec.js
@@ -1,5 +1,6 @@
import { mount } from '@vue/test-utils';
import { TEST_HOST } from 'helpers/test_constants';
+import { removeBreakLine } from 'helpers/text_helper';
import notesEventHub from '~/notes/event_hub';
import UnresolvedDiscussions from '~/vue_merge_request_widget/components/states/unresolved_discussions.vue';
@@ -42,7 +43,9 @@ describe('UnresolvedDiscussions', () => {
});
it('should have correct elements', () => {
- expect(wrapper.element.innerText).toContain(`Merge blocked: all threads must be resolved.`);
+ const text = removeBreakLine(wrapper.text()).trim();
+ expect(text).toContain('Merge blocked:');
+ expect(text).toContain('all threads must be resolved.');
expect(wrapper.element.innerText).toContain('Jump to first unresolved thread');
expect(wrapper.element.innerText).toContain('Create issue to resolve all threads');
@@ -54,7 +57,9 @@ describe('UnresolvedDiscussions', () => {
describe('without threads path', () => {
it('should not show create issue link if user cannot create issue', () => {
- expect(wrapper.element.innerText).toContain(`Merge blocked: all threads must be resolved.`);
+ const text = removeBreakLine(wrapper.text()).trim();
+ expect(text).toContain('Merge blocked:');
+ expect(text).toContain('all threads must be resolved.');
expect(wrapper.element.innerText).toContain('Jump to first unresolved thread');
expect(wrapper.element.innerText).not.toContain('Create issue to resolve all threads');
diff --git a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js b/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
deleted file mode 100644
index 82aeac1a47d..00000000000
--- a/spec/frontend/vue_merge_request_widget/components/states/mr_widget_wip_spec.js
+++ /dev/null
@@ -1,42 +0,0 @@
-import { mount } from '@vue/test-utils';
-import WorkInProgress from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
-
-let wrapper;
-
-const createComponent = (updateMergeRequest = true) => {
- wrapper = mount(WorkInProgress, {
- propsData: {
- mr: {},
- },
- data() {
- return {
- userPermissions: {
- updateMergeRequest,
- },
- };
- },
- });
-};
-
-describe('Merge request widget draft state component', () => {
- afterEach(() => {
- wrapper.destroy();
- });
-
- describe('template', () => {
- it('should have correct elements', () => {
- createComponent(true);
-
- expect(wrapper.text()).toContain(
- "Merge blocked: merge request must be marked as ready. It's still marked as draft.",
- );
- expect(wrapper.find('[data-testid="removeWipButton"]').text()).toContain('Mark as ready');
- });
-
- it('should not show removeWIP button is user cannot update MR', () => {
- createComponent(false);
-
- expect(wrapper.find('[data-testid="removeWipButton"]').exists()).toBe(false);
- });
- });
-});
diff --git a/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
new file mode 100644
index 00000000000..e610ceb2122
--- /dev/null
+++ b/spec/frontend/vue_merge_request_widget/components/states/work_in_progress_spec.js
@@ -0,0 +1,182 @@
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import getStateQueryResponse from 'test_fixtures/graphql/merge_requests/get_state.query.graphql.json';
+import { createAlert } from '~/flash';
+import WorkInProgress, {
+ MSG_SOMETHING_WENT_WRONG,
+ MSG_MARK_READY,
+} from '~/vue_merge_request_widget/components/states/work_in_progress.vue';
+import draftQuery from '~/vue_merge_request_widget/queries/states/draft.query.graphql';
+import getStateQuery from '~/vue_merge_request_widget/queries/get_state.query.graphql';
+import removeDraftMutation from '~/vue_merge_request_widget/queries/toggle_draft.mutation.graphql';
+import MergeRequest from '~/merge_request';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+
+Vue.use(VueApollo);
+
+const TEST_PROJECT_ID = getStateQueryResponse.data.project.id;
+const TEST_MR_ID = getStateQueryResponse.data.project.mergeRequest.id;
+const TEST_MR_IID = '23';
+const TEST_MR_TITLE = 'Test MR Title';
+const TEST_PROJECT_PATH = 'lorem/ipsum';
+
+jest.mock('~/flash');
+jest.mock('~/merge_request');
+
+describe('~/vue_merge_request_widget/components/states/work_in_progress.vue', () => {
+ let wrapper;
+ let apolloProvider;
+
+ let draftQuerySpy;
+ let removeDraftMutationSpy;
+
+ const findWIPButton = () => wrapper.findByTestId('removeWipButton');
+
+ const createDraftQueryResponse = (canUpdateMergeRequest) => ({
+ data: {
+ project: {
+ __typename: 'Project',
+ id: TEST_PROJECT_ID,
+ mergeRequest: {
+ __typename: 'MergeRequest',
+ id: TEST_MR_ID,
+ userPermissions: {
+ updateMergeRequest: canUpdateMergeRequest,
+ },
+ },
+ },
+ },
+ });
+ const createRemoveDraftMutationResponse = () => ({
+ data: {
+ mergeRequestSetDraft: {
+ __typename: 'MergeRequestSetWipPayload',
+ errors: [],
+ mergeRequest: {
+ __typename: 'MergeRequest',
+ id: TEST_MR_ID,
+ title: TEST_MR_TITLE,
+ draft: false,
+ mergeableDiscussionsState: true,
+ },
+ },
+ },
+ });
+
+ const createComponent = async () => {
+ wrapper = mountExtended(WorkInProgress, {
+ apolloProvider,
+ propsData: {
+ mr: {
+ issuableId: TEST_MR_ID,
+ title: TEST_MR_TITLE,
+ iid: TEST_MR_IID,
+ targetProjectFullPath: TEST_PROJECT_PATH,
+ },
+ },
+ });
+
+ await waitForPromises();
+
+ // why: work_in_progress.vue has some coupling that this query has been read before
+ // for some reason this has to happen **after** the component has mounted
+ // or apollo throws errors.
+ apolloProvider.defaultClient.cache.writeQuery({
+ query: getStateQuery,
+ variables: {
+ projectPath: TEST_PROJECT_PATH,
+ iid: TEST_MR_IID,
+ },
+ data: getStateQueryResponse.data,
+ });
+ };
+
+ beforeEach(() => {
+ draftQuerySpy = jest.fn().mockResolvedValue(createDraftQueryResponse(true));
+ removeDraftMutationSpy = jest.fn().mockResolvedValue(createRemoveDraftMutationResponse());
+
+ apolloProvider = createMockApollo([
+ [draftQuery, draftQuerySpy],
+ [removeDraftMutation, removeDraftMutationSpy],
+ ]);
+ });
+
+ describe('when user can update MR', () => {
+ beforeEach(async () => {
+ await createComponent();
+ });
+
+ it('renders text', () => {
+ const message = wrapper.text();
+ expect(message).toContain('Merge blocked:');
+ expect(message).toContain('Select Mark as ready to remove it from Draft status.');
+ });
+
+ it('renders mark ready button', () => {
+ expect(findWIPButton().text()).toBe(MSG_MARK_READY);
+ });
+
+ it('does not call remove draft mutation', () => {
+ expect(removeDraftMutationSpy).not.toHaveBeenCalled();
+ });
+
+ describe('when mark ready button is clicked', () => {
+ beforeEach(async () => {
+ findWIPButton().vm.$emit('click');
+
+ await waitForPromises();
+ });
+
+ it('calls mutation spy', () => {
+ expect(removeDraftMutationSpy).toHaveBeenCalledWith({
+ draft: false,
+ iid: TEST_MR_IID,
+ projectPath: TEST_PROJECT_PATH,
+ });
+ });
+
+ it('does not create alert', () => {
+ expect(createAlert).not.toHaveBeenCalled();
+ });
+
+ it('calls toggleDraftStatus', () => {
+ expect(MergeRequest.toggleDraftStatus).toHaveBeenCalledWith(TEST_MR_TITLE, true);
+ });
+ });
+
+ describe('when mutation fails and ready button is clicked', () => {
+ beforeEach(async () => {
+ removeDraftMutationSpy.mockRejectedValue(new Error('TEST FAIL'));
+ findWIPButton().vm.$emit('click');
+
+ await waitForPromises();
+ });
+
+ it('creates alert', () => {
+ expect(createAlert).toHaveBeenCalledWith({
+ message: MSG_SOMETHING_WENT_WRONG,
+ });
+ });
+
+ it('does not call toggleDraftStatus', () => {
+ expect(MergeRequest.toggleDraftStatus).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('when user cannot update MR', () => {
+ beforeEach(async () => {
+ draftQuerySpy.mockResolvedValue(createDraftQueryResponse(false));
+
+ createComponent();
+
+ await waitForPromises();
+ });
+
+ it('does not render mark ready button', () => {
+ expect(findWIPButton().exists()).toBe(false);
+ });
+ });
+});
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 7e941c5ceaa..973866176c2 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
@@ -7,6 +7,8 @@ import StatusIcon from '~/vue_merge_request_widget/components/extensions/status_
import ActionButtons from '~/vue_merge_request_widget/components/widget/action_buttons.vue';
import Widget from '~/vue_merge_request_widget/components/widget/widget.vue';
import WidgetContentRow from '~/vue_merge_request_widget/components/widget/widget_content_row.vue';
+import * as logger from '~/lib/logger';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/vue_merge_request_widget/components/extensions/telemetry', () => ({
createTelemetryHub: jest.fn().mockReturnValue({
@@ -32,7 +34,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
isCollapsible: false,
loadingText: 'Loading widget',
widgetName: 'WidgetTest',
- fetchCollapsedData: () => Promise.resolve([]),
+ fetchCollapsedData: () => Promise.resolve({ headers: {}, status: HTTP_STATUS_OK }),
value: {
collapsed: null,
expanded: null,
@@ -56,7 +58,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('fetches collapsed', async () => {
const fetchCollapsedData = jest
.fn()
- .mockReturnValue(Promise.resolve({ headers: {}, status: 200, data: {} }));
+ .mockReturnValue(Promise.resolve({ headers: {}, status: HTTP_STATUS_OK, data: {} }));
createComponent({ propsData: { fetchCollapsedData } });
await waitForPromises();
@@ -83,7 +85,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('displays loading icon until request is made and then displays status icon when the request is complete', async () => {
const fetchCollapsedData = jest
.fn()
- .mockReturnValue(Promise.resolve({ headers: {}, status: 200, data: {} }));
+ .mockReturnValue(Promise.resolve({ headers: {}, status: HTTP_STATUS_OK, data: {} }));
createComponent({ propsData: { fetchCollapsedData, statusIconName: 'warning' } });
@@ -122,15 +124,23 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
describe('fetch', () => {
it('sets the data.collapsed property after a successfull call - multiPolling: false', async () => {
- const mockData = { headers: {}, status: 200, data: { vulnerabilities: [] } };
+ const mockData = { headers: {}, status: HTTP_STATUS_OK, data: { vulnerabilities: [] } };
createComponent({ propsData: { fetchCollapsedData: async () => mockData } });
await waitForPromises();
expect(wrapper.emitted('input')[0][0]).toEqual({ collapsed: mockData.data, expanded: null });
});
it('sets the data.collapsed property after a successfull call - multiPolling: true', async () => {
- const mockData1 = { headers: {}, status: 200, data: { vulnerabilities: [{ vuln: 1 }] } };
- const mockData2 = { headers: {}, status: 200, data: { vulnerabilities: [{ vuln: 2 }] } };
+ const mockData1 = {
+ headers: {},
+ status: HTTP_STATUS_OK,
+ data: { vulnerabilities: [{ vuln: 1 }] },
+ };
+ const mockData2 = {
+ headers: {},
+ status: HTTP_STATUS_OK,
+ data: { vulnerabilities: [{ vuln: 2 }] },
+ };
createComponent({
propsData: {
@@ -150,6 +160,21 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
});
});
+ it('throws an error when the handler does not include headers or status objects', async () => {
+ const error = new Error(Widget.MISSING_RESPONSE_HEADERS);
+ jest.spyOn(Sentry, 'captureException').mockImplementation();
+ jest.spyOn(logger, 'logError').mockImplementation();
+ createComponent({
+ propsData: {
+ fetchCollapsedData: () => Promise.resolve({}),
+ },
+ });
+ await waitForPromises();
+ expect(wrapper.emitted('input')).toBeUndefined();
+ expect(Sentry.captureException).toHaveBeenCalledWith(error);
+ expect(logger.logError).toHaveBeenCalledWith(error.message);
+ });
+
it('calls sentry when failed', async () => {
const error = new Error('Something went wrong');
jest.spyOn(Sentry, 'captureException').mockImplementation();
@@ -279,13 +304,13 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
it('fetches expanded data when clicked for the first time', async () => {
const mockDataCollapsed = {
headers: {},
- status: 200,
+ status: HTTP_STATUS_OK,
data: { vulnerabilities: [{ vuln: 1 }] },
};
const mockDataExpanded = {
headers: {},
- status: 200,
+ status: HTTP_STATUS_OK,
data: { vulnerabilities: [{ vuln: 2 }] },
};
@@ -377,7 +402,7 @@ describe('~/vue_merge_request_widget/components/widget/widget.vue', () => {
isCollapsible: true,
actionButtons: [
{
- fullReport: true,
+ trackFullReportClicked: true,
href: '#',
target: '_blank',
id: 'full-report-button',
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
index 16c2adaffaf..e23cd92f53e 100644
--- 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
@@ -82,11 +82,8 @@ describe('vue_merge_request_widget/extensions/security_reports/mr_widget_securit
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', () => {
+ it('does not render the widget', () => {
+ expect(wrapper.html()).toBe('');
expect(findDropdown().exists()).toBe(false);
});
});
diff --git a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
index d9faa7b2d25..13384e1efca 100644
--- a/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
+++ b/spec/frontend/vue_merge_request_widget/extentions/terraform/index_spec.js
@@ -3,6 +3,7 @@ import { mountExtended } from 'helpers/vue_test_utils_helper';
import waitForPromises from 'helpers/wait_for_promises';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import extensionsContainer from '~/vue_merge_request_widget/components/extensions/container';
import { registerExtension } from '~/vue_merge_request_widget/components/extensions';
@@ -22,8 +23,6 @@ describe('Terraform extension', () => {
let mock;
const endpoint = '/path/to/terraform/report.json';
- const successStatusCode = 200;
- const errorStatusCode = 500;
const findListItem = (at) => wrapper.findAllByTestId('extension-list-item').at(at);
@@ -57,7 +56,7 @@ describe('Terraform extension', () => {
describe('while loading', () => {
const loadingText = 'Loading Terraform reports...';
it('should render loading text', async () => {
- mockPollingApi(successStatusCode, plans, {});
+ mockPollingApi(HTTP_STATUS_OK, plans, {});
createComponent();
expect(wrapper.text()).toContain(loadingText);
@@ -68,7 +67,7 @@ describe('Terraform extension', () => {
describe('when the fetching fails', () => {
beforeEach(() => {
- mockPollingApi(errorStatusCode, null, {});
+ mockPollingApi(HTTP_STATUS_INTERNAL_SERVER_ERROR, null, {});
return createComponent();
});
@@ -85,7 +84,7 @@ describe('Terraform extension', () => {
${'1 valid and 2 invalid reports'} | ${{ 0: validPlanWithName, 1: invalidPlanWithName, 2: invalidPlanWithName }} | ${'Terraform report was generated in your pipelines'} | ${'2 Terraform reports failed to generate'}
`('and received $responseType', ({ response, summaryTitle, summarySubtitle }) => {
beforeEach(async () => {
- mockPollingApi(successStatusCode, response, {});
+ mockPollingApi(HTTP_STATUS_OK, response, {});
return createComponent();
});
@@ -102,7 +101,7 @@ describe('Terraform extension', () => {
describe('expanded data', () => {
beforeEach(async () => {
- mockPollingApi(successStatusCode, plans, {});
+ mockPollingApi(HTTP_STATUS_OK, plans, {});
await createComponent();
wrapper.findByTestId('toggle-button').trigger('click');
@@ -164,7 +163,7 @@ describe('Terraform extension', () => {
describe('successful poll', () => {
beforeEach(() => {
- mockPollingApi(successStatusCode, plans, {});
+ mockPollingApi(HTTP_STATUS_OK, plans, {});
return createComponent();
});
@@ -176,7 +175,7 @@ describe('Terraform extension', () => {
describe('polling fails', () => {
beforeEach(() => {
- mockPollingApi(errorStatusCode, null, {});
+ mockPollingApi(HTTP_STATUS_INTERNAL_SERVER_ERROR, null, {});
return createComponent();
});
diff --git a/spec/frontend/vue_merge_request_widget/mock_data.js b/spec/frontend/vue_merge_request_widget/mock_data.js
index 20d00a116bb..46e1919b0ea 100644
--- a/spec/frontend/vue_merge_request_widget/mock_data.js
+++ b/spec/frontend/vue_merge_request_widget/mock_data.js
@@ -1,5 +1,94 @@
import { SUCCESS } from '~/vue_merge_request_widget/components/deployment/constants';
+export const mockDownstreamPipelinesRest = ({ includeSourceJobRetried = true } = {}) => [
+ {
+ id: 632,
+ user: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'https://gdk.test:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ active: false,
+ coverage: null,
+ source: 'parent_pipeline',
+ source_job: {
+ name: 'bridge_job',
+ retried: includeSourceJobRetried ? false : null,
+ },
+ path: '/kitchen-sink/bakery/-/pipelines/632',
+ details: {
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/kitchen-sink/bakery/-/pipelines/632',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ },
+ project: {
+ id: 21,
+ name: 'bakery',
+ full_path: '/kitchen-sink/bakery',
+ full_name: 'kitchen-sink / bakery',
+ refs_url: '/kitchen-sink/bakery/refs',
+ },
+ },
+ {
+ id: 633,
+ user: {
+ id: 1,
+ username: 'root',
+ name: 'Administrator',
+ state: 'active',
+ avatar_url:
+ 'https://secure.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ web_url: 'https://gdk.test:3000/root',
+ show_status: false,
+ path: '/root',
+ },
+ active: false,
+ coverage: null,
+ source: 'parent_pipeline',
+ source_job: {
+ name: 'bridge_job',
+ retried: includeSourceJobRetried ? true : null,
+ },
+ path: '/kitchen-sink/bakery/-/pipelines/633',
+ details: {
+ status: {
+ icon: 'status_success',
+ text: 'passed',
+ label: 'passed',
+ group: 'success',
+ tooltip: 'passed',
+ has_details: true,
+ details_path: '/kitchen-sink/bakery/-/pipelines/633',
+ illustration: null,
+ favicon:
+ '/assets/ci_favicons/favicon_status_success-8451333011eee8ce9f2ab25dc487fe24a8758c694827a582f17f42b0a90446a2.png',
+ },
+ },
+ project: {
+ id: 21,
+ name: 'bakery',
+ full_path: '/kitchen-sink/bakery',
+ full_name: 'kitchen-sink / bakery',
+ refs_url: '/kitchen-sink/bakery/refs',
+ },
+ },
+];
+
export const artifacts = [
{
text: 'result.txt',
@@ -207,6 +296,7 @@ export default {
retry_path: '/root/acets-app/pipelines/172/retry',
created_at: '2017-04-07T12:27:19.520Z',
updated_at: '2017-04-07T15:28:44.800Z',
+ triggered: mockDownstreamPipelinesRest(),
},
pipelineCoverageDelta: '15.25',
buildsWithCoverage: [
diff --git a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
index 683858b331d..f37276ad594 100644
--- a/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
+++ b/spec/frontend/vue_merge_request_widget/mr_widget_options_spec.js
@@ -11,6 +11,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import { securityReportMergeRequestDownloadPathsQueryResponse } from 'jest/vue_shared/security_reports/mock_data';
import api from '~/api';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK, HTTP_STATUS_NO_CONTENT } from '~/lib/utils/http_status';
import Poll from '~/lib/utils/poll';
import { setFaviconOverlay } from '~/lib/utils/favicon';
import notify from '~/lib/utils/notify';
@@ -74,8 +75,10 @@ describe('MrWidgetOptions', () => {
gon.features = { asyncMrWidget: true };
mock = new MockAdapter(axios);
- mock.onGet(mockData.merge_request_widget_path).reply(() => [200, { ...mockData }]);
- mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, { ...mockData }]);
+ mock.onGet(mockData.merge_request_widget_path).reply(() => [HTTP_STATUS_OK, { ...mockData }]);
+ mock
+ .onGet(mockData.merge_request_cached_widget_path)
+ .reply(() => [HTTP_STATUS_OK, { ...mockData }]);
});
afterEach(() => {
@@ -805,8 +808,8 @@ describe('MrWidgetOptions', () => {
// Override top-level mocked requests, which always use a fresh copy of
// mockData, which always includes the full pipeline object.
- mock.onGet(mockData.merge_request_widget_path).reply(() => [200, mrData]);
- mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [200, mrData]);
+ mock.onGet(mockData.merge_request_widget_path).reply(() => [HTTP_STATUS_OK, mrData]);
+ mock.onGet(mockData.merge_request_cached_widget_path).reply(() => [HTTP_STATUS_OK, mrData]);
return createComponent(mrData, {
apolloMock: [
@@ -837,7 +840,7 @@ describe('MrWidgetOptions', () => {
describe('suggestPipeline', () => {
beforeEach(() => {
- mock.onAny().reply(200);
+ mock.onAny().reply(HTTP_STATUS_OK);
});
describe('given feature flag is enabled', () => {
@@ -986,12 +989,12 @@ describe('MrWidgetOptions', () => {
() =>
Promise.resolve({
headers: { 'poll-interval': 0 },
- status: 200,
+ status: HTTP_STATUS_OK,
data: { reports: 'parsed' },
}),
() =>
Promise.resolve({
- status: 200,
+ status: HTTP_STATUS_OK,
data: { reports: 'parsed' },
}),
]),
@@ -1009,11 +1012,11 @@ describe('MrWidgetOptions', () => {
() =>
Promise.resolve({
headers: { 'poll-interval': 1 },
- status: 204,
+ status: HTTP_STATUS_NO_CONTENT,
}),
() =>
Promise.resolve({
- status: 200,
+ status: HTTP_STATUS_OK,
data: { reports: 'parsed' },
}),
]),
diff --git a/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js
index 1a109aad911..be31c65b5e2 100644
--- a/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js
+++ b/spec/frontend/vue_merge_request_widget/stores/artifacts_list/actions_spec.js
@@ -3,6 +3,11 @@ import { TEST_HOST } from 'helpers/test_constants';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
import {
+ HTTP_STATUS_INTERNAL_SERVER_ERROR,
+ HTTP_STATUS_NO_CONTENT,
+ HTTP_STATUS_OK,
+} from '~/lib/utils/http_status';
+import {
setEndpoint,
requestArtifacts,
clearEtagPoll,
@@ -61,7 +66,7 @@ describe('Artifacts App Store Actions', () => {
describe('success', () => {
it('dispatches requestArtifacts and receiveArtifactsSuccess', () => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(200, [
+ mock.onGet(`${TEST_HOST}/endpoint.json`).replyOnce(HTTP_STATUS_OK, [
{
text: 'result.txt',
url: 'asda',
@@ -89,7 +94,7 @@ describe('Artifacts App Store Actions', () => {
job_path: 'asda',
},
],
- status: 200,
+ status: HTTP_STATUS_OK,
},
type: 'receiveArtifactsSuccess',
},
@@ -100,7 +105,7 @@ describe('Artifacts App Store Actions', () => {
describe('error', () => {
beforeEach(() => {
- mock.onGet(`${TEST_HOST}/endpoint.json`).reply(500);
+ mock.onGet(`${TEST_HOST}/endpoint.json`).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
});
it('dispatches requestArtifacts and receiveArtifactsError', () => {
@@ -126,7 +131,7 @@ describe('Artifacts App Store Actions', () => {
it('should commit RECEIVE_ARTIFACTS_SUCCESS mutation with 200', () => {
return testAction(
receiveArtifactsSuccess,
- { data: { summary: {} }, status: 200 },
+ { data: { summary: {} }, status: HTTP_STATUS_OK },
mockedState,
[{ type: types.RECEIVE_ARTIFACTS_SUCCESS, payload: { summary: {} } }],
[],
@@ -136,7 +141,7 @@ describe('Artifacts App Store Actions', () => {
it('should not commit RECEIVE_ARTIFACTS_SUCCESS mutation with 204', () => {
return testAction(
receiveArtifactsSuccess,
- { data: { summary: {} }, status: 204 },
+ { data: { summary: {} }, status: HTTP_STATUS_NO_CONTENT },
mockedState,
[],
[],
diff --git a/spec/frontend/vue_merge_request_widget/test_extensions.js b/spec/frontend/vue_merge_request_widget/test_extensions.js
index 1977f550577..e9e5d931323 100644
--- a/spec/frontend/vue_merge_request_widget/test_extensions.js
+++ b/spec/frontend/vue_merge_request_widget/test_extensions.js
@@ -1,3 +1,4 @@
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { EXTENSION_ICONS } from '~/vue_merge_request_widget/constants';
export const workingExtension = (shouldCollapse = true) => ({
@@ -120,7 +121,7 @@ export const pollingFullDataExtension = {
return Promise.resolve([
{
headers: { 'poll-interval': 0 },
- status: 200,
+ status: HTTP_STATUS_OK,
data: {
id: 1,
text: 'Hello world',
@@ -152,7 +153,7 @@ export const fullReportExtension = {
text: 'test',
href: `testref`,
target: '_blank',
- fullReport: true,
+ trackFullReportClicked: true,
},
];
},
diff --git a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
index 5a0ee5a59ba..98a357bac2b 100644
--- a/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
+++ b/spec/frontend/vue_shared/alert_details/sidebar/alert_sidebar_assignees_spec.js
@@ -3,6 +3,7 @@ import axios from 'axios';
import MockAdapter from 'axios-mock-adapter';
import { nextTick } from 'vue';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import SidebarAssignee from '~/vue_shared/alert_details/components/sidebar/sidebar_assignee.vue';
import SidebarAssignees from '~/vue_shared/alert_details/components/sidebar/sidebar_assignees.vue';
import AlertSetAssignees from '~/vue_shared/alert_details/graphql/mutations/alert_set_assignees.mutation.graphql';
@@ -97,7 +98,7 @@ describe('Alert Details Sidebar Assignees', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(mockPath).replyOnce(200, mockUsers);
+ mock.onGet(mockPath).replyOnce(HTTP_STATUS_OK, mockUsers);
mountComponent({
data: { alert: mockAlert },
sidebarCollapsed: false,
@@ -187,7 +188,7 @@ describe('Alert Details Sidebar Assignees', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet(mockPath).replyOnce(200, mockUsers);
+ mock.onGet(mockPath).replyOnce(HTTP_STATUS_OK, mockUsers);
mountComponent({
data: { alert: mockAlert },
diff --git a/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap b/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
index b7b43264330..ad08120fada 100644
--- a/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
+++ b/spec/frontend/vue_shared/components/__snapshots__/memory_graph_spec.js.snap
@@ -9,6 +9,7 @@ exports[`MemoryGraph Render chart should draw container with chart 1`] = `
data="Nov 12 2019 19:17:33,2.87,Nov 12 2019 19:18:33,2.78,Nov 12 2019 19:19:33,2.78,Nov 12 2019 19:20:33,3.01"
gradient=""
height="25"
+ smooth="0"
tooltiplabel="MB"
/>
</div>
diff --git a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
index e1860d3399b..3f7ec156c19 100644
--- a/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
+++ b/spec/frontend/vue_shared/components/confidentiality_badge_spec.js
@@ -1,13 +1,13 @@
import { GlBadge } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import { WorkspaceType, IssuableType } from '~/issues/constants';
+import { WorkspaceType, TYPE_ISSUE, TYPE_EPIC } from '~/issues/constants';
import ConfidentialityBadge from '~/vue_shared/components/confidentiality_badge.vue';
const createComponent = ({
workspaceType = WorkspaceType.project,
- issuableType = IssuableType.Issue,
+ issuableType = TYPE_ISSUE,
} = {}) =>
shallowMount(ConfidentialityBadge, {
propsData: {
@@ -28,9 +28,9 @@ describe('ConfidentialityBadge', () => {
});
it.each`
- workspaceType | issuableType | expectedTooltip
- ${WorkspaceType.project} | ${IssuableType.Issue} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'}
- ${WorkspaceType.group} | ${IssuableType.Epic} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'}
+ workspaceType | issuableType | expectedTooltip
+ ${WorkspaceType.project} | ${TYPE_ISSUE} | ${'Only project members with at least the Reporter role, the author, and assignees can view or be notified about this issue.'}
+ ${WorkspaceType.group} | ${TYPE_EPIC} | ${'Only group members with at least the Reporter role can view or be notified about this epic.'}
`(
'should render gl-badge with correct tooltip when workspaceType is $workspaceType and issuableType is $issuableType',
({ workspaceType, issuableType, expectedTooltip }) => {
diff --git a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
index 0d329b6a065..b0c0fc79676 100644
--- a/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
+++ b/spec/frontend/vue_shared/components/content_viewer/viewers/markdown_viewer_spec.js
@@ -3,6 +3,7 @@ import { mount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MarkdownViewer from '~/vue_shared/components/content_viewer/viewers/markdown_viewer.vue';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -35,9 +36,11 @@ describe('MarkdownViewer', () => {
describe('success', () => {
beforeEach(() => {
- mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).replyOnce(200, {
- body: '<b>testing</b> {{gl_md_img_1}}',
- });
+ mock
+ .onPost(`${gon.relative_url_root}/testproject/preview_markdown`)
+ .replyOnce(HTTP_STATUS_OK, {
+ body: '<b>testing</b> {{gl_md_img_1}}',
+ });
});
it('renders a skeleton loader while the markdown is loading', () => {
@@ -100,9 +103,11 @@ describe('MarkdownViewer', () => {
describe('error', () => {
beforeEach(() => {
- mock.onPost(`${gon.relative_url_root}/testproject/preview_markdown`).replyOnce(500, {
- body: 'Internal Server Error',
- });
+ mock
+ .onPost(`${gon.relative_url_root}/testproject/preview_markdown`)
+ .replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR, {
+ body: 'Internal Server Error',
+ });
});
it('renders an error message if loading the markdown preview fails', () => {
createComponent();
diff --git a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
index 2c5bb86d8a5..c1495e8264a 100644
--- a/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
+++ b/spec/frontend/vue_shared/components/date_time_picker/date_time_picker_input_spec.js
@@ -56,11 +56,11 @@ describe('DateTimePickerInput', () => {
it('input event is emitted when focus is lost', () => {
createComponent();
- jest.spyOn(wrapper.vm, '$emit');
+
const input = wrapper.find('input');
input.setValue(inputValue);
input.trigger('blur');
- expect(wrapper.vm.$emit).toHaveBeenCalledWith('input', inputValue);
+ expect(wrapper.emitted('input')[0][0]).toEqual(inputValue);
});
});
diff --git a/spec/frontend/vue_shared/components/dismissible_container_spec.js b/spec/frontend/vue_shared/components/dismissible_container_spec.js
index f7030f38709..7d8581e11e9 100644
--- a/spec/frontend/vue_shared/components/dismissible_container_spec.js
+++ b/spec/frontend/vue_shared/components/dismissible_container_spec.js
@@ -1,6 +1,7 @@
import { shallowMount } from '@vue/test-utils';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import dismissibleContainer from '~/vue_shared/components/dismissible_container.vue';
describe('DismissibleContainer', () => {
@@ -28,7 +29,7 @@ describe('DismissibleContainer', () => {
});
it('successfully dismisses', () => {
- mockAxios.onPost(propsData.path).replyOnce(200);
+ mockAxios.onPost(propsData.path).replyOnce(HTTP_STATUS_OK);
const button = findBtn();
button.trigger('click');
diff --git a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
index c34041f9305..119d6448507 100644
--- a/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
+++ b/spec/frontend/vue_shared/components/dropdown_keyboard_navigation_spec.js
@@ -61,27 +61,8 @@ describe('DropdownKeyboardNavigation', () => {
});
describe('keydown events', () => {
- let incrementSpy;
-
beforeEach(() => {
createComponent();
- incrementSpy = jest.spyOn(wrapper.vm, 'increment');
- });
-
- afterEach(() => {
- incrementSpy.mockRestore();
- });
-
- it('onKeydown-Down calls increment(1)', () => {
- helpers.arrowDown();
-
- expect(incrementSpy).toHaveBeenCalledWith(1);
- });
-
- it('onKeydown-Up calls increment(-1)', () => {
- helpers.arrowUp();
-
- expect(incrementSpy).toHaveBeenCalledWith(-1);
});
it('onKeydown-Tab $emits @tab event', () => {
diff --git a/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
new file mode 100644
index 00000000000..6b98f6c5e89
--- /dev/null
+++ b/spec/frontend/vue_shared/components/entity_select/entity_select_spec.js
@@ -0,0 +1,268 @@
+import { nextTick } from 'vue';
+import { GlCollapsibleListbox, GlFormGroup } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
+import { QUERY_TOO_SHORT_MESSAGE } from '~/vue_shared/components/entity_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('EntitySelect', () => {
+ let wrapper;
+ let fetchItemsMock;
+ let fetchInitialSelectionTextMock;
+
+ // Mocks
+ const itemMock = {
+ text: 'selectedGroup',
+ value: '1',
+ };
+
+ // Stubs
+ const GlAlert = {
+ template: '<div><slot /></div>',
+ };
+
+ // Props
+ const label = 'label';
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+ const headerText = 'headerText';
+ const defaultToggleText = 'defaultToggleText';
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findInput = () => wrapper.findByTestId('input');
+
+ // Helpers
+ const createComponent = ({ props = {}, slots = {}, stubs = {} } = {}) => {
+ wrapper = shallowMountExtended(EntitySelect, {
+ propsData: {
+ label,
+ inputName,
+ inputId,
+ headerText,
+ defaultToggleText,
+ fetchItems: fetchItemsMock,
+ ...props,
+ },
+ stubs: {
+ GlAlert,
+ EntitySelect,
+ ...stubs,
+ },
+ slots,
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+ const search = (searchString) => findListbox().vm.$emit('search', searchString);
+ const selectGroup = async () => {
+ openListbox();
+ await nextTick();
+ findListbox().vm.$emit('select', itemMock.value);
+ return nextTick();
+ };
+
+ beforeEach(() => {
+ fetchItemsMock = jest.fn().mockImplementation(() => ({ items: [itemMock], totalPages: 1 }));
+ });
+
+ describe('on mount', () => {
+ it('calls the fetch function when the listbox is opened', async () => {
+ createComponent();
+ openListbox();
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(1);
+ });
+
+ it("fetches the initially selected value's name", async () => {
+ fetchInitialSelectionTextMock = jest.fn().mockImplementation(() => itemMock.text);
+ createComponent({
+ props: {
+ fetchInitialSelectionText: fetchInitialSelectionTextMock,
+ initialSelection: itemMock.value,
+ },
+ });
+ await nextTick();
+
+ expect(fetchInitialSelectionTextMock).toHaveBeenCalledTimes(1);
+ expect(findListbox().props('toggleText')).toBe(itemMock.text);
+ });
+ });
+
+ it("renders the error slot's content", () => {
+ const selector = 'data-test-id="error-element"';
+ createComponent({
+ slots: {
+ error: `<div ${selector} />`,
+ },
+ });
+
+ expect(wrapper.find(`[${selector}]`).exists()).toBe(true);
+ });
+
+ it('renders the label slot if provided', () => {
+ const testid = 'label-slot';
+ createComponent({
+ slots: {
+ label: `<div data-testid="${testid}" />`,
+ },
+ stubs: {
+ GlFormGroup,
+ },
+ });
+
+ expect(wrapper.findByTestId(testid).exists()).toBe(true);
+ });
+
+ describe('selection', () => {
+ it('uses the default toggle text while no group is selected', () => {
+ createComponent();
+
+ expect(findListbox().props('toggleText')).toBe(defaultToggleText);
+ });
+
+ describe('once a group is selected', () => {
+ it(`uses the selected group's name as the toggle text`, async () => {
+ createComponent();
+ await selectGroup();
+
+ expect(findListbox().props('toggleText')).toBe(itemMock.text);
+ });
+
+ it(`uses the selected group's ID as the listbox' and input value`, async () => {
+ createComponent();
+ await selectGroup();
+
+ expect(findListbox().attributes('selected')).toBe(itemMock.value);
+ expect(findInput().attributes('value')).toBe(itemMock.value);
+ });
+
+ it(`on reset, falls back to the default toggle text`, async () => {
+ createComponent();
+ await selectGroup();
+
+ findListbox().vm.$emit('reset');
+ await nextTick();
+
+ expect(findListbox().props('toggleText')).toBe(defaultToggleText);
+ });
+ });
+ });
+
+ describe('search', () => {
+ it('sets `searching` to `true` when first opening the dropdown', async () => {
+ createComponent();
+
+ expect(findListbox().props('searching')).toBe(false);
+
+ openListbox();
+ await nextTick();
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('sets `searching` to `true` while searching', async () => {
+ createComponent();
+
+ expect(findListbox().props('searching')).toBe(false);
+
+ search('foo');
+ await nextTick();
+
+ expect(findListbox().props('searching')).toBe(true);
+ });
+
+ it('fetches groups matching the search string', async () => {
+ const searchString = 'searchString';
+ createComponent();
+ openListbox();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(1);
+
+ fetchItemsMock.mockImplementation(() => ({ items: [], totalPages: 1 }));
+ search(searchString);
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(2);
+ });
+
+ it('shows a notice if the search query is too short', async () => {
+ const searchString = 'a';
+ createComponent();
+ openListbox();
+ search(searchString);
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(1);
+ expect(findListbox().props('noResultsText')).toBe(QUERY_TOO_SHORT_MESSAGE);
+ });
+ });
+
+ describe('pagination', () => {
+ const searchString = 'searchString';
+
+ beforeEach(async () => {
+ let requestCount = 0;
+ fetchItemsMock.mockImplementation((searchQuery, page) => {
+ requestCount += 1;
+ return {
+ items: [
+ {
+ text: `Group [page: ${page} - search: ${searchQuery}]`,
+ value: `id:${requestCount}`,
+ },
+ ],
+ totalPages: 3,
+ };
+ });
+ createComponent();
+ openListbox();
+ findListbox().vm.$emit('bottom-reached');
+ return nextTick();
+ });
+
+ it('fetches the next page when bottom is reached', () => {
+ expect(fetchItemsMock).toHaveBeenCalledTimes(2);
+ expect(fetchItemsMock).toHaveBeenLastCalledWith('', 2);
+ });
+
+ it('fetches the first page when the search query changes', async () => {
+ search(searchString);
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(3);
+ expect(fetchItemsMock).toHaveBeenLastCalledWith(searchString, 1);
+ });
+
+ it('retains the search query when infinite scrolling', async () => {
+ search(searchString);
+ await nextTick();
+ findListbox().vm.$emit('bottom-reached');
+ await nextTick();
+
+ expect(fetchItemsMock).toHaveBeenCalledTimes(4);
+ expect(fetchItemsMock).toHaveBeenLastCalledWith(searchString, 2);
+ });
+
+ 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);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/entity_select/group_select_spec.js b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
new file mode 100644
index 00000000000..83560e367ea
--- /dev/null
+++ b/spec/frontend/vue_shared/components/entity_select/group_select_spec.js
@@ -0,0 +1,135 @@
+import { 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 { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
+import GroupSelect from '~/vue_shared/components/entity_select/group_select.vue';
+import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
+import {
+ GROUP_TOGGLE_TEXT,
+ GROUP_HEADER_TEXT,
+ FETCH_GROUPS_ERROR,
+ FETCH_GROUP_ERROR,
+} from '~/vue_shared/components/entity_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('GroupSelect', () => {
+ let wrapper;
+ let mock;
+
+ // Mocks
+ const groupMock = {
+ full_name: 'selectedGroup',
+ id: '1',
+ };
+ 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 findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findEntitySelect = () => wrapper.findComponent(EntitySelect);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ // Helpers
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = shallowMountExtended(GroupSelect, {
+ propsData: {
+ label,
+ inputName,
+ inputId,
+ ...props,
+ },
+ stubs: {
+ GlAlert,
+ EntitySelect,
+ },
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ describe('entity_select props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ prop | expectedValue
+ ${'label'} | ${label}
+ ${'inputName'} | ${inputName}
+ ${'inputId'} | ${inputId}
+ ${'defaultToggleText'} | ${GROUP_TOGGLE_TEXT}
+ ${'headerText'} | ${GROUP_HEADER_TEXT}
+ `('passes the $prop prop to entity-select', ({ prop, expectedValue }) => {
+ expect(findEntitySelect().props(prop)).toBe(expectedValue);
+ });
+ });
+
+ describe('on mount', () => {
+ it('fetches groups when the listbox is opened', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(0);
+
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ });
+
+ describe('with an initial selection', () => {
+ it("fetches the initially selected value's name", async () => {
+ mock.onGet(groupEndpoint).reply(HTTP_STATUS_OK, groupMock);
+ createComponent({ props: { initialSelection: groupMock.id } });
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
+ });
+
+ it('show an error if fetching the individual group fails', async () => {
+ mock
+ .onGet('/api/undefined/groups.json')
+ .reply(HTTP_STATUS_OK, [{ full_name: 'notTheSelectedGroup', id: '2' }]);
+ mock.onGet(groupEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent({ props: { initialSelection: groupMock.id } });
+
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_GROUP_ERROR);
+ });
+ });
+ });
+
+ it('shows an error when fetching groups fails', async () => {
+ mock.onGet('/api/undefined/groups.json').reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent();
+ openListbox();
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_GROUPS_ERROR);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/entity_select/project_select_spec.js b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
new file mode 100644
index 00000000000..57dce032d30
--- /dev/null
+++ b/spec/frontend/vue_shared/components/entity_select/project_select_spec.js
@@ -0,0 +1,248 @@
+import { GlCollapsibleListbox } from '@gitlab/ui';
+import MockAdapter from 'axios-mock-adapter';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK, HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
+import ProjectSelect from '~/vue_shared/components/entity_select/project_select.vue';
+import EntitySelect from '~/vue_shared/components/entity_select/entity_select.vue';
+import {
+ PROJECT_TOGGLE_TEXT,
+ PROJECT_HEADER_TEXT,
+ FETCH_PROJECTS_ERROR,
+ FETCH_PROJECT_ERROR,
+} from '~/vue_shared/components/entity_select/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+
+describe('ProjectSelect', () => {
+ let wrapper;
+ let mock;
+
+ // Stubs
+ const GlAlert = {
+ template: '<div><slot /></div>',
+ };
+
+ // Props
+ const label = 'label';
+ const inputName = 'inputName';
+ const inputId = 'inputId';
+ const groupId = '22';
+ const userId = '1';
+
+ // Mocks
+ const apiVersion = 'v4';
+ const projectMock = {
+ name_with_namespace: 'selectedProject',
+ id: '1',
+ };
+ const projectsEndpoint = `/api/${apiVersion}/projects.json`;
+ const groupProjectEndpoint = `/api/${apiVersion}/groups/${groupId}/projects.json`;
+ const userProjectEndpoint = `/api/${apiVersion}/users/${userId}/projects`;
+ const projectEndpoint = `/api/${apiVersion}/projects/${projectMock.id}`;
+
+ // Finders
+ const findListbox = () => wrapper.findComponent(GlCollapsibleListbox);
+ const findEntitySelect = () => wrapper.findComponent(EntitySelect);
+ const findAlert = () => wrapper.findComponent(GlAlert);
+
+ // Helpers
+ const createComponent = ({ props = {} } = {}) => {
+ wrapper = mountExtended(ProjectSelect, {
+ propsData: {
+ label,
+ inputName,
+ inputId,
+ groupId,
+ ...props,
+ },
+ stubs: {
+ GlAlert,
+ EntitySelect,
+ },
+ });
+ };
+ const openListbox = () => findListbox().vm.$emit('shown');
+
+ beforeAll(() => {
+ gon.api_version = apiVersion;
+ });
+
+ beforeEach(() => {
+ mock = new MockAdapter(axios);
+ });
+
+ afterEach(() => {
+ mock.restore();
+ });
+
+ it('renders HTML label when hasHtmlLabel is true', () => {
+ const testid = 'html-label';
+ createComponent({
+ props: {
+ label: `<div data-testid="${testid}" />`,
+ hasHtmlLabel: true,
+ },
+ });
+
+ expect(wrapper.findByTestId(testid).exists()).toBe(true);
+ });
+
+ describe('entity_select props', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it.each`
+ prop | expectedValue
+ ${'label'} | ${label}
+ ${'inputName'} | ${inputName}
+ ${'inputId'} | ${inputId}
+ ${'defaultToggleText'} | ${PROJECT_TOGGLE_TEXT}
+ ${'headerText'} | ${PROJECT_HEADER_TEXT}
+ ${'clearable'} | ${true}
+ `('passes the $prop prop to entity-select', ({ prop, expectedValue }) => {
+ expect(findEntitySelect().props(prop)).toBe(expectedValue);
+ });
+ });
+
+ describe('on mount', () => {
+ it('fetches projects when the listbox is opened', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(0);
+
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(mock.history.get[0].url).toBe(groupProjectEndpoint);
+ expect(mock.history.get[0].params).toEqual({
+ include_subgroups: false,
+ order_by: 'similarity',
+ per_page: 20,
+ search: '',
+ simple: true,
+ with_shared: true,
+ });
+ });
+
+ it('includes projects from subgroups if includeSubgroups is true', async () => {
+ createComponent({
+ props: {
+ includeSubgroups: true,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].params.include_subgroups).toBe(true);
+ });
+
+ it('fetches projects globally if no group ID is provided', async () => {
+ createComponent({
+ props: {
+ groupId: null,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].url).toBe(projectsEndpoint);
+ expect(mock.history.get[0].params).toEqual({
+ membership: false,
+ order_by: 'similarity',
+ per_page: 20,
+ search: '',
+ simple: true,
+ });
+ });
+
+ it('restricts search to owned projects if membership is true', async () => {
+ createComponent({
+ props: {
+ groupId: null,
+ membership: true,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].params.membership).toBe(true);
+ });
+
+ it("fetches the user's projects if a user ID is provided", async () => {
+ createComponent({
+ props: {
+ groupId: null,
+ userId,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].url).toBe(userProjectEndpoint);
+ expect(mock.history.get[0].params).toEqual({
+ per_page: 20,
+ search: '',
+ with_shared: true,
+ include_subgroups: false,
+ });
+ });
+
+ it.each([null, groupId])(
+ 'fetches with the provided sort key when groupId is %s',
+ async (groupIdProp) => {
+ const orderBy = 'last_activity_at';
+ createComponent({
+ props: {
+ groupId: groupIdProp,
+ orderBy,
+ },
+ });
+ openListbox();
+ await waitForPromises();
+
+ expect(mock.history.get[0].params.order_by).toBe(orderBy);
+ },
+ );
+
+ describe('with an initial selection', () => {
+ it("fetches the initially selected value's name", async () => {
+ mock.onGet(projectEndpoint).reply(HTTP_STATUS_OK, projectMock);
+ createComponent({ props: { initialSelection: projectMock.id } });
+ await waitForPromises();
+
+ expect(mock.history.get).toHaveLength(1);
+ expect(findListbox().props('toggleText')).toBe(projectMock.name_with_namespace);
+ });
+
+ it('show an error if fetching the individual project fails', async () => {
+ mock
+ .onGet(groupProjectEndpoint)
+ .reply(HTTP_STATUS_OK, [{ full_name: 'notTheSelectedProject', id: '2' }]);
+ mock.onGet(projectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent({ props: { initialSelection: projectMock.id } });
+
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_PROJECT_ERROR);
+ });
+ });
+ });
+
+ it('shows an error when fetching projects fails', async () => {
+ mock.onGet(groupProjectEndpoint).reply(HTTP_STATUS_INTERNAL_SERVER_ERROR);
+ createComponent();
+ openListbox();
+ expect(findAlert().exists()).toBe(false);
+
+ await waitForPromises();
+
+ expect(findAlert().exists()).toBe(true);
+ expect(findAlert().text()).toBe(FETCH_PROJECTS_ERROR);
+ });
+});
diff --git a/spec/frontend/vue_shared/components/group_select/utils_spec.js b/spec/frontend/vue_shared/components/entity_select/utils_spec.js
index 5188e1aabf1..9aa1baf204e 100644
--- a/spec/frontend/vue_shared/components/group_select/utils_spec.js
+++ b/spec/frontend/vue_shared/components/entity_select/utils_spec.js
@@ -1,6 +1,6 @@
-import { groupsPath } from '~/vue_shared/components/group_select/utils';
+import { groupsPath } from '~/vue_shared/components/entity_select/utils';
-describe('group_select utils', () => {
+describe('entity_select utils', () => {
describe('groupsPath', () => {
it.each`
groupsFilter | parentGroupID | expectedPath
diff --git a/spec/frontend/vue_shared/components/file_icon_spec.js b/spec/frontend/vue_shared/components/file_icon_spec.js
index 3f4bfc86b67..0fcc0678c13 100644
--- a/spec/frontend/vue_shared/components/file_icon_spec.js
+++ b/spec/frontend/vue_shared/components/file_icon_spec.js
@@ -8,10 +8,7 @@ describe('File Icon component', () => {
const findSvgIcon = () => wrapper.find('svg');
const findGlIcon = () => wrapper.findComponent(GlIcon);
const getIconName = () =>
- findSvgIcon()
- .find('use')
- .element.getAttribute('xlink:href')
- .replace(`${gon.sprite_file_icons}#`, '');
+ findSvgIcon().find('use').element.getAttribute('href').replace(`${gon.sprite_file_icons}#`, '');
const createComponent = (props = {}) => {
wrapper = shallowMount(FileIcon, {
diff --git a/spec/frontend/vue_shared/components/file_row_spec.js b/spec/frontend/vue_shared/components/file_row_spec.js
index c3a71d7fda3..b70d4565f56 100644
--- a/spec/frontend/vue_shared/components/file_row_spec.js
+++ b/spec/frontend/vue_shared/components/file_row_spec.js
@@ -100,15 +100,6 @@ describe('File row component', () => {
});
});
- it('indents row based on level', () => {
- createComponent({
- file: file('t4'),
- level: 2,
- });
-
- expect(wrapper.find('.file-row-name').element.style.marginLeft).toBe('16px');
- });
-
it('renders header for file', () => {
createComponent({
file: {
diff --git a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js
index 66c6267027b..305f56255a5 100644
--- a/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js
+++ b/spec/frontend/vue_shared/components/filtered_search_bar/store/modules/filters/mutations_spec.js
@@ -1,6 +1,7 @@
import { get } from 'lodash';
import { mockBranches } from 'jest/vue_shared/components/filtered_search_bar/mock_data';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR } from '~/lib/utils/http_status';
import * as types from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutation_types';
import mutations from '~/vue_shared/components/filtered_search_bar/store/modules/filters/mutations';
import initialState from '~/vue_shared/components/filtered_search_bar/store/modules/filters/state';
@@ -16,7 +17,6 @@ const labels = filterLabels.map(convertObjectPropsToCamelCase);
const filterValue = { value: 'foo' };
describe('Filters mutations', () => {
- const errorCode = 500;
beforeEach(() => {
state = initialState();
});
@@ -79,35 +79,35 @@ describe('Filters mutations', () => {
${types.RECEIVE_BRANCHES_SUCCESS} | ${'branches'} | ${'errorCode'} | ${null}
${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'isLoading'} | ${false}
${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'data'} | ${[]}
- ${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_BRANCHES_ERROR} | ${'branches'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
${types.REQUEST_MILESTONES} | ${'milestones'} | ${'isLoading'} | ${true}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'isLoading'} | ${false}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'data'} | ${milestones}
${types.RECEIVE_MILESTONES_SUCCESS} | ${'milestones'} | ${'errorCode'} | ${null}
${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'isLoading'} | ${false}
${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'data'} | ${[]}
- ${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_MILESTONES_ERROR} | ${'milestones'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
${types.REQUEST_AUTHORS} | ${'authors'} | ${'isLoading'} | ${true}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'isLoading'} | ${false}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'data'} | ${users}
${types.RECEIVE_AUTHORS_SUCCESS} | ${'authors'} | ${'errorCode'} | ${null}
${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'isLoading'} | ${false}
${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'data'} | ${[]}
- ${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_AUTHORS_ERROR} | ${'authors'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
${types.REQUEST_LABELS} | ${'labels'} | ${'isLoading'} | ${true}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'isLoading'} | ${false}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'data'} | ${labels}
${types.RECEIVE_LABELS_SUCCESS} | ${'labels'} | ${'errorCode'} | ${null}
${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'isLoading'} | ${false}
${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'data'} | ${[]}
- ${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_LABELS_ERROR} | ${'labels'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
${types.REQUEST_ASSIGNEES} | ${'assignees'} | ${'isLoading'} | ${true}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'isLoading'} | ${false}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'data'} | ${users}
${types.RECEIVE_ASSIGNEES_SUCCESS} | ${'assignees'} | ${'errorCode'} | ${null}
${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'isLoading'} | ${false}
${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'data'} | ${[]}
- ${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'errorCode'} | ${errorCode}
+ ${types.RECEIVE_ASSIGNEES_ERROR} | ${'assignees'} | ${'errorCode'} | ${HTTP_STATUS_INTERNAL_SERVER_ERROR}
`('$mutation will set $stateKey with a given value', ({ mutation, rootKey, stateKey, value }) => {
mutations[mutation](state, value);
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
deleted file mode 100644
index 87dd7795b98..00000000000
--- a/spec/frontend/vue_shared/components/group_select/group_select_spec.js
+++ /dev/null
@@ -1,322 +0,0 @@
-import { nextTick } from 'vue';
-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 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';
-
-describe('GroupSelect', () => {
- let wrapper;
- let mock;
-
- // Mocks
- const groupMock = {
- full_name: 'selectedGroup',
- id: '1',
- };
- 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');
- const search = (searchString) => findListbox().vm.$emit('search', searchString);
- const createComponentWithGroups = () => {
- mock.onGet('/api/undefined/groups.json').reply(200, [groupMock]);
- createComponent();
- openListbox();
- return waitForPromises();
- };
- const selectGroup = () => {
- findListbox().vm.$emit('select', groupMock.id);
- return nextTick();
- };
-
- beforeEach(() => {
- mock = new MockAdapter(axios);
- });
-
- afterEach(() => {
- 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();
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(0);
-
- openListbox();
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(1);
- });
-
- describe('with an initial selection', () => {
- it('if the selected group is not part of the fetched list, fetches it individually', async () => {
- mock.onGet(groupEndpoint).reply(200, groupMock);
- createComponent({ props: { initialSelection: groupMock.id } });
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(1);
- expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
- });
-
- it('show an error if fetching the individual group fails', async () => {
- mock
- .onGet('/api/undefined/groups.json')
- .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(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(FETCH_GROUP_ERROR);
- });
- });
- });
-
- it('shows an error when fetching groups fails', async () => {
- mock.onGet('/api/undefined/groups.json').reply(500);
- createComponent();
- openListbox();
- expect(findAlert().exists()).toBe(false);
-
- await waitForPromises();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(FETCH_GROUPS_ERROR);
- });
-
- describe('selection', () => {
- it('uses the default toggle text while no group is selected', async () => {
- await createComponentWithGroups();
-
- expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT);
- });
-
- describe('once a group is selected', () => {
- it(`uses the selected group's name as the toggle text`, async () => {
- await createComponentWithGroups();
- await selectGroup();
-
- expect(findListbox().props('toggleText')).toBe(groupMock.full_name);
- });
-
- it(`uses the selected group's ID as the listbox' and input value`, async () => {
- await createComponentWithGroups();
- await selectGroup();
-
- expect(findListbox().attributes('selected')).toBe(groupMock.id);
- expect(findInput().attributes('value')).toBe(groupMock.id);
- });
-
- it(`on reset, falls back to the default toggle text`, async () => {
- await createComponentWithGroups();
- await selectGroup();
-
- findListbox().vm.$emit('reset');
- await nextTick();
-
- expect(findListbox().props('toggleText')).toBe(TOGGLE_TEXT);
- });
- });
- });
-
- describe('search', () => {
- it('sets `searching` to `true` when first opening the dropdown', async () => {
- createComponent();
-
- expect(findListbox().props('searching')).toBe(false);
-
- openListbox();
- await nextTick();
-
- expect(findListbox().props('searching')).toBe(true);
- });
-
- it('sets `searching` to `true` while searching', async () => {
- await createComponentWithGroups();
-
- expect(findListbox().props('searching')).toBe(false);
-
- search('foo');
- await nextTick();
-
- expect(findListbox().props('searching')).toBe(true);
- });
-
- it('fetches groups matching the search string', async () => {
- const searchString = 'searchString';
- await createComponentWithGroups();
-
- expect(mock.history.get).toHaveLength(1);
-
- search(searchString);
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(2);
- 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 () => {
- const searchString = 'a';
- await createComponentWithGroups();
- search(searchString);
- await waitForPromises();
-
- expect(mock.history.get).toHaveLength(1);
- 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 94e1ece8c6b..458f2cc5374 100644
--- a/spec/frontend/vue_shared/components/header_ci_component_spec.js
+++ b/spec/frontend/vue_shared/components/header_ci_component_spec.js
@@ -1,7 +1,7 @@
import { GlButton, GlAvatarLink, GlTooltip } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import CiIconBadge from '~/vue_shared/components/ci_badge_link.vue';
+import CiBadgeLink from '~/vue_shared/components/ci_badge_link.vue';
import HeaderCi from '~/vue_shared/components/header_ci_component.vue';
import TimeagoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
@@ -28,7 +28,7 @@ describe('Header CI Component', () => {
hasSidebarButton: true,
};
- const findIconBadge = () => wrapper.findComponent(CiIconBadge);
+ const findCiBadgeLink = () => wrapper.findComponent(CiBadgeLink);
const findTimeAgo = () => wrapper.findComponent(TimeagoTooltip);
const findUserLink = () => wrapper.findComponent(GlAvatarLink);
const findSidebarToggleBtn = () => wrapper.findComponent(GlButton);
@@ -59,7 +59,7 @@ describe('Header CI Component', () => {
});
it('should render status badge', () => {
- expect(findIconBadge().exists()).toBe(true);
+ expect(findCiBadgeLink().exists()).toBe(true);
});
it('should render timeago date', () => {
diff --git a/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js b/spec/frontend/vue_shared/components/incubation/incubation_alert_spec.js
index 7dca360c7ee..1783538beb3 100644
--- a/spec/frontend/ml/experiment_tracking/components/incubation_alert_spec.js
+++ b/spec/frontend/vue_shared/components/incubation/incubation_alert_spec.js
@@ -1,6 +1,6 @@
import { mount } from '@vue/test-utils';
import { GlAlert, GlButton } from '@gitlab/ui';
-import IncubationAlert from '~/ml/experiment_tracking/components/incubation_alert.vue';
+import IncubationAlert from '~/vue_shared/components/incubation/incubation_alert.vue';
describe('IncubationAlert', () => {
let wrapper;
@@ -10,13 +10,20 @@ describe('IncubationAlert', () => {
const findButton = () => wrapper.findComponent(GlButton);
beforeEach(() => {
- wrapper = mount(IncubationAlert);
+ wrapper = mount(IncubationAlert, {
+ propsData: {
+ featureName: 'some feature',
+ linkToFeedbackIssue: 'some_link',
+ },
+ });
+ });
+
+ it('displays the feature name in the title', () => {
+ expect(wrapper.html()).toContain('some feature is in incubating phase');
});
it('displays link to issue', () => {
- expect(findButton().attributes().href).toBe(
- 'https://gitlab.com/gitlab-org/gitlab/-/issues/381660',
- );
+ expect(findButton().attributes().href).toBe('some_link');
});
it('is removed if dismissed', async () => {
diff --git a/spec/frontend/vue_shared/components/incubation/pagination_spec.js b/spec/frontend/vue_shared/components/incubation/pagination_spec.js
new file mode 100644
index 00000000000..a621e60c627
--- /dev/null
+++ b/spec/frontend/vue_shared/components/incubation/pagination_spec.js
@@ -0,0 +1,76 @@
+import { GlKeysetPagination } from '@gitlab/ui';
+import { mountExtended } from 'helpers/vue_test_utils_helper';
+import Pagination from '~/vue_shared/components/incubation/pagination.vue';
+
+describe('~/vue_shared/incubation/components/pagination.vue', () => {
+ let wrapper;
+
+ const pageInfo = {
+ startCursor: 'eyJpZCI6IjE2In0',
+ endCursor: 'eyJpZCI6IjIifQ',
+ hasNextPage: true,
+ hasPreviousPage: true,
+ };
+
+ const findPagination = () => wrapper.findComponent(GlKeysetPagination);
+
+ const createWrapper = (pageInfoProp) => {
+ wrapper = mountExtended(Pagination, {
+ propsData: pageInfoProp,
+ });
+ };
+
+ describe('when neither next nor previous page exists', () => {
+ beforeEach(() => {
+ const emptyPageInfo = { ...pageInfo, hasPreviousPage: false, hasNextPage: false };
+
+ createWrapper(emptyPageInfo);
+ });
+
+ it('should not render pagination component', () => {
+ expect(wrapper.html()).toBe('');
+ });
+ });
+
+ describe('when Pagination is rendered for environment details page', () => {
+ beforeEach(() => {
+ createWrapper(pageInfo);
+ });
+
+ it('should pass correct props to keyset pagination', () => {
+ expect(findPagination().exists()).toBe(true);
+ expect(findPagination().props()).toEqual(expect.objectContaining(pageInfo));
+ });
+
+ describe.each([
+ {
+ testPageInfo: pageInfo,
+ expectedAfter: `cursor=${pageInfo.endCursor}`,
+ expectedBefore: `cursor=${pageInfo.startCursor}`,
+ },
+ {
+ testPageInfo: { ...pageInfo, hasNextPage: true, hasPreviousPage: false },
+ expectedAfter: `cursor=${pageInfo.endCursor}`,
+ expectedBefore: '',
+ },
+ {
+ testPageInfo: { ...pageInfo, hasNextPage: false, hasPreviousPage: true },
+ expectedAfter: '',
+ expectedBefore: `cursor=${pageInfo.startCursor}`,
+ },
+ ])(
+ 'button links generation for $testPageInfo',
+ ({ testPageInfo, expectedAfter, expectedBefore }) => {
+ beforeEach(() => {
+ createWrapper(testPageInfo);
+ });
+
+ it(`should have button links defined as ${expectedAfter || 'empty'} and
+ ${expectedBefore || 'empty'}`, () => {
+ expect(findPagination().props().prevButtonLink).toContain(expectedBefore);
+ expect(findPagination().props().nextButtonLink).toContain(expectedAfter);
+ });
+ },
+ );
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/field_spec.js b/spec/frontend/vue_shared/components/markdown/field_spec.js
index 3b8e78bbadd..68ce07f86b9 100644
--- a/spec/frontend/vue_shared/components/markdown/field_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/field_spec.js
@@ -1,11 +1,13 @@
+import $ from 'jquery';
import { nextTick } from 'vue';
import AxiosMockAdapter from 'axios-mock-adapter';
import { TEST_HOST, FIXTURES_PATH } from 'spec/test_constants';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import MarkdownField from '~/vue_shared/components/markdown/field.vue';
import MarkdownFieldHeader from '~/vue_shared/components/markdown/header.vue';
import MarkdownToolbar from '~/vue_shared/components/markdown/toolbar.vue';
-import { mountExtended } from 'helpers/vue_test_utils_helper';
+import { mountExtended, shallowMountExtended } from 'helpers/vue_test_utils_helper';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -74,6 +76,22 @@ describe('Markdown field component', () => {
);
}
+ function createWrapper({ autocompleteDataSources = {} } = {}) {
+ subject = shallowMountExtended(MarkdownField, {
+ propsData: {
+ markdownDocsPath,
+ markdownPreviewPath,
+ isSubmitting: false,
+ textareaValue,
+ lines: [],
+ enablePreview: true,
+ restrictedToolBarItems,
+ showContentEditorSwitcher: false,
+ autocompleteDataSources,
+ },
+ });
+ }
+
const getPreviewLink = () => subject.findByTestId('preview-tab');
const getWriteLink = () => subject.findByTestId('write-tab');
const getMarkdownButton = () => subject.find('.js-md');
@@ -84,6 +102,7 @@ describe('Markdown field component', () => {
const findDropzone = () => subject.find('.div-dropzone');
const findMarkdownHeader = () => subject.findComponent(MarkdownFieldHeader);
const findMarkdownToolbar = () => subject.findComponent(MarkdownToolbar);
+ const findGlForm = () => $(subject.vm.$refs['gl-form']).data('glForm');
describe('mounted', () => {
const previewHTML = `
@@ -100,6 +119,18 @@ describe('Markdown field component', () => {
findDropzone().element.addEventListener('click', dropzoneSpy);
});
+ describe('GlForm', () => {
+ beforeEach(() => {
+ createWrapper({ autocompleteDataSources: { commands: '/foobar/-/autocomplete_sources' } });
+ });
+
+ it('initializes GlForm with autocomplete data sources', () => {
+ expect(findGlForm().autoComplete.dataSources).toMatchObject({
+ commands: '/foobar/-/autocomplete_sources',
+ });
+ });
+ });
+
it('renders textarea inside backdrop', () => {
expect(subject.find('.zen-backdrop textarea').element).not.toBeNull();
});
@@ -107,7 +138,7 @@ describe('Markdown field component', () => {
it('renders referenced commands on markdown preview', async () => {
axiosMock
.onPost(markdownPreviewPath)
- .reply(200, { references: { users: [], commands: 'test command' } });
+ .reply(HTTP_STATUS_OK, { references: { users: [], commands: 'test command' } });
previewLink = getPreviewLink();
previewLink.vm.$emit('click', { target: {} });
@@ -121,7 +152,7 @@ describe('Markdown field component', () => {
describe('markdown preview', () => {
beforeEach(() => {
- axiosMock.onPost(markdownPreviewPath).reply(200, { body: previewHTML });
+ axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { body: previewHTML });
});
it('sets preview link as active', async () => {
@@ -267,7 +298,7 @@ describe('Markdown field component', () => {
const users = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11].map((i) => `user_${i}`);
it('shows warning on mention of all users', async () => {
- axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
+ axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { references: { users } });
subject.setProps({ textareaValue: 'hello @all' });
@@ -279,7 +310,7 @@ describe('Markdown field component', () => {
});
it('removes warning when all mention is removed', async () => {
- axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
+ axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { references: { users } });
subject.setProps({ textareaValue: 'hello @all' });
@@ -298,7 +329,7 @@ describe('Markdown field component', () => {
});
it('removes warning when all mention is removed while endpoint is loading', async () => {
- axiosMock.onPost(markdownPreviewPath).reply(200, { references: { users } });
+ axiosMock.onPost(markdownPreviewPath).reply(HTTP_STATUS_OK, { references: { users } });
jest.spyOn(axios, 'post');
subject.setProps({ textareaValue: 'hello @all' });
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 e3df2cde1c1..26b536984ff 100644
--- a/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
+++ b/spec/frontend/vue_shared/components/markdown/markdown_editor_spec.js
@@ -24,6 +24,7 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
const formFieldName = 'form[markdown_field]';
const formFieldPlaceholder = 'Write some markdown';
const formFieldAriaLabel = 'Edit your content';
+ const autocompleteDataSources = { commands: '/foobar/-/autcomplete_sources' };
let mock;
const buildWrapper = ({ propsData = {}, attachTo, stubs = {} } = {}) => {
@@ -35,11 +36,14 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
markdownDocsPath,
quickActionsDocsPath,
enableAutocomplete,
+ autocompleteDataSources,
enablePreview,
- formFieldId,
- formFieldName,
- formFieldPlaceholder,
- formFieldAriaLabel,
+ formFieldProps: {
+ id: formFieldId,
+ name: formFieldName,
+ placeholder: formFieldPlaceholder,
+ 'aria-label': formFieldAriaLabel,
+ },
...propsData,
},
stubs: {
@@ -66,18 +70,17 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
it('displays markdown field by default', () => {
buildWrapper({ propsData: { supportsQuickActions: true } });
- expect(findMarkdownField().props()).toEqual(
- expect.objectContaining({
- markdownPreviewPath: renderMarkdownPath,
- quickActionsDocsPath,
- canAttachFile: true,
- enableAutocomplete,
- textareaValue: value,
- markdownDocsPath,
- uploadsPath: window.uploads_path,
- enablePreview,
- }),
- );
+ expect(findMarkdownField().props()).toMatchObject({
+ autocompleteDataSources,
+ markdownPreviewPath: renderMarkdownPath,
+ quickActionsDocsPath,
+ canAttachFile: true,
+ enableAutocomplete,
+ textareaValue: value,
+ markdownDocsPath,
+ uploadsPath: window.uploads_path,
+ enablePreview,
+ });
});
it('renders markdown field textarea', () => {
@@ -95,6 +98,12 @@ describe('vue_shared/component/markdown/markdown_editor', () => {
expect(findTextarea().element.value).toBe(value);
});
+ it('fails to render if textarea id and name is not passed', () => {
+ expect(() => {
+ buildWrapper({ propsData: { formFieldProps: {} } });
+ }).toThrow('Invalid prop: custom validator check failed for prop "formFieldProps"');
+ });
+
it(`emits ${EDITING_MODE_CONTENT_EDITOR} event when enableContentEditor emitted from markdown editor`, async () => {
buildWrapper();
diff --git a/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js b/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
index adcf57b76a4..c1e61f6e43d 100644
--- a/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
+++ b/spec/frontend/vue_shared/components/markdown_drawer/utils/fetch_spec.js
@@ -4,6 +4,7 @@ import {
splitDocument,
} from '~/vue_shared/components/markdown_drawer/utils/fetch';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import {
MOCK_HTML,
MOCK_DRAWER_DATA,
@@ -20,9 +21,9 @@ describe('utils/fetch', () => {
});
describe.each`
- axiosMock | type | toExpect
- ${{ code: 200, res: MOCK_HTML }} | ${'success'} | ${MOCK_DRAWER_DATA}
- ${{ code: 500, res: null }} | ${'error'} | ${MOCK_DRAWER_DATA_ERROR}
+ axiosMock | type | toExpect
+ ${{ code: HTTP_STATUS_OK, res: MOCK_HTML }} | ${'success'} | ${MOCK_DRAWER_DATA}
+ ${{ code: HTTP_STATUS_INTERNAL_SERVER_ERROR, res: null }} | ${'error'} | ${MOCK_DRAWER_DATA_ERROR}
`('process markdown data', ({ axiosMock, type, toExpect }) => {
describe(`if api fetch responds with ${type}`, () => {
beforeEach(() => {
diff --git a/spec/frontend/vue_shared/components/new_resource_dropdown/mock_data.js b/spec/frontend/vue_shared/components/new_resource_dropdown/mock_data.js
new file mode 100644
index 00000000000..19b1453e8ac
--- /dev/null
+++ b/spec/frontend/vue_shared/components/new_resource_dropdown/mock_data.js
@@ -0,0 +1,54 @@
+export const emptySearchProjectsQueryResponse = {
+ data: {
+ projects: {
+ nodes: [],
+ },
+ },
+};
+
+export const emptySearchProjectsWithinGroupQueryResponse = {
+ data: {
+ group: {
+ id: '1',
+ projects: emptySearchProjectsQueryResponse.data.projects,
+ },
+ },
+};
+
+export const project1 = {
+ id: 'gid://gitlab/Group/26',
+ name: 'Super Mario Project',
+ nameWithNamespace: 'Mushroom Kingdom / Super Mario Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/super-mario-project',
+};
+
+export const project2 = {
+ id: 'gid://gitlab/Group/59',
+ name: 'Mario Kart Project',
+ nameWithNamespace: 'Mushroom Kingdom / Mario Kart Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-kart-project',
+};
+
+export const project3 = {
+ id: 'gid://gitlab/Group/103',
+ name: 'Mario Party Project',
+ nameWithNamespace: 'Mushroom Kingdom / Mario Party Project',
+ webUrl: 'https://127.0.0.1:3000/mushroom-kingdom/mario-party-project',
+};
+
+export const searchProjectsQueryResponse = {
+ data: {
+ projects: {
+ nodes: [project1, project2, project3],
+ },
+ },
+};
+
+export const searchProjectsWithinGroupQueryResponse = {
+ data: {
+ group: {
+ id: '1',
+ projects: searchProjectsQueryResponse.data.projects,
+ },
+ },
+};
diff --git a/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
new file mode 100644
index 00000000000..31320b1d2a6
--- /dev/null
+++ b/spec/frontend/vue_shared/components/new_resource_dropdown/new_resource_dropdown_spec.js
@@ -0,0 +1,262 @@
+import { GlDropdown, GlDropdownItem, GlSearchBoxByType } from '@gitlab/ui';
+import { mount, shallowMount } from '@vue/test-utils';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import NewResourceDropdown from '~/vue_shared/components/new_resource_dropdown/new_resource_dropdown.vue';
+import searchUserProjectsWithIssuesEnabledQuery from '~/vue_shared/components/new_resource_dropdown/graphql/search_user_projects_with_issues_enabled.query.graphql';
+import { RESOURCE_TYPES } from '~/vue_shared/components/new_resource_dropdown/constants';
+import searchProjectsWithinGroupQuery from '~/issues/list/queries/search_projects.query.graphql';
+import { DASH_SCOPE, joinPaths } from '~/lib/utils/url_utility';
+import { DEBOUNCE_DELAY } from '~/vue_shared/components/filtered_search_bar/constants';
+import { useLocalStorageSpy } from 'helpers/local_storage_helper';
+import {
+ emptySearchProjectsQueryResponse,
+ emptySearchProjectsWithinGroupQueryResponse,
+ project1,
+ project2,
+ project3,
+ searchProjectsQueryResponse,
+ searchProjectsWithinGroupQueryResponse,
+} from './mock_data';
+
+jest.mock('~/flash');
+
+describe('NewResourceDropdown component', () => {
+ useLocalStorageSpy();
+
+ let wrapper;
+
+ Vue.use(VueApollo);
+
+ // Props
+ const withinGroupProps = {
+ query: searchProjectsWithinGroupQuery,
+ queryVariables: { fullPath: 'mushroom-kingdom' },
+ extractProjects: (data) => data.group.projects.nodes,
+ };
+
+ const mountComponent = ({
+ search = '',
+ query = searchUserProjectsWithIssuesEnabledQuery,
+ queryResponse = searchProjectsQueryResponse,
+ mountFn = shallowMount,
+ propsData = {},
+ } = {}) => {
+ const requestHandlers = [[query, jest.fn().mockResolvedValue(queryResponse)]];
+ const apolloProvider = createMockApollo(requestHandlers);
+
+ return mountFn(NewResourceDropdown, {
+ apolloProvider,
+ propsData,
+ data() {
+ return { search };
+ },
+ });
+ };
+
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findInput = () => wrapper.findComponent(GlSearchBoxByType);
+ const showDropdown = async () => {
+ findDropdown().vm.$emit('shown');
+ await waitForPromises();
+ jest.advanceTimersByTime(DEBOUNCE_DELAY);
+ await waitForPromises();
+ };
+
+ afterEach(() => {
+ localStorage.clear();
+ });
+
+ it('renders a split dropdown', () => {
+ wrapper = mountComponent();
+
+ expect(findDropdown().props('split')).toBe(true);
+ });
+
+ it('renders a label for the dropdown toggle button', () => {
+ wrapper = mountComponent();
+
+ expect(findDropdown().attributes('toggle-text')).toBe(
+ NewResourceDropdown.i18n.toggleButtonLabel,
+ );
+ });
+
+ it('focuses on input when dropdown is shown', async () => {
+ wrapper = mountComponent({ mountFn: mount });
+
+ const inputSpy = jest.spyOn(findInput().vm, 'focusInput');
+
+ await showDropdown();
+
+ expect(inputSpy).toHaveBeenCalledTimes(1);
+ });
+
+ describe.each`
+ description | propsData | query | queryResponse | emptyResponse
+ ${'by default'} | ${undefined} | ${searchUserProjectsWithIssuesEnabledQuery} | ${searchProjectsQueryResponse} | ${emptySearchProjectsQueryResponse}
+ ${'within a group'} | ${withinGroupProps} | ${searchProjectsWithinGroupQuery} | ${searchProjectsWithinGroupQueryResponse} | ${emptySearchProjectsWithinGroupQueryResponse}
+ `('$description', ({ propsData, query, queryResponse, emptyResponse }) => {
+ it('renders projects options', async () => {
+ wrapper = mountComponent({ mountFn: mount, query, queryResponse, propsData });
+ await showDropdown();
+
+ const listItems = wrapper.findAll('li');
+
+ expect(listItems.at(0).text()).toBe(project1.nameWithNamespace);
+ expect(listItems.at(1).text()).toBe(project2.nameWithNamespace);
+ expect(listItems.at(2).text()).toBe(project3.nameWithNamespace);
+ });
+
+ it('renders `No matches found` when there are no matches', async () => {
+ wrapper = mountComponent({
+ search: 'no matches',
+ query,
+ queryResponse: emptyResponse,
+ mountFn: mount,
+ propsData,
+ });
+
+ await showDropdown();
+
+ expect(wrapper.find('li').text()).toBe(NewResourceDropdown.i18n.noMatchesFound);
+ });
+
+ describe.each`
+ resourceType | expectedDefaultLabel | expectedPath | expectedLabel
+ ${'issue'} | ${'Select project to create issue'} | ${'issues/new'} | ${'New issue in'}
+ ${'merge-request'} | ${'Select project to create merge request'} | ${'merge_requests/new'} | ${'New merge request in'}
+ ${'milestone'} | ${'Select project to create milestone'} | ${'milestones/new'} | ${'New milestone in'}
+ `(
+ 'with resource type $resourceType',
+ ({ resourceType, expectedDefaultLabel, expectedPath, expectedLabel }) => {
+ describe('when no project is selected', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({
+ query,
+ queryResponse,
+ propsData: { ...propsData, resourceType },
+ });
+ });
+
+ it('dropdown button is not a link', () => {
+ expect(findDropdown().attributes('split-href')).toBeUndefined();
+ });
+
+ it('displays default text on the dropdown button', () => {
+ expect(findDropdown().props('text')).toBe(expectedDefaultLabel);
+ });
+ });
+
+ describe('when a project is selected', () => {
+ beforeEach(async () => {
+ wrapper = mountComponent({
+ mountFn: mount,
+ query,
+ queryResponse,
+ propsData: { ...propsData, resourceType },
+ });
+ await showDropdown();
+
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ });
+
+ it('dropdown button is a link', () => {
+ const href = joinPaths(project1.webUrl, DASH_SCOPE, expectedPath);
+
+ expect(findDropdown().attributes('split-href')).toBe(href);
+ });
+
+ it('displays project name on the dropdown button', () => {
+ expect(findDropdown().props('text')).toBe(`${expectedLabel} ${project1.name}`);
+ });
+ });
+ },
+ );
+ });
+
+ describe('without localStorage', () => {
+ beforeEach(() => {
+ wrapper = mountComponent({ mountFn: mount });
+ });
+
+ it('does not attempt to save the selected project to the localStorage', async () => {
+ await showDropdown();
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+
+ expect(localStorage.setItem).not.toHaveBeenCalled();
+ });
+ });
+
+ describe('with localStorage', () => {
+ it('retrieves the selected project from the localStorage', async () => {
+ localStorage.setItem(
+ 'group--new-issue-recent-project',
+ JSON.stringify({
+ webUrl: project1.webUrl,
+ name: project1.name,
+ }),
+ );
+ wrapper = mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
+ await nextTick();
+ const dropdown = findDropdown();
+
+ expect(dropdown.attributes('split-href')).toBe(
+ joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new'),
+ );
+ expect(dropdown.props('text')).toBe(`New issue in ${project1.name}`);
+ });
+
+ it('retrieves legacy cache from the localStorage', async () => {
+ localStorage.setItem(
+ 'group--new-issue-recent-project',
+ JSON.stringify({
+ url: `${project1.webUrl}/issues/new`,
+ name: project1.name,
+ }),
+ );
+ wrapper = mountComponent({ mountFn: mount, propsData: { withLocalStorage: true } });
+ await nextTick();
+ const dropdown = findDropdown();
+
+ expect(dropdown.attributes('split-href')).toBe(
+ joinPaths(project1.webUrl, DASH_SCOPE, 'issues/new'),
+ );
+ expect(dropdown.props('text')).toBe(`New issue in ${project1.name}`);
+ });
+
+ describe.each(RESOURCE_TYPES)('with resource type %s', (resourceType) => {
+ it('computes the local storage key without a group', async () => {
+ wrapper = mountComponent({
+ mountFn: mount,
+ propsData: { resourceType, withLocalStorage: true },
+ });
+ await showDropdown();
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ await nextTick();
+
+ expect(localStorage.setItem).toHaveBeenLastCalledWith(
+ `group--new-${resourceType}-recent-project`,
+ expect.any(String),
+ );
+ });
+
+ it('computes the local storage key with a group', async () => {
+ const groupId = '22';
+ wrapper = mountComponent({
+ mountFn: mount,
+ propsData: { groupId, resourceType, withLocalStorage: true },
+ });
+ await showDropdown();
+ wrapper.findComponent(GlDropdownItem).vm.$emit('click', project1);
+ await nextTick();
+
+ expect(localStorage.setItem).toHaveBeenLastCalledWith(
+ `group-${groupId}-new-${resourceType}-recent-project`,
+ expect.any(String),
+ );
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/notes/system_note_spec.js b/spec/frontend/vue_shared/components/notes/system_note_spec.js
index 559f9bcb1a8..bcfd7a8ec70 100644
--- a/spec/frontend/vue_shared/components/notes/system_note_spec.js
+++ b/spec/frontend/vue_shared/components/notes/system_note_spec.js
@@ -4,6 +4,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import createStore from '~/notes/stores';
import IssueSystemNote from '~/vue_shared/components/notes/system_note.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import { renderGFM } from '~/behaviors/markdown/render_gfm';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -85,7 +86,7 @@ describe('system note component', () => {
it('renders outdated code lines', async () => {
mock
.onGet('/outdated_line_change_path')
- .reply(200, [
+ .reply(HTTP_STATUS_OK, [
{ rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
]);
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
deleted file mode 100644
index c8ca75787f1..00000000000
--- a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal_spec.js
+++ /dev/null
@@ -1,72 +0,0 @@
-import { GlModal } from '@gitlab/ui';
-import { shallowMount } from '@vue/test-utils';
-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 findRunnerAwsInstructions = () => wrapper.findComponent(RunnerAwsInstructions);
-
- const createComponent = (options) => {
- wrapper = shallowMount(RunnerAwsDeploymentsModal, {
- propsData: {
- modalId: mockModalId,
- },
- ...options,
- });
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- 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('renders modal contents', () => {
- expect(findRunnerAwsInstructions().exists()).toBe(true);
- });
-
- it('when contents trigger closing, modal closes', () => {
- const mockClose = jest.fn();
-
- createComponent({
- stubs: {
- GlModal: {
- template: '<div><slot/></div>',
- methods: {
- close: mockClose,
- },
- },
- },
- });
-
- expect(mockClose).toHaveBeenCalledTimes(0);
-
- findRunnerAwsInstructions().vm.$emit('close');
-
- expect(mockClose).toHaveBeenCalledTimes(1);
- });
-});
diff --git a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js b/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js
deleted file mode 100644
index 639668761ea..00000000000
--- a/spec/frontend/vue_shared/components/runner_aws_deployments/runner_aws_deployments_spec.js
+++ /dev/null
@@ -1,41 +0,0 @@
-import { shallowMount } from '@vue/test-utils';
-import { nextTick } from 'vue';
-import { extendedWrapper } from 'helpers/vue_test_utils_helper';
-import RunnerAwsDeployments from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments.vue';
-import RunnerAwsDeploymentsModal from '~/vue_shared/components/runner_aws_deployments/runner_aws_deployments_modal.vue';
-
-describe('RunnerAwsDeployments component', () => {
- let wrapper;
-
- const findModalButton = () => wrapper.findByTestId('show-modal-button');
- const findModal = () => wrapper.findComponent(RunnerAwsDeploymentsModal);
-
- const createComponent = () => {
- wrapper = extendedWrapper(shallowMount(RunnerAwsDeployments));
- };
-
- beforeEach(() => {
- createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
-
- it('should show the "Deploy GitLab Runner in AWS" button', () => {
- expect(findModalButton().exists()).toBe(true);
- expect(findModalButton().text()).toBe('Deploy GitLab Runner in AWS');
- });
-
- it('should not render the modal once mounted', () => {
- expect(findModal().exists()).toBe(false);
- });
-
- it('should render the modal once clicked', async () => {
- findModalButton().vm.$emit('click');
-
- await nextTick();
-
- expect(findModal().exists()).toBe(true);
- });
-});
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
index 4d566dbec0c..6d8f895a185 100644
--- 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
@@ -16,14 +16,18 @@ import {
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';
+import RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
+import ModalCopyButton from '~/vue_shared/components/modal_copy_button.vue';
+
jest.mock('~/lib/utils/url_utility', () => ({
...jest.requireActual('~/lib/utils/url_utility'),
visitUrl: jest.fn(),
}));
+const mockRegistrationToken = 'MY_TOKEN';
+
describe('RunnerAwsInstructions', () => {
let wrapper;
@@ -31,6 +35,7 @@ describe('RunnerAwsInstructions', () => {
const findEasyButtons = () => wrapper.findAllComponents(GlFormRadio);
const findEasyButtonAt = (i) => findEasyButtons().at(i);
const findLink = () => wrapper.findComponent(GlLink);
+ const findModalCopyButton = () => wrapper.findComponent(ModalCopyButton);
const findOkButton = () =>
wrapper
.findAllComponents(GlButton)
@@ -38,8 +43,12 @@ describe('RunnerAwsInstructions', () => {
.at(0);
const findCloseButton = () => wrapper.findByText(__('Close'));
- const createComponent = () => {
+ const createComponent = ({ props = {} } = {}) => {
wrapper = shallowMountExtended(RunnerAwsInstructions, {
+ propsData: {
+ registrationToken: mockRegistrationToken,
+ ...props,
+ },
stubs: {
GlSprintf,
},
@@ -109,6 +118,22 @@ describe('RunnerAwsInstructions', () => {
expect(findLink().attributes('href')).toBe(AWS_README_URL);
});
+ it('shows registration token and copy button', () => {
+ const token = wrapper.findByText(mockRegistrationToken);
+
+ expect(token.exists()).toBe(true);
+ expect(token.element.tagName).toBe('PRE');
+
+ expect(findModalCopyButton().props('text')).toBe(mockRegistrationToken);
+ });
+
+ it('does not show registration token and copy button when token is not present', () => {
+ createComponent({ props: { registrationToken: null } });
+
+ expect(wrapper.find('pre').exists()).toBe(false);
+ expect(findModalCopyButton().exists()).toBe(false);
+ });
+
it('triggers the modal to close', () => {
findCloseButton().vm.$emit('click');
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 19f2dd137ff..8f593b6aa1b 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
@@ -11,6 +11,7 @@ import RunnerInstructionsModal from '~/vue_shared/components/runner_instructions
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 RunnerAwsInstructions from '~/vue_shared/components/runner_instructions/instructions/runner_aws_instructions.vue';
import { mockRunnerPlatforms } from './mock_data';
@@ -156,6 +157,7 @@ describe('RunnerInstructionsModal component', () => {
platform | component
${'docker'} | ${RunnerDockerInstructions}
${'kubernetes'} | ${RunnerKubernetesInstructions}
+ ${'aws'} | ${RunnerAwsInstructions}
`('with platform "$platform"', ({ platform, component }) => {
beforeEach(async () => {
createComponent({ props: { defaultPlatformName: platform } });
diff --git a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
index 1e08394dd56..66d27b5d605 100644
--- a/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
+++ b/spec/frontend/vue_shared/components/security_reports/__snapshots__/security_summary_spec.js.snap
@@ -22,7 +22,7 @@ exports[`SecuritySummary component given the message {"countMessage": "%{critica
<span>
<strong
- class="text-danger-600 gl-px-2"
+ class="gl-text-red-600 gl-px-2"
>
1 High
@@ -55,7 +55,7 @@ exports[`SecuritySummary component given the message {"countMessage": "%{critica
>
<span>
<strong
- class="text-danger-800 gl-pl-4"
+ class="gl-text-red-800 gl-pl-4"
>
1 Critical
@@ -98,7 +98,7 @@ exports[`SecuritySummary component given the message {"countMessage": "%{critica
>
<span>
<strong
- class="text-danger-800 gl-pl-4"
+ class="gl-text-red-800 gl-pl-4"
>
1 Critical
@@ -108,7 +108,7 @@ exports[`SecuritySummary component given the message {"countMessage": "%{critica
<span>
<strong
- class="text-danger-600 gl-px-2"
+ class="gl-text-red-600 gl-px-2"
>
2 High
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap
new file mode 100644
index 00000000000..26c9a6f8d5a
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/components/__snapshots__/chunk_spec.js.snap
@@ -0,0 +1,24 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Chunk component rendering isHighlighted is true renders line numbers 1`] = `
+<div
+ class="gl-p-0! gl-z-index-3 diff-line-num gl-border-r gl-display-flex line-links line-numbers"
+ data-testid="line-numbers"
+>
+ <a
+ class="gl-user-select-none gl-shadow-none! file-line-blame"
+ href="some/blame/path.js#L71"
+ />
+
+ <a
+ class="gl-user-select-none gl-shadow-none! file-line-num"
+ data-line-number="71"
+ href="#L71"
+ id="L71"
+ >
+
+ 71
+
+ </a>
+</div>
+`;
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
new file mode 100644
index 00000000000..da9067a8ddc
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_deprecated_spec.js
@@ -0,0 +1,123 @@
+import { nextTick } from 'vue';
+import { GlIntersectionObserver } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import Chunk from '~/vue_shared/components/source_viewer/components/chunk_deprecated.vue';
+import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
+import LineHighlighter from '~/blob/line_highlighter';
+
+const lineHighlighter = new LineHighlighter();
+jest.mock('~/blob/line_highlighter', () =>
+ jest.fn().mockReturnValue({
+ highlightHash: jest.fn(),
+ }),
+);
+
+const DEFAULT_PROPS = {
+ chunkIndex: 2,
+ isHighlighted: false,
+ content: '// Line 1 content \n // Line 2 content',
+ startingFrom: 140,
+ totalLines: 50,
+ language: 'javascript',
+ blamePath: 'blame/file.js',
+};
+
+const hash = '#L142';
+
+describe('Chunk component', () => {
+ let wrapper;
+ let idleCallbackSpy;
+
+ const createComponent = (props = {}) => {
+ wrapper = shallowMountExtended(Chunk, {
+ mocks: { $route: { hash } },
+ propsData: { ...DEFAULT_PROPS, ...props },
+ });
+ };
+
+ const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
+ const findChunkLines = () => wrapper.findAllComponents(ChunkLine);
+ const findLineNumbers = () => wrapper.findAllByTestId('line-number');
+ const findContent = () => wrapper.findByTestId('content');
+
+ beforeEach(() => {
+ idleCallbackSpy = jest.spyOn(window, 'requestIdleCallback').mockImplementation((fn) => fn());
+ createComponent();
+ });
+
+ afterEach(() => wrapper.destroy());
+
+ describe('Intersection observer', () => {
+ it('renders an Intersection observer component', () => {
+ expect(findIntersectionObserver().exists()).toBe(true);
+ });
+
+ it('emits an appear event when intersection-observer appears', () => {
+ findIntersectionObserver().vm.$emit('appear');
+
+ expect(wrapper.emitted('appear')).toEqual([[DEFAULT_PROPS.chunkIndex]]);
+ });
+
+ it('does not emit an appear event is isHighlighted is true', () => {
+ createComponent({ isHighlighted: true });
+ findIntersectionObserver().vm.$emit('appear');
+
+ expect(wrapper.emitted('appear')).toEqual(undefined);
+ });
+ });
+
+ describe('rendering', () => {
+ it('does not register window.requestIdleCallback if isFirstChunk prop is true, renders lines immediately', () => {
+ jest.clearAllMocks();
+ createComponent({ isFirstChunk: true });
+
+ expect(window.requestIdleCallback).not.toHaveBeenCalled();
+ expect(findContent().exists()).toBe(true);
+ });
+
+ it('does not render a Chunk Line component if isHighlighted is false', () => {
+ expect(findChunkLines().length).toBe(0);
+ });
+
+ it('does not render simplified line numbers and content if browser is not in idle state', () => {
+ idleCallbackSpy.mockRestore();
+ createComponent();
+
+ expect(findLineNumbers()).toHaveLength(0);
+ expect(findContent().exists()).toBe(false);
+ });
+
+ it('renders simplified line numbers and content if isHighlighted is false', () => {
+ expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines);
+
+ expect(findLineNumbers().at(0).attributes('id')).toBe(`L${DEFAULT_PROPS.startingFrom + 1}`);
+
+ expect(findContent().text()).toBe(DEFAULT_PROPS.content);
+ });
+
+ it('renders Chunk Line components if isHighlighted is true', () => {
+ const splitContent = DEFAULT_PROPS.content.split('\n');
+ createComponent({ isHighlighted: true });
+
+ expect(findChunkLines().length).toBe(splitContent.length);
+
+ expect(findChunkLines().at(0).props()).toMatchObject({
+ number: DEFAULT_PROPS.startingFrom + 1,
+ content: splitContent[0],
+ language: DEFAULT_PROPS.language,
+ blamePath: DEFAULT_PROPS.blamePath,
+ });
+ });
+
+ it('does not scroll to route hash if last chunk is not loaded', () => {
+ expect(LineHighlighter).not.toHaveBeenCalled();
+ });
+
+ it('scrolls to route hash if last chunk is loaded', async () => {
+ createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
+ await nextTick();
+ expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
+ expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
index 657bd59dac6..95ef11d776a 100644
--- a/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/components/chunk_spec.js
@@ -2,27 +2,7 @@ import { nextTick } from 'vue';
import { GlIntersectionObserver } from '@gitlab/ui';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
-import ChunkLine from '~/vue_shared/components/source_viewer/components/chunk_line.vue';
-import LineHighlighter from '~/blob/line_highlighter';
-
-const lineHighlighter = new LineHighlighter();
-jest.mock('~/blob/line_highlighter', () =>
- jest.fn().mockReturnValue({
- highlightHash: jest.fn(),
- }),
-);
-
-const DEFAULT_PROPS = {
- chunkIndex: 2,
- isHighlighted: false,
- content: '// Line 1 content \n // Line 2 content',
- startingFrom: 140,
- totalLines: 50,
- language: 'javascript',
- blamePath: 'blame/file.js',
-};
-
-const hash = '#L142';
+import { CHUNK_1, CHUNK_2 } from '../mock_data';
describe('Chunk component', () => {
let wrapper;
@@ -30,14 +10,13 @@ describe('Chunk component', () => {
const createComponent = (props = {}) => {
wrapper = shallowMountExtended(Chunk, {
- mocks: { $route: { hash } },
- propsData: { ...DEFAULT_PROPS, ...props },
+ propsData: { ...CHUNK_1, ...props },
+ provide: { glFeatures: { fileLineBlame: true } },
});
};
const findIntersectionObserver = () => wrapper.findComponent(GlIntersectionObserver);
- const findChunkLines = () => wrapper.findAllComponents(ChunkLine);
- const findLineNumbers = () => wrapper.findAllByTestId('line-number');
+ const findLineNumbers = () => wrapper.findAllByTestId('line-numbers');
const findContent = () => wrapper.findByTestId('content');
beforeEach(() => {
@@ -52,72 +31,57 @@ describe('Chunk component', () => {
expect(findIntersectionObserver().exists()).toBe(true);
});
- it('emits an appear event when intersection-observer appears', () => {
+ it('renders highlighted content if appear event is emitted', async () => {
+ createComponent({ chunkIndex: 1, isHighlighted: false });
findIntersectionObserver().vm.$emit('appear');
- expect(wrapper.emitted('appear')).toEqual([[DEFAULT_PROPS.chunkIndex]]);
- });
-
- it('does not emit an appear event is isHighlighted is true', () => {
- createComponent({ isHighlighted: true });
- findIntersectionObserver().vm.$emit('appear');
+ await nextTick();
- expect(wrapper.emitted('appear')).toEqual(undefined);
+ expect(findContent().exists()).toBe(true);
});
});
describe('rendering', () => {
- it('does not register window.requestIdleCallback if isFirstChunk prop is true, renders lines immediately', () => {
+ it('does not register window.requestIdleCallback for the first chunk, renders content immediately', () => {
jest.clearAllMocks();
- createComponent({ isFirstChunk: true });
expect(window.requestIdleCallback).not.toHaveBeenCalled();
- expect(findContent().exists()).toBe(true);
- });
-
- it('does not render a Chunk Line component if isHighlighted is false', () => {
- expect(findChunkLines().length).toBe(0);
+ expect(findContent().text()).toBe(CHUNK_1.highlightedContent);
});
- it('does not render simplified line numbers and content if browser is not in idle state', () => {
+ it('does not render content if browser is not in idle state', () => {
idleCallbackSpy.mockRestore();
- createComponent();
+ createComponent({ chunkIndex: 1, ...CHUNK_2 });
expect(findLineNumbers()).toHaveLength(0);
expect(findContent().exists()).toBe(false);
});
- it('renders simplified line numbers and content if isHighlighted is false', () => {
- expect(findLineNumbers().length).toBe(DEFAULT_PROPS.totalLines);
+ describe('isHighlighted is false', () => {
+ beforeEach(() => createComponent(CHUNK_2));
- expect(findLineNumbers().at(0).attributes('id')).toBe(`L${DEFAULT_PROPS.startingFrom + 1}`);
+ it('does not render line numbers', () => {
+ expect(findLineNumbers()).toHaveLength(0);
+ });
- expect(findContent().text()).toBe(DEFAULT_PROPS.content);
+ it('renders raw content', () => {
+ expect(findContent().text()).toBe(CHUNK_2.rawContent);
+ });
});
- it('renders Chunk Line components if isHighlighted is true', () => {
- const splitContent = DEFAULT_PROPS.content.split('\n');
- createComponent({ isHighlighted: true });
+ describe('isHighlighted is true', () => {
+ beforeEach(() => createComponent({ ...CHUNK_2, isHighlighted: true }));
- expect(findChunkLines().length).toBe(splitContent.length);
+ it('renders line numbers', () => {
+ expect(findLineNumbers()).toHaveLength(CHUNK_2.totalLines);
- expect(findChunkLines().at(0).props()).toMatchObject({
- number: DEFAULT_PROPS.startingFrom + 1,
- content: splitContent[0],
- language: DEFAULT_PROPS.language,
- blamePath: DEFAULT_PROPS.blamePath,
+ // Opted for a snapshot test here since the output is simple and verifies native HTML elements
+ expect(findLineNumbers().at(0).element).toMatchSnapshot();
});
- });
- it('does not scroll to route hash if last chunk is not loaded', () => {
- expect(LineHighlighter).not.toHaveBeenCalled();
- });
-
- it('scrolls to route hash if last chunk is loaded', async () => {
- createComponent({ totalChunks: DEFAULT_PROPS.chunkIndex + 1 });
- await nextTick();
- expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
- expect(lineHighlighter.highlightHash).toHaveBeenCalledWith(hash);
+ it('renders highlighted content', () => {
+ expect(findContent().text()).toBe(CHUNK_2.highlightedContent);
+ });
});
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
index 4a995e2fde1..d2dd4afe09e 100644
--- a/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
+++ b/spec/frontend/vue_shared/components/source_viewer/highlight_util_spec.js
@@ -1,15 +1,10 @@
-import hljs from 'highlight.js/lib/core';
-import languageLoader from '~/content_editor/services/highlight_js_language_loader';
+import hljs from 'highlight.js';
import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import { highlight } from '~/vue_shared/components/source_viewer/workers/highlight_utils';
+import { LINES_PER_CHUNK, NEWLINE } from '~/vue_shared/components/source_viewer/constants';
-jest.mock('highlight.js/lib/core', () => ({
- highlight: jest.fn().mockReturnValue({}),
- registerLanguage: jest.fn(),
-}));
-
-jest.mock('~/content_editor/services/highlight_js_language_loader', () => ({
- javascript: jest.fn().mockReturnValue({ default: jest.fn() }),
+jest.mock('highlight.js', () => ({
+ highlight: jest.fn().mockReturnValue({ value: 'highlighted content' }),
}));
jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({
@@ -17,28 +12,61 @@ jest.mock('~/vue_shared/components/source_viewer/plugins/index', () => ({
}));
const fileType = 'text';
-const content = 'function test() { return true };';
+const rawContent = 'function test() { return true }; \n // newline';
+const highlightedContent = 'highlighted content';
const language = 'javascript';
describe('Highlight utility', () => {
- beforeEach(() => highlight(fileType, content, language));
-
- it('loads the language', () => {
- expect(languageLoader.javascript).toHaveBeenCalled();
- });
+ beforeEach(() => highlight(fileType, rawContent, language));
it('registers the plugins', () => {
expect(registerPlugins).toHaveBeenCalled();
});
- it('registers the language', () => {
- expect(hljs.registerLanguage).toHaveBeenCalledWith(
- language,
- languageLoader[language]().default,
+ it('highlights the content', () => {
+ expect(hljs.highlight).toHaveBeenCalledWith(rawContent, { language });
+ });
+
+ it('splits the content into chunks', () => {
+ const contentArray = Array.from({ length: 140 }, () => 'newline'); // simulate 140 lines of code
+
+ const chunks = [
+ {
+ language,
+ highlightedContent,
+ rawContent: contentArray.slice(0, 70).join(NEWLINE), // first 70 lines
+ startingFrom: 0,
+ totalLines: LINES_PER_CHUNK,
+ },
+ {
+ language,
+ highlightedContent: '',
+ rawContent: contentArray.slice(70, 140).join(NEWLINE), // last 70 lines
+ startingFrom: 70,
+ totalLines: LINES_PER_CHUNK,
+ },
+ ];
+
+ expect(highlight(fileType, contentArray.join(NEWLINE), language)).toEqual(
+ expect.arrayContaining(chunks),
);
});
+});
- it('highlights the content', () => {
- expect(hljs.highlight).toHaveBeenCalledWith(content, { language });
+describe('unsupported languages', () => {
+ const unsupportedLanguage = 'some_unsupported_language';
+
+ beforeEach(() => highlight(fileType, rawContent, unsupportedLanguage));
+
+ it('does not register plugins', () => {
+ expect(registerPlugins).not.toHaveBeenCalled();
+ });
+
+ it('does not attempt to highlight the content', () => {
+ expect(hljs.highlight).not.toHaveBeenCalled();
+ });
+
+ it('does not return a result', () => {
+ expect(highlight(fileType, rawContent, unsupportedLanguage)).toBe(undefined);
});
});
diff --git a/spec/frontend/vue_shared/components/source_viewer/mock_data.js b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
new file mode 100644
index 00000000000..f35e9607d5c
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/mock_data.js
@@ -0,0 +1,24 @@
+const path = 'some/path.js';
+const blamePath = 'some/blame/path.js';
+
+export const LANGUAGE_MOCK = 'docker';
+
+export const BLOB_DATA_MOCK = { language: LANGUAGE_MOCK, path, blamePath };
+
+export const CHUNK_1 = {
+ isHighlighted: true,
+ rawContent: 'chunk 1 raw',
+ highlightedContent: 'chunk 1 highlighted',
+ totalLines: 70,
+ startingFrom: 0,
+ blamePath,
+};
+
+export const CHUNK_2 = {
+ isHighlighted: false,
+ rawContent: 'chunk 2 raw',
+ highlightedContent: 'chunk 2 highlighted',
+ totalLines: 40,
+ startingFrom: 70,
+ blamePath,
+};
diff --git a/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
new file mode 100644
index 00000000000..0beec8e9d3e
--- /dev/null
+++ b/spec/frontend/vue_shared/components/source_viewer/source_viewer_deprecated_spec.js
@@ -0,0 +1,177 @@
+import hljs from 'highlight.js/lib/core';
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer_deprecated.vue';
+import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
+import Chunk from '~/vue_shared/components/source_viewer/components/chunk_deprecated.vue';
+import {
+ EVENT_ACTION,
+ EVENT_LABEL_VIEWER,
+ EVENT_LABEL_FALLBACK,
+ ROUGE_TO_HLJS_LANGUAGE_MAP,
+ LINES_PER_CHUNK,
+} from '~/vue_shared/components/source_viewer/constants';
+import waitForPromises from 'helpers/wait_for_promises';
+import LineHighlighter from '~/blob/line_highlighter';
+import eventHub from '~/notes/event_hub';
+import Tracking from '~/tracking';
+
+jest.mock('~/blob/line_highlighter');
+jest.mock('highlight.js/lib/core');
+jest.mock('~/vue_shared/components/source_viewer/plugins/index');
+Vue.use(VueRouter);
+const router = new VueRouter();
+
+const generateContent = (content, totalLines = 1, delimiter = '\n') => {
+ let generatedContent = '';
+ for (let i = 0; i < totalLines; i += 1) {
+ generatedContent += `Line: ${i + 1} = ${content}${delimiter}`;
+ }
+ return generatedContent;
+};
+
+const execImmediately = (callback) => callback();
+
+describe('Source Viewer component', () => {
+ let wrapper;
+ const language = 'docker';
+ const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
+ const chunk1 = generateContent('// Some source code 1', 70);
+ const chunk2 = generateContent('// Some source code 2', 70);
+ const chunk3 = generateContent('// Some source code 3', 70, '\r\n');
+ const chunk3Result = generateContent('// Some source code 3', 70, '\n');
+ const content = chunk1 + chunk2 + chunk3;
+ const path = 'some/path.js';
+ const blamePath = 'some/blame/path.js';
+ const fileType = 'javascript';
+ const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType };
+ const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
+
+ const createComponent = async (blob = {}) => {
+ wrapper = shallowMountExtended(SourceViewer, {
+ router,
+ propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } },
+ });
+ await waitForPromises();
+ };
+
+ const findChunks = () => wrapper.findAllComponents(Chunk);
+
+ beforeEach(() => {
+ hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
+ hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
+ jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
+ jest.spyOn(eventHub, '$emit');
+ jest.spyOn(Tracking, 'event');
+
+ return createComponent();
+ });
+
+ afterEach(() => wrapper.destroy());
+
+ describe('event tracking', () => {
+ it('fires a tracking event when the component is created', () => {
+ const eventData = { label: EVENT_LABEL_VIEWER, property: language };
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ });
+
+ it('does not emit an error event when the language is supported', () => {
+ expect(wrapper.emitted('error')).toBeUndefined();
+ });
+
+ it('fires a tracking event and emits an error when the language is not supported', () => {
+ const unsupportedLanguage = 'apex';
+ const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
+ createComponent({ language: unsupportedLanguage });
+
+ expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
+
+ 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 }));
+
+ it('registers our plugins for Highlight.js', () => {
+ expect(registerPlugins).toHaveBeenCalledWith(hljs, fileType, content);
+ });
+
+ it('registers the language definition', async () => {
+ const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith(
+ mappedLanguage,
+ languageDefinition.default,
+ );
+ });
+
+ it('registers json language definition if fileType is package_json', async () => {
+ await createComponent({ language: 'json', fileType: 'package_json' });
+ const languageDefinition = await import(`highlight.js/lib/languages/json`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default);
+ });
+
+ it('correctly maps languages starting with uppercase', async () => {
+ await createComponent({ language: 'Ruby' });
+ const languageDefinition = await import(`highlight.js/lib/languages/ruby`);
+
+ expect(hljs.registerLanguage).toHaveBeenCalledWith('ruby', languageDefinition.default);
+ });
+
+ it('highlights the first chunk', () => {
+ expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
+ expect(findChunks().at(0).props('isFirstChunk')).toBe(true);
+ });
+
+ describe('auto-detects if a language cannot be loaded', () => {
+ beforeEach(() => createComponent({ language: 'some_unknown_language' }));
+
+ it('highlights the content with auto-detection', () => {
+ expect(hljs.highlightAuto).toHaveBeenCalledWith(chunk1.trim());
+ });
+ });
+ });
+
+ describe('rendering', () => {
+ it.each`
+ chunkIndex | chunkContent | totalChunks
+ ${0} | ${chunk1} | ${0}
+ ${1} | ${chunk2} | ${3}
+ ${2} | ${chunk3Result} | ${3}
+ `('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => {
+ const chunk = findChunks().at(chunkIndex);
+
+ expect(chunk.props('content')).toContain(chunkContent.trim());
+
+ expect(chunk.props()).toMatchObject({
+ totalLines: LINES_PER_CHUNK,
+ startingFrom: LINES_PER_CHUNK * chunkIndex,
+ totalChunks,
+ });
+ });
+
+ it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
+ findChunks().at(0).vm.$emit('appear');
+ expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
+ });
+ });
+
+ describe('LineHighlighter', () => {
+ it('instantiates the lineHighlighter class', async () => {
+ expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
+ });
+ });
+});
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 5461d38599d..1c75442b4a8 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
@@ -1,70 +1,27 @@
-import hljs from 'highlight.js/lib/core';
-import Vue from 'vue';
-import VueRouter from 'vue-router';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import SourceViewer from '~/vue_shared/components/source_viewer/source_viewer.vue';
-import { registerPlugins } from '~/vue_shared/components/source_viewer/plugins/index';
import Chunk from '~/vue_shared/components/source_viewer/components/chunk.vue';
-import {
- EVENT_ACTION,
- EVENT_LABEL_VIEWER,
- EVENT_LABEL_FALLBACK,
- ROUGE_TO_HLJS_LANGUAGE_MAP,
- LINES_PER_CHUNK,
-} from '~/vue_shared/components/source_viewer/constants';
-import waitForPromises from 'helpers/wait_for_promises';
-import LineHighlighter from '~/blob/line_highlighter';
-import eventHub from '~/notes/event_hub';
+import { EVENT_ACTION, EVENT_LABEL_VIEWER } from '~/vue_shared/components/source_viewer/constants';
import Tracking from '~/tracking';
+import addBlobLinksTracking from '~/blob/blob_links_tracking';
+import { BLOB_DATA_MOCK, CHUNK_1, CHUNK_2, LANGUAGE_MOCK } from './mock_data';
-jest.mock('~/blob/line_highlighter');
-jest.mock('highlight.js/lib/core');
-jest.mock('~/vue_shared/components/source_viewer/plugins/index');
-Vue.use(VueRouter);
-const router = new VueRouter();
-
-const generateContent = (content, totalLines = 1, delimiter = '\n') => {
- let generatedContent = '';
- for (let i = 0; i < totalLines; i += 1) {
- generatedContent += `Line: ${i + 1} = ${content}${delimiter}`;
- }
- return generatedContent;
-};
-
-const execImmediately = (callback) => callback();
+jest.mock('~/blob/blob_links_tracking');
describe('Source Viewer component', () => {
let wrapper;
- const language = 'docker';
- const mappedLanguage = ROUGE_TO_HLJS_LANGUAGE_MAP[language];
- const chunk1 = generateContent('// Some source code 1', 70);
- const chunk2 = generateContent('// Some source code 2', 70);
- const chunk3 = generateContent('// Some source code 3', 70, '\r\n');
- const chunk3Result = generateContent('// Some source code 3', 70, '\n');
- const content = chunk1 + chunk2 + chunk3;
- const path = 'some/path.js';
- const blamePath = 'some/blame/path.js';
- const fileType = 'javascript';
- const DEFAULT_BLOB_DATA = { language, rawTextBlob: content, path, blamePath, fileType };
- const highlightedContent = `<span data-testid='test-highlighted' id='LC1'>${content}</span><span id='LC2'></span>`;
+ const CHUNKS_MOCK = [CHUNK_1, CHUNK_2];
- const createComponent = async (blob = {}) => {
+ const createComponent = () => {
wrapper = shallowMountExtended(SourceViewer, {
- router,
- propsData: { blob: { ...DEFAULT_BLOB_DATA, ...blob } },
+ propsData: { blob: BLOB_DATA_MOCK, chunks: CHUNKS_MOCK },
});
- await waitForPromises();
};
const findChunks = () => wrapper.findAllComponents(Chunk);
beforeEach(() => {
- hljs.highlight.mockImplementation(() => ({ value: highlightedContent }));
- hljs.highlightAuto.mockImplementation(() => ({ value: highlightedContent }));
- jest.spyOn(window, 'requestIdleCallback').mockImplementation(execImmediately);
- jest.spyOn(eventHub, '$emit');
jest.spyOn(Tracking, 'event');
-
return createComponent();
});
@@ -72,106 +29,19 @@ describe('Source Viewer component', () => {
describe('event tracking', () => {
it('fires a tracking event when the component is created', () => {
- const eventData = { label: EVENT_LABEL_VIEWER, property: language };
+ const eventData = { label: EVENT_LABEL_VIEWER, property: LANGUAGE_MOCK };
expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
});
- it('does not emit an error event when the language is supported', () => {
- expect(wrapper.emitted('error')).toBeUndefined();
- });
-
- it('fires a tracking event and emits an error when the language is not supported', () => {
- const unsupportedLanguage = 'apex';
- const eventData = { label: EVENT_LABEL_FALLBACK, property: unsupportedLanguage };
- createComponent({ language: unsupportedLanguage });
-
- expect(Tracking.event).toHaveBeenCalledWith(undefined, EVENT_ACTION, eventData);
- expect(wrapper.emitted('error')).toHaveLength(1);
- });
- });
-
- 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 }));
-
- it('registers our plugins for Highlight.js', () => {
- expect(registerPlugins).toHaveBeenCalledWith(hljs, fileType, content);
- });
-
- it('registers the language definition', async () => {
- const languageDefinition = await import(`highlight.js/lib/languages/${mappedLanguage}`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith(
- mappedLanguage,
- languageDefinition.default,
- );
- });
-
- it('registers json language definition if fileType is package_json', async () => {
- await createComponent({ language: 'json', fileType: 'package_json' });
- const languageDefinition = await import(`highlight.js/lib/languages/json`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith('json', languageDefinition.default);
- });
-
- it('correctly maps languages starting with uppercase', async () => {
- await createComponent({ language: 'Ruby' });
- const languageDefinition = await import(`highlight.js/lib/languages/ruby`);
-
- expect(hljs.registerLanguage).toHaveBeenCalledWith('ruby', languageDefinition.default);
- });
-
- it('highlights the first chunk', () => {
- expect(hljs.highlight).toHaveBeenCalledWith(chunk1.trim(), { language: mappedLanguage });
- expect(findChunks().at(0).props('isFirstChunk')).toBe(true);
- });
-
- describe('auto-detects if a language cannot be loaded', () => {
- beforeEach(() => createComponent({ language: 'some_unknown_language' }));
-
- it('highlights the content with auto-detection', () => {
- expect(hljs.highlightAuto).toHaveBeenCalledWith(chunk1.trim());
- });
+ it('adds blob links tracking', () => {
+ expect(addBlobLinksTracking).toHaveBeenCalled();
});
});
describe('rendering', () => {
- it.each`
- chunkIndex | chunkContent | totalChunks
- ${0} | ${chunk1} | ${0}
- ${1} | ${chunk2} | ${3}
- ${2} | ${chunk3Result} | ${3}
- `('renders chunk $chunkIndex', ({ chunkIndex, chunkContent, totalChunks }) => {
- const chunk = findChunks().at(chunkIndex);
-
- expect(chunk.props('content')).toContain(chunkContent.trim());
-
- expect(chunk.props()).toMatchObject({
- totalLines: LINES_PER_CHUNK,
- startingFrom: LINES_PER_CHUNK * chunkIndex,
- totalChunks,
- });
- });
-
- it('emits showBlobInteractionZones on the eventHub when chunk appears', () => {
- findChunks().at(0).vm.$emit('appear');
- expect(eventHub.$emit).toHaveBeenCalledWith('showBlobInteractionZones', path);
- });
- });
-
- describe('LineHighlighter', () => {
- it('instantiates the lineHighlighter class', async () => {
- expect(LineHighlighter).toHaveBeenCalledWith({ scrollBehavior: 'auto' });
+ it('renders a Chunk component for each chunk', () => {
+ expect(findChunks().at(0).props()).toMatchObject(CHUNK_1);
+ expect(findChunks().at(1).props()).toMatchObject(CHUNK_2);
});
});
});
diff --git a/spec/frontend/vue_shared/components/url_sync_spec.js b/spec/frontend/vue_shared/components/url_sync_spec.js
index acda1a64a75..30a7439579f 100644
--- a/spec/frontend/vue_shared/components/url_sync_spec.js
+++ b/spec/frontend/vue_shared/components/url_sync_spec.js
@@ -1,7 +1,10 @@
import { shallowMount } from '@vue/test-utils';
-import { historyPushState } from '~/lib/utils/common_utils';
+import { historyPushState, historyReplaceState } from '~/lib/utils/common_utils';
import { mergeUrlParams, setUrlParams } from '~/lib/utils/url_utility';
-import UrlSyncComponent, { URL_SET_PARAMS_STRATEGY } from '~/vue_shared/components/url_sync.vue';
+import UrlSyncComponent, {
+ URL_SET_PARAMS_STRATEGY,
+ HISTORY_REPLACE_UPDATE_METHOD,
+} from '~/vue_shared/components/url_sync.vue';
jest.mock('~/lib/utils/url_utility', () => ({
mergeUrlParams: jest.fn((query, url) => `urlParams: ${JSON.stringify(query)} ${url}`),
@@ -10,6 +13,7 @@ jest.mock('~/lib/utils/url_utility', () => ({
jest.mock('~/lib/utils/common_utils', () => ({
historyPushState: jest.fn(),
+ historyReplaceState: jest.fn(),
}));
describe('url sync component', () => {
@@ -18,14 +22,12 @@ describe('url sync component', () => {
const findButton = () => wrapper.find('button');
- const createComponent = ({
- query = mockQuery,
- scopedSlots,
- slots,
- urlParamsUpdateStrategy,
- } = {}) => {
+ const createComponent = ({ props = {}, scopedSlots, slots } = {}) => {
wrapper = shallowMount(UrlSyncComponent, {
- propsData: { query, ...(urlParamsUpdateStrategy && { urlParamsUpdateStrategy }) },
+ propsData: {
+ query: mockQuery,
+ ...props,
+ },
scopedSlots,
slots,
});
@@ -35,32 +37,27 @@ describe('url sync component', () => {
wrapper.destroy();
});
- const expectUrlSyncFactory = (
+ const expectUrlSyncWithMergeUrlParams = (
query,
times,
- urlParamsUpdateStrategy,
- urlOptions,
- urlReturnValue,
+ mergeUrlParamsReturnValue,
+ historyMethod = historyPushState,
) => {
- expect(urlParamsUpdateStrategy).toHaveBeenCalledTimes(times);
- expect(urlParamsUpdateStrategy).toHaveBeenCalledWith(query, window.location.href, urlOptions);
-
- expect(historyPushState).toHaveBeenCalledTimes(times);
- expect(historyPushState).toHaveBeenCalledWith(urlReturnValue);
- };
+ expect(mergeUrlParams).toHaveBeenCalledTimes(times);
+ expect(mergeUrlParams).toHaveBeenCalledWith(query, window.location.href, {
+ spreadArrays: true,
+ });
- const expectUrlSyncWithMergeUrlParams = (query, times, mergeUrlParamsReturnValue) => {
- expectUrlSyncFactory(
- query,
- times,
- mergeUrlParams,
- { spreadArrays: true },
- mergeUrlParamsReturnValue,
- );
+ expect(historyMethod).toHaveBeenCalledTimes(times);
+ expect(historyMethod).toHaveBeenCalledWith(mergeUrlParamsReturnValue);
};
const expectUrlSyncWithSetUrlParams = (query, times, setUrlParamsReturnValue) => {
- expectUrlSyncFactory(query, times, setUrlParams, true, setUrlParamsReturnValue);
+ expect(setUrlParams).toHaveBeenCalledTimes(times);
+ expect(setUrlParams).toHaveBeenCalledWith(query, window.location.href, true, true, true);
+
+ expect(historyPushState).toHaveBeenCalledTimes(times);
+ expect(historyPushState).toHaveBeenCalledWith(setUrlParamsReturnValue);
};
describe('with query as a props', () => {
@@ -86,13 +83,32 @@ describe('url sync component', () => {
describe('with url-params-update-strategy equals to URL_SET_PARAMS_STRATEGY', () => {
it('uses setUrlParams to generate URL', () => {
createComponent({
- urlParamsUpdateStrategy: URL_SET_PARAMS_STRATEGY,
+ props: {
+ urlParamsUpdateStrategy: URL_SET_PARAMS_STRATEGY,
+ },
});
expectUrlSyncWithSetUrlParams(mockQuery, 1, setUrlParams.mock.results[0].value);
});
});
+ describe('with history-update-method equals to HISTORY_REPLACE_UPDATE_METHOD', () => {
+ it('uses historyReplaceState to update the URL', () => {
+ createComponent({
+ props: {
+ historyUpdateMethod: HISTORY_REPLACE_UPDATE_METHOD,
+ },
+ });
+
+ expectUrlSyncWithMergeUrlParams(
+ mockQuery,
+ 1,
+ mergeUrlParams.mock.results[0].value,
+ historyReplaceState,
+ );
+ });
+ });
+
describe('with scoped slot', () => {
const scopedSlots = {
default: `
@@ -101,13 +117,13 @@ describe('url sync component', () => {
};
it('renders the scoped slot', () => {
- createComponent({ query: null, scopedSlots });
+ createComponent({ props: { query: null }, scopedSlots });
expect(findButton().exists()).toBe(true);
});
it('syncs the url with the scoped slots function', () => {
- createComponent({ query: null, scopedSlots });
+ createComponent({ props: { query: null }, scopedSlots });
findButton().trigger('click');
@@ -121,7 +137,7 @@ describe('url sync component', () => {
};
it('renders the default slot', () => {
- createComponent({ query: null, slots });
+ createComponent({ props: { query: null }, slots });
expect(findButton().exists()).toBe(true);
});
diff --git a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
index 1ad6d043399..63371b1492b 100644
--- a/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
+++ b/spec/frontend/vue_shared/components/user_avatar/user_avatar_list_spec.js
@@ -4,6 +4,7 @@ import { nextTick } from 'vue';
import { TEST_HOST } from 'spec/test_constants';
import UserAvatarLink from '~/vue_shared/components/user_avatar/user_avatar_link.vue';
import UserAvatarList from '~/vue_shared/components/user_avatar/user_avatar_list.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
const TEST_IMAGE_SIZE = 7;
const TEST_BREAKPOINT = 5;
@@ -16,10 +17,13 @@ const createUser = (id) => ({
web_url: `${TEST_HOST}/${id}`,
avatar_url: `${TEST_HOST}/${id}/avatar`,
});
+
const createList = (n) =>
Array(n)
.fill(1)
.map((x, id) => createUser(id));
+const createListCamelCase = (n) =>
+ createList(n).map((user) => convertObjectPropsToCamelCase(user, { deep: true }));
describe('UserAvatarList', () => {
let props;
@@ -75,14 +79,14 @@ describe('UserAvatarList', () => {
props.breakpoint = 0;
});
- it('renders avatars', () => {
+ const linkProps = () =>
+ wrapper.findAllComponents(UserAvatarLink).wrappers.map((x) => x.props());
+
+ it('renders avatars when user has snake_case attributes', () => {
const items = createList(20);
factory({ propsData: { items } });
- const links = wrapper.findAllComponents(UserAvatarLink);
- const linkProps = links.wrappers.map((x) => x.props());
-
- expect(linkProps).toEqual(
+ expect(linkProps()).toEqual(
items.map((x) =>
expect.objectContaining({
linkHref: x.web_url,
@@ -94,6 +98,23 @@ describe('UserAvatarList', () => {
),
);
});
+
+ it('renders avatars when user has camelCase attributes', () => {
+ const items = createListCamelCase(20);
+ factory({ propsData: { items } });
+
+ expect(linkProps()).toEqual(
+ items.map((x) =>
+ expect.objectContaining({
+ linkHref: x.webUrl,
+ imgSrc: x.avatarUrl,
+ imgAlt: x.name,
+ tooltipText: x.name,
+ imgSize: TEST_IMAGE_SIZE,
+ }),
+ ),
+ );
+ });
});
describe('with breakpoint and length equal to breakpoint', () => {
diff --git a/spec/frontend/vue_shared/components/user_select_spec.js b/spec/frontend/vue_shared/components/user_select_spec.js
index 874796f653a..b0e9584a15b 100644
--- a/spec/frontend/vue_shared/components/user_select_spec.js
+++ b/spec/frontend/vue_shared/components/user_select_spec.js
@@ -285,6 +285,20 @@ describe('User select dropdown', () => {
expect(wrapper.emitted('input')).toEqual([[[]]]);
});
+ it('hides the dropdown after clicking on `Unassigned`', async () => {
+ createComponent({
+ props: {
+ value: [assignee],
+ },
+ });
+ wrapper.vm.$refs.dropdown.hide = jest.fn();
+ await waitForPromises();
+
+ findUnassignLink().trigger('click');
+
+ expect(wrapper.vm.$refs.dropdown.hide).toHaveBeenCalledTimes(1);
+ });
+
it('emits an empty array after unselecting the only selected assignee', async () => {
createComponent({
props: {
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 2a0d2089fe3..18afe049149 100644
--- a/spec/frontend/vue_shared/components/web_ide_link_spec.js
+++ b/spec/frontend/vue_shared/components/web_ide_link_spec.js
@@ -1,4 +1,4 @@
-import { GlButton, GlModal, GlPopover } from '@gitlab/ui';
+import { GlButton, GlLink, GlModal, GlPopover } from '@gitlab/ui';
import { nextTick } from 'vue';
import ActionsButton from '~/vue_shared/components/actions_button.vue';
@@ -147,6 +147,11 @@ describe('Web IDE link component', () => {
const findForkConfirmModal = () => wrapper.findComponent(ConfirmForkModal);
const findUserCalloutDismisser = () => wrapper.findComponent(UserCalloutDismisser);
const findNewWebIdeCalloutPopover = () => wrapper.findComponent(GlPopover);
+ const findTryItOutLink = () =>
+ wrapper
+ .findAllComponents(GlLink)
+ .filter((link) => link.text().includes('Try it out'))
+ .at(0);
it.each([
{
@@ -516,6 +521,12 @@ describe('Web IDE link component', () => {
expect(dismiss).toHaveBeenCalled();
});
+ it('dismisses the callout when try it now link is clicked', () => {
+ findTryItOutLink().vm.$emit('click');
+
+ expect(dismiss).toHaveBeenCalled();
+ });
+
it('dismisses the callout when action button is clicked', () => {
findActionsButton().vm.$emit('actionClicked');
diff --git a/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
index d59cbce6633..a0b1d64b97c 100644
--- a/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
+++ b/spec/frontend/vue_shared/issuable/issuable_blocked_icon_spec.js
@@ -8,6 +8,7 @@ import waitForPromises from 'helpers/wait_for_promises';
import IssuableBlockedIcon from '~/vue_shared/components/issuable_blocked_icon/issuable_blocked_icon.vue';
import { blockingIssuablesQueries } from '~/vue_shared/components/issuable_blocked_icon/constants';
import { issuableTypes } from '~/boards/constants';
+import { TYPE_ISSUE } from '~/issues/constants';
import { truncate } from '~/lib/utils/text_utility';
import {
mockIssue,
@@ -57,7 +58,7 @@ describe('IssuableBlockedIcon', () => {
item = mockBlockedIssue1,
blockingIssuablesSpy = jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1),
issuableItem = mockIssue,
- issuableType = issuableTypes.issue,
+ issuableType = TYPE_ISSUE,
} = {}) => {
mockApollo = createMockApollo([
[blockingIssuablesQueries[issuableType].query, blockingIssuablesSpy],
@@ -86,7 +87,7 @@ describe('IssuableBlockedIcon', () => {
data = {},
loading = false,
mockIssuable = mockIssue,
- issuableType = issuableTypes.issue,
+ issuableType = TYPE_ISSUE,
} = {}) => {
wrapper = extendedWrapper(
shallowMount(IssuableBlockedIcon, {
@@ -120,9 +121,9 @@ describe('IssuableBlockedIcon', () => {
};
it.each`
- mockIssuable | issuableType | expectedIcon
- ${mockIssue} | ${issuableTypes.issue} | ${'issue-block'}
- ${mockEpic} | ${issuableTypes.epic} | ${'entity-blocked'}
+ mockIssuable | issuableType | expectedIcon
+ ${mockIssue} | ${TYPE_ISSUE} | ${'issue-block'}
+ ${mockEpic} | ${issuableTypes.epic} | ${'entity-blocked'}
`(
'should render blocked icon for $issuableType',
({ mockIssuable, issuableType, expectedIcon }) => {
@@ -152,9 +153,9 @@ describe('IssuableBlockedIcon', () => {
describe('on mouseenter on blocked icon', () => {
it.each`
- item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy
- ${mockBlockedIssue1} | ${issuableTypes.issue} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)}
- ${mockBlockedEpic1} | ${issuableTypes.epic} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)}
+ item | issuableType | mockBlockingIssuable | issuableItem | blockingIssuablesSpy
+ ${mockBlockedIssue1} | ${TYPE_ISSUE} | ${mockBlockingIssue1} | ${mockIssue} | ${jest.fn().mockResolvedValue(mockBlockingIssuablesResponse1)}
+ ${mockBlockedEpic1} | ${issuableTypes.epic} | ${mockBlockingEpic1} | ${mockEpic} | ${jest.fn().mockResolvedValue(mockBlockingEpicIssuablesResponse1)}
`(
'should query for blocking issuables and render the result for $issuableType',
async ({ item, issuableType, issuableItem, mockBlockingIssuable, blockingIssuablesSpy }) => {
diff --git a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
index 43ff68e30b5..221da35de3d 100644
--- a/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
+++ b/spec/frontend/vue_shared/security_reports/security_reports_app_spec.js
@@ -16,6 +16,7 @@ import {
} from 'jest/vue_shared/security_reports/mock_data';
import { createAlert } from '~/flash';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_INTERNAL_SERVER_ERROR, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import HelpIcon from '~/vue_shared/security_reports/components/help_icon.vue';
import SecurityReportDownloadDropdown from '~/vue_shared/security_reports/components/security_report_download_dropdown.vue';
import {
@@ -187,7 +188,7 @@ describe('Security reports app', () => {
describe('when loading', () => {
beforeEach(() => {
mock = new MockAdapter(axios, { delayResponse: 1 });
- mock.onGet(path).replyOnce(200, successResponse);
+ mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse);
createComponentWithFlagEnabled({
propsData: {
@@ -209,7 +210,7 @@ describe('Security reports app', () => {
describe('when successfully loaded', () => {
beforeEach(() => {
- mock.onGet(path).replyOnce(200, successResponse);
+ mock.onGet(path).replyOnce(HTTP_STATUS_OK, successResponse);
createComponentWithFlagEnabled({
propsData: {
@@ -231,7 +232,7 @@ describe('Security reports app', () => {
describe('when an error occurs', () => {
beforeEach(() => {
- mock.onGet(path).replyOnce(500);
+ mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponentWithFlagEnabled({
propsData: {
@@ -253,7 +254,7 @@ describe('Security reports app', () => {
describe('when the comparison endpoint is not provided', () => {
beforeEach(() => {
- mock.onGet(path).replyOnce(500);
+ mock.onGet(path).replyOnce(HTTP_STATUS_INTERNAL_SERVER_ERROR);
createComponentWithFlagEnabled();
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
index 46bfd7eceb1..0cab950cb77 100644
--- a/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
+++ b/spec/frontend/vue_shared/security_reports/store/modules/sast/actions_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/security_reports/store/modules/sast/actions';
import * as types from '~/vue_shared/security_reports/store/modules/sast/mutation_types';
import createState from '~/vue_shared/security_reports/store/modules/sast/state';
@@ -99,9 +100,9 @@ describe('sast report actions', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
- .replyOnce(200, reports.diff)
+ .replyOnce(HTTP_STATUS_OK, reports.diff)
.onGet(vulnerabilityFeedbackPath)
- .replyOnce(200, reports.enrichData);
+ .replyOnce(HTTP_STATUS_OK, reports.enrichData);
});
it('should dispatch the `receiveDiffSuccess` action', () => {
@@ -128,7 +129,7 @@ describe('sast report actions', () => {
describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => {
beforeEach(() => {
rootState.canReadVulnerabilityFeedback = false;
- mock.onGet(diffEndpoint).replyOnce(200, reports.diff);
+ mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff);
});
it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => {
@@ -157,9 +158,9 @@ describe('sast report actions', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
- .replyOnce(200, reports.diff)
+ .replyOnce(HTTP_STATUS_OK, reports.diff)
.onGet(vulnerabilityFeedbackPath)
- .replyOnce(404);
+ .replyOnce(HTTP_STATUS_NOT_FOUND);
});
it('should dispatch the `receiveError` action', () => {
@@ -177,9 +178,9 @@ describe('sast report actions', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
- .replyOnce(404)
+ .replyOnce(HTTP_STATUS_NOT_FOUND)
.onGet(vulnerabilityFeedbackPath)
- .replyOnce(200, reports.enrichData);
+ .replyOnce(HTTP_STATUS_OK, reports.enrichData);
});
it('should dispatch the `receiveDiffError` action', () => {
diff --git a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
index 4f4f653bb72..7197784c3e8 100644
--- a/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
+++ b/spec/frontend/vue_shared/security_reports/store/modules/secret_detection/actions_spec.js
@@ -2,6 +2,7 @@ import MockAdapter from 'axios-mock-adapter';
import testAction from 'helpers/vuex_action_helper';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_NOT_FOUND, HTTP_STATUS_OK } from '~/lib/utils/http_status';
import * as actions from '~/vue_shared/security_reports/store/modules/secret_detection/actions';
import * as types from '~/vue_shared/security_reports/store/modules/secret_detection/mutation_types';
import createState from '~/vue_shared/security_reports/store/modules/secret_detection/state';
@@ -99,9 +100,9 @@ describe('secret detection report actions', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
- .replyOnce(200, reports.diff)
+ .replyOnce(HTTP_STATUS_OK, reports.diff)
.onGet(vulnerabilityFeedbackPath)
- .replyOnce(200, reports.enrichData);
+ .replyOnce(HTTP_STATUS_OK, reports.enrichData);
});
it('should dispatch the `receiveDiffSuccess` action', () => {
@@ -129,7 +130,7 @@ describe('secret detection report actions', () => {
describe('when diff endpoint responds successfully and fetching vulnerability feedback is not authorized', () => {
beforeEach(() => {
rootState.canReadVulnerabilityFeedback = false;
- mock.onGet(diffEndpoint).replyOnce(200, reports.diff);
+ mock.onGet(diffEndpoint).replyOnce(HTTP_STATUS_OK, reports.diff);
});
it('should dispatch the `receiveDiffSuccess` action with empty enrich data', () => {
@@ -158,9 +159,9 @@ describe('secret detection report actions', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
- .replyOnce(200, reports.diff)
+ .replyOnce(HTTP_STATUS_OK, reports.diff)
.onGet(vulnerabilityFeedbackPath)
- .replyOnce(404);
+ .replyOnce(HTTP_STATUS_NOT_FOUND);
});
it('should dispatch the `receiveDiffError` action', () => {
@@ -178,9 +179,9 @@ describe('secret detection report actions', () => {
beforeEach(() => {
mock
.onGet(diffEndpoint)
- .replyOnce(404)
+ .replyOnce(HTTP_STATUS_NOT_FOUND)
.onGet(vulnerabilityFeedbackPath)
- .replyOnce(200, reports.enrichData);
+ .replyOnce(HTTP_STATUS_OK, reports.enrichData);
});
it('should dispatch the `receiveDiffError` action', () => {
diff --git a/spec/frontend/vue_shared/security_reports/store/utils_spec.js b/spec/frontend/vue_shared/security_reports/store/utils_spec.js
new file mode 100644
index 00000000000..c8750cd58a0
--- /dev/null
+++ b/spec/frontend/vue_shared/security_reports/store/utils_spec.js
@@ -0,0 +1,63 @@
+import { enrichVulnerabilityWithFeedback } from '~/vue_shared/security_reports/store/utils';
+import {
+ FEEDBACK_TYPE_DISMISSAL,
+ FEEDBACK_TYPE_ISSUE,
+ FEEDBACK_TYPE_MERGE_REQUEST,
+} from '~/vue_shared/security_reports/constants';
+
+describe('security reports store utils', () => {
+ const vulnerability = { uuid: 1 };
+
+ describe('enrichVulnerabilityWithFeedback', () => {
+ const dismissalFeedback = {
+ feedback_type: FEEDBACK_TYPE_DISMISSAL,
+ finding_uuid: vulnerability.uuid,
+ };
+ const dismissalVuln = { ...vulnerability, isDismissed: true, dismissalFeedback };
+
+ const issueFeedback = {
+ feedback_type: FEEDBACK_TYPE_ISSUE,
+ issue_iid: 1,
+ finding_uuid: vulnerability.uuid,
+ };
+ const issueVuln = { ...vulnerability, hasIssue: true, issue_feedback: issueFeedback };
+ const mrFeedback = {
+ feedback_type: FEEDBACK_TYPE_MERGE_REQUEST,
+ merge_request_iid: 1,
+ finding_uuid: vulnerability.uuid,
+ };
+ const mrVuln = {
+ ...vulnerability,
+ hasMergeRequest: true,
+ merge_request_feedback: mrFeedback,
+ };
+
+ it.each`
+ feedbacks | expected
+ ${[dismissalFeedback]} | ${dismissalVuln}
+ ${[{ ...issueFeedback, issue_iid: null }]} | ${vulnerability}
+ ${[issueFeedback]} | ${issueVuln}
+ ${[{ ...mrFeedback, merge_request_iid: null }]} | ${vulnerability}
+ ${[mrFeedback]} | ${mrVuln}
+ ${[dismissalFeedback, issueFeedback, mrFeedback]} | ${{ ...dismissalVuln, ...issueVuln, ...mrVuln }}
+ `('returns expected enriched vulnerability: $expected', ({ feedbacks, expected }) => {
+ const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks);
+
+ expect(enrichedVulnerability).toEqual(expected);
+ });
+
+ it('matches correct feedback objects to vulnerability', () => {
+ const feedbacks = [
+ dismissalFeedback,
+ issueFeedback,
+ mrFeedback,
+ { ...dismissalFeedback, finding_uuid: 2 },
+ { ...issueFeedback, finding_uuid: 2 },
+ { ...mrFeedback, finding_uuid: 2 },
+ ];
+ const enrichedVulnerability = enrichVulnerabilityWithFeedback(vulnerability, feedbacks);
+
+ expect(enrichedVulnerability).toEqual({ ...dismissalVuln, ...issueVuln, ...mrVuln });
+ });
+ });
+});
diff --git a/spec/frontend/webhooks/components/test_dropdown_spec.js b/spec/frontend/webhooks/components/test_dropdown_spec.js
new file mode 100644
index 00000000000..2f62ca13469
--- /dev/null
+++ b/spec/frontend/webhooks/components/test_dropdown_spec.js
@@ -0,0 +1,63 @@
+import { GlDisclosureDropdown } from '@gitlab/ui';
+import { mount } from '@vue/test-utils';
+import { getByRole } from '@testing-library/dom';
+import HookTestDropdown from '~/webhooks/components/test_dropdown.vue';
+
+const mockItems = [
+ {
+ text: 'Foo',
+ href: '#foo',
+ },
+];
+
+describe('HookTestDropdown', () => {
+ let wrapper;
+
+ const findDisclosure = () => wrapper.findComponent(GlDisclosureDropdown);
+ const clickItem = (itemText) => {
+ const item = getByRole(wrapper.element, 'button', { name: itemText });
+ item.dispatchEvent(new MouseEvent('click'));
+ };
+
+ const createComponent = (props) => {
+ wrapper = mount(HookTestDropdown, {
+ propsData: {
+ items: mockItems,
+ ...props,
+ },
+ });
+ };
+
+ it('passes the expected props to GlDisclosureDropdown', () => {
+ const size = 'small';
+ createComponent({ size });
+
+ expect(findDisclosure().props()).toMatchObject({
+ items: mockItems.map((item) => ({
+ text: item.text,
+ })),
+ size,
+ });
+ });
+
+ describe('clicking on an item', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('triggers @rails/ujs data-method=post handling', () => {
+ const railsEventPromise = new Promise((resolve) => {
+ document.addEventListener('click', ({ target }) => {
+ expect(target.tagName).toBe('A');
+ expect(target.dataset.method).toBe('post');
+ expect(target.getAttribute('href')).toBe(mockItems[0].href);
+ resolve();
+ });
+ });
+
+ clickItem(mockItems[0].text);
+
+ return railsEventPromise;
+ });
+ });
+});
diff --git a/spec/frontend/whats_new/components/app_spec.js b/spec/frontend/whats_new/components/app_spec.js
index da95b51c0b1..ee15034daff 100644
--- a/spec/frontend/whats_new/components/app_spec.js
+++ b/spec/frontend/whats_new/components/app_spec.js
@@ -91,6 +91,7 @@ describe('App', () => {
expect(actions.openDrawer).toHaveBeenCalledWith(expect.any(Object), 'version-digest');
expect(trackingSpy).toHaveBeenCalledWith(undefined, 'click_whats_new_drawer', {
label: 'namespace_id',
+ property: 'navigation_top',
value: 'namespace-840',
});
});
diff --git a/spec/frontend/whats_new/store/actions_spec.js b/spec/frontend/whats_new/store/actions_spec.js
index c9614c7330b..5f5e4e53be2 100644
--- a/spec/frontend/whats_new/store/actions_spec.js
+++ b/spec/frontend/whats_new/store/actions_spec.js
@@ -3,6 +3,7 @@ import { useLocalStorageSpy } from 'helpers/local_storage_helper';
import testAction from 'helpers/vuex_action_helper';
import waitForPromises from 'helpers/wait_for_promises';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import actions from '~/whats_new/store/actions';
import * as types from '~/whats_new/store/mutation_types';
@@ -33,7 +34,7 @@ describe('whats new actions', () => {
axiosMock = new MockAdapter(axios);
axiosMock
.onGet('/-/whats_new')
- .replyOnce(200, [{ title: 'Whats New Drawer', url: 'www.url.com' }], {
+ .replyOnce(HTTP_STATUS_OK, [{ title: 'Whats New Drawer', url: 'www.url.com' }], {
'x-next-page': '2',
});
@@ -49,7 +50,7 @@ describe('whats new actions', () => {
axiosMock
.onGet('/-/whats_new', { params: { page: undefined, v: undefined } })
- .replyOnce(200, [{ title: 'GitLab Stories' }]);
+ .replyOnce(HTTP_STATUS_OK, [{ title: 'GitLab Stories' }]);
testAction(
actions.fetchItems,
@@ -66,7 +67,7 @@ describe('whats new actions', () => {
axiosMock
.onGet('/-/whats_new', { params: { page: 8, v: 42 } })
- .replyOnce(200, [{ title: 'GitLab Stories' }]);
+ .replyOnce(HTTP_STATUS_OK, [{ title: 'GitLab Stories' }]);
testAction(
actions.fetchItems,
diff --git a/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
new file mode 100644
index 00000000000..5901642b8a1
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/__snapshots__/work_item_note_replying_spec.js.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Work Item Note Replying should have the note body and header 1`] = `"<note-header-stub author=\\"[object Object]\\" actiontext=\\"\\" noteabletype=\\"\\" expanded=\\"true\\" showspinner=\\"true\\"></note-header-stub>"`;
diff --git a/spec/frontend/work_items/components/notes/system_note_spec.js b/spec/frontend/work_items/components/notes/system_note_spec.js
index 3e3b8bf65b2..fd5f373d076 100644
--- a/spec/frontend/work_items/components/notes/system_note_spec.js
+++ b/spec/frontend/work_items/components/notes/system_note_spec.js
@@ -6,6 +6,7 @@ import { renderGFM } from '~/behaviors/markdown/render_gfm';
import WorkItemSystemNote from '~/work_items/components/notes/system_note.vue';
import NoteHeader from '~/notes/components/note_header.vue';
import axios from '~/lib/utils/axios_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
jest.mock('~/behaviors/markdown/render_gfm');
@@ -95,7 +96,7 @@ describe('system note component', () => {
it.skip('renders outdated code lines', async () => {
mock
.onGet('/outdated_line_change_path')
- .reply(200, [
+ .reply(HTTP_STATUS_OK, [
{ rich_text: 'console.log', type: 'new', line_code: '123', old_line: null, new_line: 1 },
]);
diff --git a/spec/frontend/work_items/components/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
index 07c00119398..2a65e91a906 100644
--- a/spec/frontend/work_items/components/work_item_comment_form_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_add_note_spec.js
@@ -5,21 +5,23 @@ 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 { clearDraft } from '~/lib/utils/autosave';
+import { config } from '~/graphql_shared/issuable_client';
+import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
+import WorkItemCommentLocked from '~/work_items/components/notes/work_item_comment_locked.vue';
+import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
+import createNoteMutation from '~/work_items/graphql/notes/create_work_item_note.mutation.graphql';
import { TRACKING_CATEGORY_SHOW } from '~/work_items/constants';
import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
workItemResponseFactory,
workItemQueryResponse,
projectWorkItemResponse,
createWorkItemNoteResponse,
-} from '../mock_data';
+ mockWorkItemNotesResponse,
+} from '../../mock_data';
jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal');
jest.mock('~/lib/utils/autosave');
@@ -35,18 +37,7 @@ describe('WorkItemCommentForm', () => {
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 findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
const createComponent = async ({
mutationHandler = mutationSuccessHandler,
@@ -56,6 +47,7 @@ describe('WorkItemCommentForm', () => {
fetchByIid = false,
signedIn = true,
isEditing = true,
+ workItemType = 'Task',
} = {}) => {
workItemResponseHandler = jest.fn().mockResolvedValue(workItemResponse);
@@ -64,21 +56,36 @@ describe('WorkItemCommentForm', () => {
window.gon.current_user_avatar_url = 'avatar.png';
}
- const { id } = workItemQueryResponse.data.workItem;
- wrapper = shallowMount(WorkItemCommentForm, {
- apolloProvider: createMockApollo([
+ const apolloProvider = createMockApollo(
+ [
[workItemQuery, workItemResponseHandler],
[createNoteMutation, mutationHandler],
[workItemByIidQuery, workItemByIidResponseHandler],
- ]),
+ ],
+ {},
+ { ...config.cacheConfig },
+ );
+
+ apolloProvider.clients.defaultClient.writeQuery({
+ query: workItemNotesQuery,
+ variables: {
+ id: workItemId,
+ pageSize: 100,
+ },
+ data: mockWorkItemNotesResponse.data,
+ });
+
+ const { id } = workItemQueryResponse.data.workItem;
+ wrapper = shallowMount(WorkItemAddNote, {
+ apolloProvider,
propsData: {
workItemId: id,
fullPath: 'test-project-path',
queryVariables,
fetchByIid,
+ workItemType,
},
stubs: {
- MarkdownField,
WorkItemCommentLocked,
},
});
@@ -99,9 +106,7 @@ describe('WorkItemCommentForm', () => {
signedIn: true,
});
- setText(noteText);
-
- clickSave();
+ findCommentForm().vm.$emit('submitForm', noteText);
await waitForPromises();
@@ -109,6 +114,7 @@ describe('WorkItemCommentForm', () => {
input: {
noteableId: workItemId,
body: noteText,
+ discussionId: null,
},
});
});
@@ -117,9 +123,7 @@ describe('WorkItemCommentForm', () => {
await createComponent();
const trackingSpy = mockTracking(undefined, wrapper.element, jest.spyOn);
- setText('test');
-
- clickSave();
+ findCommentForm().vm.$emit('submitForm', 'test');
await waitForPromises();
@@ -130,6 +134,33 @@ describe('WorkItemCommentForm', () => {
});
});
+ it('emits `replied` event and hides form after successful mutation', async () => {
+ await createComponent({
+ isEditing: true,
+ signedIn: true,
+ queryVariables: {
+ id: mockWorkItemNotesResponse.data.workItem.id,
+ },
+ });
+
+ findCommentForm().vm.$emit('submitForm', 'some text');
+ await waitForPromises();
+
+ expect(wrapper.emitted('replied')).toEqual([[]]);
+ });
+
+ it('clears a draft after successful mutation', async () => {
+ await createComponent({
+ isEditing: true,
+ signedIn: true,
+ });
+
+ findCommentForm().vm.$emit('submitForm', 'some text');
+ await waitForPromises();
+
+ expect(clearDraft).toHaveBeenCalledWith('gid://gitlab/WorkItem/1-comment');
+ });
+
it('emits error when mutation returns error', async () => {
const error = 'eror';
@@ -138,16 +169,26 @@ describe('WorkItemCommentForm', () => {
mutationHandler: jest.fn().mockResolvedValue({
data: {
createNote: {
- note: null,
+ note: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ discussion: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ notes: {
+ nodes: [],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ __typename: 'Note',
+ },
+ __typename: 'CreateNotePayload',
errors: [error],
},
},
}),
});
- setText('updated desc');
-
- clickSave();
+ findCommentForm().vm.$emit('submitForm', 'updated desc');
await waitForPromises();
@@ -162,24 +203,12 @@ describe('WorkItemCommentForm', () => {
mutationHandler: jest.fn().mockRejectedValue(new Error(error)),
});
- setText('updated desc');
-
- clickSave();
+ findCommentForm().vm.$emit('submitForm', 'updated desc');
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 () => {
diff --git a/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
new file mode 100644
index 00000000000..23a9f285804
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_comment_form_spec.js
@@ -0,0 +1,164 @@
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import waitForPromises from 'helpers/wait_for_promises';
+import * as autosave from '~/lib/utils/autosave';
+import { ESC_KEY, ENTER_KEY } from '~/lib/utils/keys';
+import * as confirmViaGlModal from '~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal';
+import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
+import MarkdownEditor from '~/vue_shared/components/markdown/markdown_editor.vue';
+
+const draftComment = 'draft comment';
+
+jest.mock('~/lib/utils/autosave', () => ({
+ updateDraft: jest.fn(),
+ clearDraft: jest.fn(),
+ getDraft: jest.fn().mockReturnValue(draftComment),
+}));
+jest.mock('~/lib/utils/confirm_via_gl_modal/confirm_via_gl_modal', () => ({
+ confirmAction: jest.fn().mockResolvedValue(true),
+}));
+
+describe('Work item comment form component', () => {
+ let wrapper;
+
+ const mockAutosaveKey = 'test-auto-save-key';
+
+ const findMarkdownEditor = () => wrapper.findComponent(MarkdownEditor);
+ const findCancelButton = () => wrapper.find('[data-testid="cancel-button"]');
+ const findConfirmButton = () => wrapper.find('[data-testid="confirm-button"]');
+
+ const createComponent = ({ isSubmitting = false, initialValue = '' } = {}) => {
+ wrapper = shallowMount(WorkItemCommentForm, {
+ propsData: {
+ workItemType: 'Issue',
+ ariaLabel: 'test-aria-label',
+ autosaveKey: mockAutosaveKey,
+ isSubmitting,
+ initialValue,
+ },
+ provide: {
+ fullPath: 'test-project-path',
+ },
+ });
+ };
+
+ it('passes correct markdown preview path to markdown editor', () => {
+ createComponent();
+
+ expect(findMarkdownEditor().props('renderMarkdownPath')).toBe(
+ '/test-project-path/preview_markdown?target_type=Issue',
+ );
+ });
+
+ it('passes correct form field props to markdown editor', () => {
+ createComponent();
+
+ expect(findMarkdownEditor().props('formFieldProps')).toEqual({
+ 'aria-label': 'test-aria-label',
+ id: 'work-item-add-or-edit-comment',
+ name: 'work-item-add-or-edit-comment',
+ placeholder: 'Write a comment or drag your files here…',
+ });
+ });
+
+ it('passes correct `loading` prop to confirm button', () => {
+ createComponent({ isSubmitting: true });
+
+ expect(findConfirmButton().props('loading')).toBe(true);
+ });
+
+ it('passes a draft from local storage as a value to markdown editor if the draft exists', () => {
+ createComponent({ initialValue: 'parent comment' });
+ expect(findMarkdownEditor().props('value')).toBe(draftComment);
+ });
+
+ it('passes an initialValue prop as a value to markdown editor if storage draft does not exist', () => {
+ jest.spyOn(autosave, 'getDraft').mockImplementation(() => '');
+ createComponent({ initialValue: 'parent comment' });
+
+ expect(findMarkdownEditor().props('value')).toBe('parent comment');
+ });
+
+ it('passes an empty string as a value to markdown editor if storage draft and initialValue are empty', () => {
+ createComponent();
+
+ expect(findMarkdownEditor().props('value')).toBe('');
+ });
+
+ describe('on markdown editor input', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('sets correct comment text value', async () => {
+ expect(findMarkdownEditor().props('value')).toBe('');
+
+ findMarkdownEditor().vm.$emit('input', 'new comment');
+ await nextTick();
+
+ expect(findMarkdownEditor().props('value')).toBe('new comment');
+ });
+
+ it('calls `updateDraft` with correct parameters', async () => {
+ findMarkdownEditor().vm.$emit('input', 'new comment');
+
+ expect(autosave.updateDraft).toHaveBeenCalledWith(mockAutosaveKey, 'new comment');
+ });
+ });
+
+ describe('on cancel editing', () => {
+ beforeEach(() => {
+ jest.spyOn(autosave, 'getDraft').mockImplementation(() => draftComment);
+ createComponent();
+ findMarkdownEditor().vm.$emit('keydown', new KeyboardEvent('keydown', { key: ESC_KEY }));
+
+ return waitForPromises();
+ });
+
+ it('confirms a user action if comment text is not empty', () => {
+ expect(confirmViaGlModal.confirmAction).toHaveBeenCalled();
+ });
+
+ it('emits `cancelEditing` and clears draft from the local storage', () => {
+ expect(wrapper.emitted('cancelEditing')).toHaveLength(1);
+ expect(autosave.clearDraft).toHaveBeenCalledWith(mockAutosaveKey);
+ });
+ });
+
+ it('cancels editing on clicking cancel button', async () => {
+ createComponent();
+ findCancelButton().vm.$emit('click');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('cancelEditing')).toHaveLength(1);
+ expect(autosave.clearDraft).toHaveBeenCalledWith(mockAutosaveKey);
+ });
+
+ it('emits `submitForm` event on confirm button click', () => {
+ createComponent();
+ findConfirmButton().vm.$emit('click');
+
+ expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
+ });
+
+ it('emits `submitForm` event on pressing enter with meta key on markdown editor', () => {
+ createComponent();
+ findMarkdownEditor().vm.$emit(
+ 'keydown',
+ new KeyboardEvent('keydown', { key: ENTER_KEY, metaKey: true }),
+ );
+
+ expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
+ });
+
+ it('emits `submitForm` event on pressing ctrl+enter on markdown editor', () => {
+ createComponent();
+ findMarkdownEditor().vm.$emit(
+ 'keydown',
+ new KeyboardEvent('keydown', { key: ENTER_KEY, ctrlKey: true }),
+ );
+
+ expect(wrapper.emitted('submitForm')).toEqual([[draftComment]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_comment_locked_spec.js b/spec/frontend/work_items/components/notes/work_item_comment_locked_spec.js
index 58491c4b09c..734b474c8fc 100644
--- a/spec/frontend/work_items/components/work_item_comment_locked_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_comment_locked_spec.js
@@ -1,6 +1,6 @@
import { GlLink, GlIcon } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import WorkItemCommentLocked from '~/work_items/components/work_item_comment_locked.vue';
+import WorkItemCommentLocked from '~/work_items/components/notes/work_item_comment_locked.vue';
const createComponent = ({ workItemType = 'Task', isProjectArchived = false } = {}) =>
shallowMount(WorkItemCommentLocked, {
diff --git a/spec/frontend/work_items/components/notes/work_item_discussion_spec.js b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
new file mode 100644
index 00000000000..bb65b75c4d8
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_discussion_spec.js
@@ -0,0 +1,149 @@
+import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import { nextTick } from 'vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import ToggleRepliesWidget from '~/notes/components/toggle_replies_widget.vue';
+import WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
+import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
+import WorkItemNoteReplying from '~/work_items/components/notes/work_item_note_replying.vue';
+import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.vue';
+import {
+ mockWorkItemCommentNote,
+ mockWorkItemNotesResponseWithComments,
+} from 'jest/work_items/mock_data';
+import { WIDGET_TYPE_NOTES } from '~/work_items/constants';
+
+const mockWorkItemNotesWidgetResponseWithComments = mockWorkItemNotesResponseWithComments.data.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_NOTES,
+);
+
+describe('Work Item Discussion', () => {
+ let wrapper;
+ const mockWorkItemId = 'gid://gitlab/WorkItem/625';
+
+ const findTimelineEntryItem = () => wrapper.findComponent(TimelineEntryItem);
+ const findAvatarLink = () => wrapper.findComponent(GlAvatarLink);
+ const findAvatar = () => wrapper.findComponent(GlAvatar);
+ const findToggleRepliesWidget = () => wrapper.findComponent(ToggleRepliesWidget);
+ const findAllThreads = () => wrapper.findAllComponents(WorkItemNote);
+ const findThreadAtIndex = (index) => findAllThreads().at(index);
+ const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote);
+ const findWorkItemNoteReplying = () => wrapper.findComponent(WorkItemNoteReplying);
+
+ const createComponent = ({
+ discussion = [mockWorkItemCommentNote],
+ workItemId = mockWorkItemId,
+ queryVariables = { id: workItemId },
+ fetchByIid = false,
+ fullPath = 'gitlab-org',
+ workItemType = 'Task',
+ } = {}) => {
+ wrapper = shallowMount(WorkItemDiscussion, {
+ propsData: {
+ discussion,
+ workItemId,
+ queryVariables,
+ fetchByIid,
+ fullPath,
+ workItemType,
+ },
+ });
+ };
+
+ describe('Default', () => {
+ 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('should not show the the toggle replies widget wrapper when no replies', () => {
+ expect(findToggleRepliesWidget().exists()).toBe(false);
+ });
+
+ it('should not show the comment form by default', () => {
+ expect(findWorkItemAddNote().exists()).toBe(false);
+ });
+ });
+
+ describe('When the main comments has threads', () => {
+ beforeEach(() => {
+ createComponent({
+ discussion: mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes,
+ });
+ });
+
+ it('should show the toggle replies widget', () => {
+ expect(findToggleRepliesWidget().exists()).toBe(true);
+ });
+
+ it('the number of threads should be equal to the response length', async () => {
+ findToggleRepliesWidget().vm.$emit('toggle');
+ await nextTick();
+ expect(findAllThreads()).toHaveLength(
+ mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes.length,
+ );
+ });
+
+ it('should autofocus when we click expand replies', async () => {
+ const mainComment = findThreadAtIndex(0);
+
+ mainComment.vm.$emit('startReplying');
+ await nextTick();
+ expect(findWorkItemAddNote().exists()).toBe(true);
+ expect(findWorkItemAddNote().props('autofocus')).toBe(true);
+ });
+ });
+
+ describe('When replying to any comment', () => {
+ beforeEach(async () => {
+ createComponent({
+ discussion: mockWorkItemNotesWidgetResponseWithComments.discussions.nodes[0].notes.nodes,
+ });
+ const mainComment = findThreadAtIndex(0);
+
+ mainComment.vm.$emit('startReplying');
+ await nextTick();
+ await findWorkItemAddNote().vm.$emit('replying', 'reply text');
+ });
+
+ it('should show optimistic behavior when replying', async () => {
+ expect(findAllThreads()).toHaveLength(2);
+ expect(findWorkItemNoteReplying().exists()).toBe(true);
+ });
+
+ it('should be expanded when the reply is successful', async () => {
+ findWorkItemAddNote().vm.$emit('replied');
+ await nextTick();
+ expect(findToggleRepliesWidget().exists()).toBe(true);
+ expect(findToggleRepliesWidget().props('collapsed')).toBe(false);
+ });
+ });
+
+ it('emits `deleteNote` event with correct parameter when child note component emits `deleteNote` event', () => {
+ createComponent();
+ findThreadAtIndex(0).vm.$emit('deleteNote');
+
+ expect(wrapper.emitted('deleteNote')).toEqual([[mockWorkItemCommentNote]]);
+ });
+
+ it('emits `error` event when child note emits an `error`', () => {
+ const mockErrorText = 'Houston, we have a problem';
+
+ createComponent();
+ findThreadAtIndex(0).vm.$emit('error', mockErrorText);
+
+ expect(wrapper.emitted('error')).toEqual([[mockErrorText]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
new file mode 100644
index 00000000000..d85cd46c1c3
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_note_actions_spec.js
@@ -0,0 +1,52 @@
+import { shallowMount } from '@vue/test-utils';
+import ReplyButton from '~/notes/components/note_actions/reply_button.vue';
+import WorkItemNoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
+
+describe('Work Item Note Actions', () => {
+ let wrapper;
+
+ const findReplyButton = () => wrapper.findComponent(ReplyButton);
+ const findEditButton = () => wrapper.find('[data-testid="edit-work-item-note"]');
+
+ const createComponent = ({ showReply = true, showEdit = true } = {}) => {
+ wrapper = shallowMount(WorkItemNoteActions, {
+ propsData: {
+ showReply,
+ showEdit,
+ },
+ });
+ };
+
+ describe('Default', () => {
+ it('Should show the reply button by default', () => {
+ createComponent();
+ expect(findReplyButton().exists()).toBe(true);
+ });
+ });
+
+ describe('When the reply button needs to be hidden', () => {
+ it('Should show the reply button by default', () => {
+ createComponent({ showReply: false });
+ expect(findReplyButton().exists()).toBe(false);
+ });
+ });
+
+ it('shows edit button when `showEdit` prop is true', () => {
+ createComponent();
+
+ expect(findEditButton().exists()).toBe(true);
+ });
+
+ it('does not show edit button when `showEdit` prop is false', () => {
+ createComponent({ showEdit: false });
+
+ expect(findEditButton().exists()).toBe(false);
+ });
+
+ it('emits `startEditing` event when edit button is clicked', () => {
+ createComponent();
+ findEditButton().vm.$emit('click');
+
+ expect(wrapper.emitted('startEditing')).toEqual([[]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_replying_spec.js b/spec/frontend/work_items/components/notes/work_item_note_replying_spec.js
new file mode 100644
index 00000000000..225cc3bacaf
--- /dev/null
+++ b/spec/frontend/work_items/components/notes/work_item_note_replying_spec.js
@@ -0,0 +1,34 @@
+import { shallowMount } from '@vue/test-utils';
+import WorkItemNoteReplying from '~/work_items/components/notes/work_item_note_replying.vue';
+import NoteHeader from '~/notes/components/note_header.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+
+describe('Work Item Note Replying', () => {
+ let wrapper;
+ const mockNoteBody = 'replying body';
+
+ const findTimelineEntry = () => wrapper.findComponent(TimelineEntryItem);
+ const findNoteHeader = () => wrapper.findComponent(NoteHeader);
+
+ const createComponent = ({ body = mockNoteBody } = {}) => {
+ wrapper = shallowMount(WorkItemNoteReplying, {
+ propsData: {
+ body,
+ },
+ });
+
+ window.gon.current_user_id = '1';
+ window.gon.current_user_avatar_url = 'avatar.png';
+ window.gon.current_user_fullname = 'Administrator';
+ window.gon.current_username = 'user';
+ };
+
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should have the note body and header', () => {
+ expect(findTimelineEntry().exists()).toBe(true);
+ expect(findNoteHeader().html()).toMatchSnapshot();
+ });
+});
diff --git a/spec/frontend/work_items/components/notes/work_item_note_spec.js b/spec/frontend/work_items/components/notes/work_item_note_spec.js
index 7257d5c8023..9b87419cee7 100644
--- a/spec/frontend/work_items/components/notes/work_item_note_spec.js
+++ b/spec/frontend/work_items/components/notes/work_item_note_spec.js
@@ -1,53 +1,261 @@
-import { GlAvatarLink, GlAvatar } from '@gitlab/ui';
+import { GlAvatarLink, GlDropdown } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
-import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
+import Vue, { nextTick } from 'vue';
+import VueApollo from 'vue-apollo';
+import mockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import { updateDraft } from '~/lib/utils/autosave';
+import EditedAt from '~/issues/show/components/edited.vue';
import WorkItemNote from '~/work_items/components/notes/work_item_note.vue';
+import TimelineEntryItem from '~/vue_shared/components/notes/timeline_entry_item.vue';
import NoteBody from '~/work_items/components/notes/work_item_note_body.vue';
import NoteHeader from '~/notes/components/note_header.vue';
+import NoteActions from '~/work_items/components/notes/work_item_note_actions.vue';
+import WorkItemCommentForm from '~/work_items/components/notes/work_item_comment_form.vue';
+import updateWorkItemNoteMutation from '~/work_items/graphql/notes/update_work_item_note.mutation.graphql';
import { mockWorkItemCommentNote } from 'jest/work_items/mock_data';
+Vue.use(VueApollo);
+jest.mock('~/lib/utils/autosave');
+
describe('Work Item Note', () => {
let wrapper;
+ const updatedNoteText = '# Some title';
+ const updatedNoteBody = '<h1 data-sourcepos="1:1-1:12" dir="auto">Some title</h1>';
+
+ const successHandler = jest.fn().mockResolvedValue({
+ data: {
+ updateNote: {
+ errors: [],
+ note: {
+ ...mockWorkItemCommentNote,
+ body: updatedNoteText,
+ bodyHtml: updatedNoteBody,
+ },
+ },
+ },
+ });
+ const errorHandler = jest.fn().mockRejectedValue('Oops');
+ const findAuthorAvatarLink = () => wrapper.findComponent(GlAvatarLink);
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 findNoteActions = () => wrapper.findComponent(NoteActions);
+ const findDropdown = () => wrapper.findComponent(GlDropdown);
+ const findCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
+ const findEditedAt = () => wrapper.findComponent(EditedAt);
- const createComponent = ({ note = mockWorkItemCommentNote } = {}) => {
+ const findDeleteNoteButton = () => wrapper.find('[data-testid="delete-note-action"]');
+ const findNoteWrapper = () => wrapper.find('[data-testid="note-wrapper"]');
+
+ const createComponent = ({
+ note = mockWorkItemCommentNote,
+ isFirstNote = false,
+ updateNoteMutationHandler = successHandler,
+ } = {}) => {
wrapper = shallowMount(WorkItemNote, {
propsData: {
note,
+ isFirstNote,
+ workItemType: 'Task',
},
+ apolloProvider: mockApollo([[updateWorkItemNoteMutation, updateNoteMutationHandler]]),
});
};
- beforeEach(() => {
- createComponent();
- });
+ describe('when editing', () => {
+ beforeEach(() => {
+ createComponent();
+ findNoteActions().vm.$emit('startEditing');
+ return nextTick();
+ });
- it('Should be wrapped inside the timeline entry item', () => {
- expect(findTimelineEntryItem().exists()).toBe(true);
- });
+ it('should render a comment form', () => {
+ expect(findCommentForm().exists()).toBe(true);
+ });
+
+ it('should not render note wrapper', () => {
+ expect(findNoteWrapper().exists()).toBe(false);
+ });
+
+ it('updates saved draft with current note text', () => {
+ expect(updateDraft).toHaveBeenCalledWith(
+ `${mockWorkItemCommentNote.id}-comment`,
+ mockWorkItemCommentNote.body,
+ );
+ });
- it('should have the author avatar of the work item note', () => {
- expect(findAvatarLink().exists()).toBe(true);
- expect(findAvatarLink().attributes('href')).toBe(mockWorkItemCommentNote.author.webUrl);
+ it('passes correct autosave key prop to comment form component', () => {
+ expect(findCommentForm().props('autosaveKey')).toBe(`${mockWorkItemCommentNote.id}-comment`);
+ });
+
+ it('should hide a form and show wrapper when user cancels editing', async () => {
+ findCommentForm().vm.$emit('cancelEditing');
+ await nextTick();
- expect(findAvatar().exists()).toBe(true);
- expect(findAvatar().props('src')).toBe(mockWorkItemCommentNote.author.avatarUrl);
- expect(findAvatar().props('entityName')).toBe(mockWorkItemCommentNote.author.username);
+ expect(findCommentForm().exists()).toBe(false);
+ expect(findNoteWrapper().exists()).toBe(true);
+ });
});
- it('has note header', () => {
- expect(findNoteHeader().exists()).toBe(true);
- expect(findNoteHeader().props('author')).toEqual(mockWorkItemCommentNote.author);
- expect(findNoteHeader().props('createdAt')).toBe(mockWorkItemCommentNote.createdAt);
+ describe('when submitting a form to edit a note', () => {
+ it('calls update mutation with correct variables', async () => {
+ createComponent();
+ findNoteActions().vm.$emit('startEditing');
+ await nextTick();
+
+ findCommentForm().vm.$emit('submitForm', updatedNoteText);
+
+ expect(successHandler).toHaveBeenCalledWith({
+ input: {
+ id: mockWorkItemCommentNote.id,
+ body: updatedNoteText,
+ },
+ });
+ });
+
+ it('hides the form after succesful mutation', async () => {
+ createComponent();
+ findNoteActions().vm.$emit('startEditing');
+ await nextTick();
+
+ findCommentForm().vm.$emit('submitForm', updatedNoteText);
+ await waitForPromises();
+
+ expect(findCommentForm().exists()).toBe(false);
+ });
+
+ describe('when mutation fails', () => {
+ beforeEach(async () => {
+ createComponent({ updateNoteMutationHandler: errorHandler });
+ findNoteActions().vm.$emit('startEditing');
+ await nextTick();
+
+ findCommentForm().vm.$emit('submitForm', updatedNoteText);
+ await waitForPromises();
+ });
+
+ it('opens the form again', () => {
+ expect(findCommentForm().exists()).toBe(true);
+ });
+
+ it('updates the saved draft with the latest comment text', () => {
+ expect(updateDraft).toHaveBeenCalledWith(
+ `${mockWorkItemCommentNote.id}-comment`,
+ updatedNoteText,
+ );
+ });
+
+ it('emits an error', () => {
+ expect(wrapper.emitted('error')).toHaveLength(1);
+ });
+ });
});
- it('has note body', () => {
- expect(findNoteBody().exists()).toBe(true);
- expect(findNoteBody().props('note')).toEqual(mockWorkItemCommentNote);
+ describe('when not editing', () => {
+ it('should not render a comment form', () => {
+ createComponent();
+ expect(findCommentForm().exists()).toBe(false);
+ });
+
+ it('should render note wrapper', () => {
+ createComponent();
+ expect(findNoteWrapper().exists()).toBe(true);
+ });
+
+ it('renders no "edited at" information by default', () => {
+ createComponent();
+ expect(findEditedAt().exists()).toBe(false);
+ });
+
+ it('renders "edited at" information if the note was edited', () => {
+ createComponent({
+ note: {
+ ...mockWorkItemCommentNote,
+ lastEditedAt: '2023-02-12T07:47:40Z',
+ lastEditedBy: { ...mockWorkItemCommentNote.author, webPath: 'test-path' },
+ },
+ });
+
+ expect(findEditedAt().exists()).toBe(true);
+ expect(findEditedAt().props()).toEqual({
+ updatedAt: '2023-02-12T07:47:40Z',
+ updatedByName: 'Administrator',
+ updatedByPath: 'test-path',
+ });
+ });
+
+ describe('main comment', () => {
+ beforeEach(() => {
+ createComponent({ isFirstNote: true });
+ });
+
+ it('should have the note header, actions and body', () => {
+ expect(findTimelineEntryItem().exists()).toBe(true);
+ expect(findNoteHeader().exists()).toBe(true);
+ expect(findNoteBody().exists()).toBe(true);
+ expect(findNoteActions().exists()).toBe(true);
+ });
+
+ it('should not have the Avatar link for main thread inside the timeline-entry', () => {
+ expect(findAuthorAvatarLink().exists()).toBe(false);
+ });
+
+ it('should have the reply button props', () => {
+ expect(findNoteActions().props('showReply')).toBe(true);
+ });
+ });
+
+ describe('comment threads', () => {
+ beforeEach(() => {
+ createComponent();
+ });
+
+ it('should have the note header, actions and body', () => {
+ expect(findTimelineEntryItem().exists()).toBe(true);
+ expect(findNoteHeader().exists()).toBe(true);
+ expect(findNoteBody().exists()).toBe(true);
+ expect(findNoteActions().exists()).toBe(true);
+ });
+
+ it('should have the Avatar link for comment threads', () => {
+ expect(findAuthorAvatarLink().exists()).toBe(true);
+ });
+
+ it('should not have the reply button props', () => {
+ expect(findNoteActions().props('showReply')).toBe(false);
+ });
+ });
+
+ it('should display a dropdown if user has a permission to delete a note', () => {
+ createComponent({
+ note: {
+ ...mockWorkItemCommentNote,
+ userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
+ },
+ });
+
+ expect(findDropdown().exists()).toBe(true);
+ });
+
+ it('should not display a dropdown if user has no permission to delete a note', () => {
+ createComponent();
+
+ expect(findDropdown().exists()).toBe(false);
+ });
+
+ it('should emit `deleteNote` event when delete note action is clicked', () => {
+ createComponent({
+ note: {
+ ...mockWorkItemCommentNote,
+ userPermissions: { ...mockWorkItemCommentNote.userPermissions, adminNote: true },
+ },
+ });
+
+ findDeleteNoteButton().vm.$emit('click');
+
+ expect(wrapper.emitted('deleteNote')).toEqual([[]]);
+ });
});
});
diff --git a/spec/frontend/work_items/components/widget_wrapper_spec.js b/spec/frontend/work_items/components/widget_wrapper_spec.js
new file mode 100644
index 00000000000..a87233300fc
--- /dev/null
+++ b/spec/frontend/work_items/components/widget_wrapper_spec.js
@@ -0,0 +1,46 @@
+import { nextTick } from 'vue';
+import { GlAlert, GlButton } from '@gitlab/ui';
+import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
+import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
+
+describe('WidgetWrapper component', () => {
+ let wrapper;
+
+ const createComponent = ({ error } = {}) => {
+ wrapper = shallowMountExtended(WidgetWrapper, { propsData: { error } });
+ };
+
+ const findAlert = () => wrapper.findComponent(GlAlert);
+ const findToggleButton = () => wrapper.findComponent(GlButton);
+ const findWidgetBody = () => wrapper.findByTestId('widget-body');
+
+ it('is expanded by default', () => {
+ createComponent();
+
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
+ expect(findWidgetBody().exists()).toBe(true);
+ });
+
+ it('collapses on click toggle button', async () => {
+ createComponent();
+ findToggleButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
+ expect(findWidgetBody().exists()).toBe(false);
+ });
+
+ it('shows alert when list loading fails', () => {
+ const error = 'Some error';
+ createComponent({ error });
+
+ expect(findAlert().text()).toBe(error);
+ });
+
+ it('emits event when dismissing the alert', () => {
+ createComponent({ error: 'error' });
+ findAlert().vm.$emit('dismiss');
+
+ expect(wrapper.emitted('dismissAlert')).toEqual([[]]);
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_created_updated_spec.js b/spec/frontend/work_items/components/work_item_created_updated_spec.js
new file mode 100644
index 00000000000..fe31c01df36
--- /dev/null
+++ b/spec/frontend/work_items/components/work_item_created_updated_spec.js
@@ -0,0 +1,104 @@
+import { GlAvatarLink, GlSprintf } from '@gitlab/ui';
+import { shallowMount } from '@vue/test-utils';
+import Vue from 'vue';
+import VueApollo from 'vue-apollo';
+import createMockApollo from 'helpers/mock_apollo_helper';
+import waitForPromises from 'helpers/wait_for_promises';
+import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
+import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
+import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import { workItemResponseFactory, mockAssignees } from '../mock_data';
+
+describe('WorkItemCreatedUpdated component', () => {
+ let wrapper;
+ let successHandler;
+ let successByIidHandler;
+
+ Vue.use(VueApollo);
+
+ const findCreatedAt = () => wrapper.find('[data-testid="work-item-created"]');
+ const findUpdatedAt = () => wrapper.find('[data-testid="work-item-updated"]');
+
+ const findCreatedAtText = () => findCreatedAt().text().replace(/\s+/g, ' ');
+
+ const createComponent = async ({
+ workItemId = 'gid://gitlab/WorkItem/1',
+ workItemIid = '1',
+ fetchByIid = false,
+ author = null,
+ updatedAt,
+ } = {}) => {
+ const workItemQueryResponse = workItemResponseFactory({
+ author,
+ updatedAt,
+ });
+ const byIidResponse = {
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ workItems: {
+ nodes: [workItemQueryResponse.data.workItem],
+ },
+ },
+ },
+ };
+
+ successHandler = jest.fn().mockResolvedValue(workItemQueryResponse);
+ successByIidHandler = jest.fn().mockResolvedValue(byIidResponse);
+
+ const handlers = [
+ [workItemQuery, successHandler],
+ [workItemByIidQuery, successByIidHandler],
+ ];
+
+ wrapper = shallowMount(WorkItemCreatedUpdated, {
+ apolloProvider: createMockApollo(handlers),
+ propsData: { workItemId, workItemIid, fetchByIid, fullPath: '/some/project' },
+ stubs: {
+ GlAvatarLink,
+ GlSprintf,
+ },
+ });
+
+ await waitForPromises();
+ };
+
+ describe.each([true, false])('fetchByIid is %s', (fetchByIid) => {
+ describe('work item id and iid undefined', () => {
+ beforeEach(async () => {
+ await createComponent({ workItemId: null, workItemIid: null, fetchByIid });
+ });
+
+ it('skips the work item query', () => {
+ expect(successHandler).not.toHaveBeenCalled();
+ expect(successByIidHandler).not.toHaveBeenCalled();
+ });
+ });
+
+ it('shows author name and link', async () => {
+ const author = mockAssignees[0];
+
+ await createComponent({ fetchByIid, author });
+
+ expect(findCreatedAtText()).toEqual(`Created by ${author.name}`);
+ });
+
+ it('shows created time when author is null', async () => {
+ await createComponent({ fetchByIid, author: null });
+
+ expect(findCreatedAtText()).toEqual('Created');
+ });
+
+ it('shows updated time', async () => {
+ await createComponent({ fetchByIid });
+
+ expect(findUpdatedAt().exists()).toBe(true);
+ });
+
+ it('does not show updated time for new work items', async () => {
+ await createComponent({ fetchByIid, updatedAt: null });
+
+ expect(findUpdatedAt().exists()).toBe(false);
+ });
+ });
+});
diff --git a/spec/frontend/work_items/components/work_item_description_spec.js b/spec/frontend/work_items/components/work_item_description_spec.js
index 05476ef5ca0..a12ec23c15a 100644
--- a/spec/frontend/work_items/components/work_item_description_spec.js
+++ b/spec/frontend/work_items/components/work_item_description_spec.js
@@ -16,6 +16,7 @@ import workItemQuery from '~/work_items/graphql/work_item.query.graphql';
import workItemDescriptionSubscription from '~/work_items/graphql/work_item_description.subscription.graphql';
import updateWorkItemMutation from '~/work_items/graphql/update_work_item.mutation.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
+import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
import {
updateWorkItemMutationResponse,
workItemDescriptionSubscriptionResponse,
@@ -102,6 +103,49 @@ describe('WorkItemDescription', () => {
wrapper.destroy();
});
+ describe('editing description with workItemsMvc FF enabled', () => {
+ beforeEach(() => {
+ workItemsMvc = true;
+ });
+
+ it('passes correct autocompletion data and preview markdown sources and enables quick actions', async () => {
+ const {
+ iid,
+ project: { fullPath },
+ } = workItemQueryResponse.data.workItem;
+
+ await createComponent({ isEditing: true });
+
+ expect(findMarkdownEditor().props()).toMatchObject({
+ autocompleteDataSources: autocompleteDataSources(fullPath, iid),
+ supportsQuickActions: true,
+ renderMarkdownPath: markdownPreviewPath(fullPath, iid),
+ quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
+ });
+ });
+ });
+
+ describe('editing description with workItemsMvc FF disabled', () => {
+ beforeEach(() => {
+ workItemsMvc = false;
+ });
+
+ it('passes correct autocompletion data and preview markdown sources', async () => {
+ const {
+ iid,
+ project: { fullPath },
+ } = workItemQueryResponse.data.workItem;
+
+ await createComponent({ isEditing: true });
+
+ expect(findMarkdownField().props()).toMatchObject({
+ autocompleteDataSources: autocompleteDataSources(fullPath, iid),
+ markdownPreviewPath: markdownPreviewPath(fullPath, iid),
+ quickActionsDocsPath: wrapper.vm.$options.quickActionsDocsPath,
+ });
+ });
+ });
+
describe.each([true, false])(
'editing description with workItemsMvc %workItemsMvcEnabled',
(workItemsMvcEnabled) => {
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 8976cd6e22b..938cf6e6f51 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
@@ -136,10 +136,14 @@ describe('WorkItemDetailModal component', () => {
it('updates the work item when WorkItemDetail emits `update-modal` event', async () => {
createComponent();
- findWorkItemDetail().vm.$emit('update-modal', null, 'updatedId');
+ findWorkItemDetail().vm.$emit('update-modal', undefined, {
+ id: 'updatedId',
+ iid: 'updatedIid',
+ });
await waitForPromises();
expect(findWorkItemDetail().props().workItemId).toEqual('updatedId');
+ expect(findWorkItemDetail().props().workItemIid).toEqual('updatedIid');
});
describe('delete work item', () => {
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 a50a48de921..64a7502671e 100644
--- a/spec/frontend/work_items/components/work_item_detail_spec.js
+++ b/spec/frontend/work_items/components/work_item_detail_spec.js
@@ -16,6 +16,7 @@ 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';
+import WorkItemCreatedUpdated from '~/work_items/components/work_item_created_updated.vue';
import WorkItemDueDate from '~/work_items/components/work_item_due_date.vue';
import WorkItemState from '~/work_items/components/work_item_state.vue';
import WorkItemTitle from '~/work_items/components/work_item_title.vue';
@@ -74,6 +75,7 @@ describe('WorkItemDetail component', () => {
const findLoadingIcon = () => wrapper.findComponent(GlLoadingIcon);
const findWorkItemActions = () => wrapper.findComponent(WorkItemActions);
const findWorkItemTitle = () => wrapper.findComponent(WorkItemTitle);
+ const findCreatedUpdated = () => wrapper.findComponent(WorkItemCreatedUpdated);
const findWorkItemState = () => wrapper.findComponent(WorkItemState);
const findWorkItemDescription = () => wrapper.findComponent(WorkItemDescription);
const findWorkItemDueDate = () => wrapper.findComponent(WorkItemDueDate);
@@ -92,6 +94,7 @@ describe('WorkItemDetail component', () => {
isModal = false,
updateInProgress = false,
workItemId = workItemQueryResponse.data.workItem.id,
+ workItemIid = '1',
handler = successHandler,
subscriptionHandler = titleSubscriptionHandler,
confidentialityMock = [updateWorkItemMutation, jest.fn()],
@@ -112,7 +115,7 @@ describe('WorkItemDetail component', () => {
wrapper = shallowMount(WorkItemDetail, {
apolloProvider: createMockApollo(handlers),
- propsData: { isModal, workItemId, workItemIid: '1' },
+ propsData: { isModal, workItemId, workItemIid },
data() {
return {
updateInProgress,
@@ -150,9 +153,9 @@ describe('WorkItemDetail component', () => {
setWindowLocation('');
});
- describe('when there is no `workItemId` prop', () => {
+ describe('when there is no `workItemId` and no `workItemIid` prop', () => {
beforeEach(() => {
- createComponent({ workItemId: null });
+ createComponent({ workItemId: null, workItemIid: null });
});
it('skips the work item query', () => {
@@ -656,6 +659,19 @@ describe('WorkItemDetail component', () => {
});
});
+ it('calls the IID work item query when `useIidInWorkItemsPath` feature flag is true and `iid_path` route parameter is present and is a modal', async () => {
+ setWindowLocation(`?iid_path=true`);
+
+ createComponent({ fetchByIid: true, iidPathQueryParam: 'true', isModal: true });
+ await waitForPromises();
+
+ expect(successHandler).not.toHaveBeenCalled();
+ expect(successByIidHandler).toHaveBeenCalledWith({
+ fullPath: 'group/project',
+ iid: '1',
+ });
+ });
+
describe('hierarchy widget', () => {
it('does not render children tree by default', async () => {
createComponent();
@@ -686,7 +702,7 @@ describe('WorkItemDetail component', () => {
});
it('opens the modal with the child when `show-modal` is emitted', async () => {
- createComponent({ handler });
+ createComponent({ handler, workItemsMvc2Enabled: true });
await waitForPromises();
const event = {
@@ -707,6 +723,7 @@ describe('WorkItemDetail component', () => {
createComponent({
isModal: true,
handler,
+ workItemsMvc2Enabled: true,
});
await waitForPromises();
@@ -749,4 +766,11 @@ describe('WorkItemDetail component', () => {
expect(findNotesWidget().exists()).toBe(true);
});
});
+
+ it('renders created/updated', async () => {
+ createComponent();
+ await waitForPromises();
+
+ expect(findCreatedUpdated().exists()).toBe(true);
+ });
});
diff --git a/spec/frontend/work_items/components/work_item_labels_spec.js b/spec/frontend/work_items/components/work_item_labels_spec.js
index 083bb5bc4a4..0b6ab5c3290 100644
--- a/spec/frontend/work_items/components/work_item_labels_spec.js
+++ b/spec/frontend/work_items/components/work_item_labels_spec.js
@@ -85,7 +85,7 @@ describe('WorkItemLabels component', () => {
it('focuses token selector on token selector input event', async () => {
createComponent();
findTokenSelector().vm.$emit('input', [mockLabels[0]]);
- await nextTick();
+ await waitForPromises();
expect(findEmptyState().exists()).toBe(false);
expect(findTokenSelector().element.contains(document.activeElement)).toBe(true);
@@ -189,6 +189,23 @@ describe('WorkItemLabels component', () => {
);
});
+ it('adds new labels to the end', async () => {
+ const response = workItemResponseFactory({ labels: [mockLabels[1]] });
+ const workItemQueryHandler = jest.fn().mockResolvedValue(response);
+ createComponent({
+ workItemQueryHandler,
+ updateWorkItemMutationHandler: successUpdateWorkItemMutationHandler,
+ });
+ await waitForPromises();
+
+ findTokenSelector().vm.$emit('input', [mockLabels[0]]);
+ await waitForPromises();
+
+ const labels = findTokenSelector().props('selectedTokens');
+ expect(labels[0]).toMatchObject(mockLabels[1]);
+ expect(labels[1]).toMatchObject(mockLabels[0]);
+ });
+
describe('when clicking outside the token selector', () => {
it('calls a mutation with correct variables', () => {
createComponent();
@@ -205,9 +222,7 @@ describe('WorkItemLabels component', () => {
});
it('emits an error and resets labels if mutation was rejected', async () => {
- const workItemQueryHandler = jest.fn().mockResolvedValue(workItemResponseFactory());
-
- createComponent({ updateWorkItemMutationHandler: errorHandler, workItemQueryHandler });
+ createComponent({ updateWorkItemMutationHandler: errorHandler });
await waitForPromises();
@@ -224,6 +239,23 @@ describe('WorkItemLabels component', () => {
expect(updatedLabels).toEqual(initialLabels);
});
+ it('does not make server request if no labels added or removed', async () => {
+ const updateWorkItemMutationHandler = jest
+ .fn()
+ .mockResolvedValue(updateWorkItemMutationResponse);
+
+ createComponent({ updateWorkItemMutationHandler });
+
+ await waitForPromises();
+
+ findTokenSelector().vm.$emit('input', []);
+ findTokenSelector().vm.$emit('blur', new FocusEvent({ relatedTarget: null }));
+
+ await waitForPromises();
+
+ expect(updateWorkItemMutationHandler).not.toHaveBeenCalled();
+ });
+
it('has a subscription', async () => {
createComponent();
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 5e1c46826cc..480f8fbcc58 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
@@ -40,7 +40,6 @@ describe('WorkItemLinksForm', () => {
typesResponse = projectWorkItemTypesQueryResponse,
parentConfidential = false,
hasIterationsFeature = false,
- workItemsMvcEnabled = false,
parentIteration = null,
formType = FORM_TYPES.create,
parentWorkItemType = WORK_ITEM_TYPE_VALUE_ISSUE,
@@ -62,9 +61,6 @@ describe('WorkItemLinksForm', () => {
formType,
},
provide: {
- glFeatures: {
- workItemsMvc: workItemsMvcEnabled,
- },
projectPath: 'project/path',
hasIterationsFeature,
},
diff --git a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
index a61de78c623..ec51f92b578 100644
--- a/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
+++ b/spec/frontend/work_items/components/work_item_links/work_item_links_spec.js
@@ -1,5 +1,4 @@
import Vue, { nextTick } from 'vue';
-import { GlAlert } from '@gitlab/ui';
import VueApollo from 'vue-apollo';
import { shallowMountExtended } from 'helpers/vue_test_utils_helper';
import createMockApollo from 'helpers/mock_apollo_helper';
@@ -8,6 +7,8 @@ import setWindowLocation from 'helpers/set_window_location_helper';
import { stubComponent } from 'helpers/stub_component';
import { DEFAULT_DEBOUNCE_AND_THROTTLE_MS } from '~/lib/utils/constants';
import issueDetailsQuery from 'ee_else_ce/work_items/graphql/get_issue_details.query.graphql';
+import { resolvers } from '~/graphql_shared/issuable_client';
+import WidgetWrapper from '~/work_items/components/widget_wrapper.vue';
import WorkItemLinks from '~/work_items/components/work_item_links/work_item_links.vue';
import WorkItemLinkChild from '~/work_items/components/work_item_links/work_item_link_child.vue';
import WorkItemDetailModal from '~/work_items/components/work_item_detail_modal.vue';
@@ -17,6 +18,7 @@ import changeWorkItemParentMutation from '~/work_items/graphql/update_work_item.
import getWorkItemLinksQuery from '~/work_items/graphql/work_item_links.query.graphql';
import workItemByIidQuery from '~/work_items/graphql/work_item_by_iid.query.graphql';
import {
+ getIssueDetailsResponse,
workItemHierarchyResponse,
workItemHierarchyEmptyResponse,
workItemHierarchyNoUpdatePermissionResponse,
@@ -27,39 +29,6 @@ import {
Vue.use(VueApollo);
-const issueDetailsResponse = (confidential = false) => ({
- data: {
- workspace: {
- id: 'gid://gitlab/Project/1',
- issuable: {
- id: 'gid://gitlab/Issue/4',
- confidential,
- iteration: {
- id: 'gid://gitlab/Iteration/1124',
- title: null,
- startDate: '2022-06-22',
- dueDate: '2022-07-19',
- webUrl: 'http://127.0.0.1:3000/groups/gitlab-org/-/iterations/1124',
- iterationCadence: {
- id: 'gid://gitlab/Iterations::Cadence/1101',
- title: 'Quod voluptates quidem ea eaque eligendi ex corporis.',
- __typename: 'IterationCadence',
- },
- __typename: 'Iteration',
- },
- milestone: {
- dueDate: null,
- expired: false,
- id: 'gid://gitlab/Milestone/28',
- title: 'v2.0',
- __typename: 'Milestone',
- },
- __typename: 'Issue',
- },
- __typename: 'Project',
- },
- },
-});
const showModal = jest.fn();
describe('WorkItemLinks', () => {
@@ -83,7 +52,7 @@ describe('WorkItemLinks', () => {
data = {},
fetchHandler = jest.fn().mockResolvedValue(workItemHierarchyResponse),
mutationHandler = mutationChangeParentHandler,
- issueDetailsQueryHandler = jest.fn().mockResolvedValue(issueDetailsResponse()),
+ issueDetailsQueryHandler = jest.fn().mockResolvedValue(getIssueDetailsResponse()),
hasIterationsFeature = false,
fetchByIid = false,
} = {}) => {
@@ -95,7 +64,7 @@ describe('WorkItemLinks', () => {
[issueDetailsQuery, issueDetailsQueryHandler],
[workItemByIidQuery, childWorkItemByIidHandler],
],
- {},
+ resolvers,
{ addTypename: true },
);
@@ -127,12 +96,12 @@ describe('WorkItemLinks', () => {
},
});
+ wrapper.vm.$refs.wrapper.show = jest.fn();
+
await waitForPromises();
};
- const findAlert = () => wrapper.findComponent(GlAlert);
- const findToggleButton = () => wrapper.findByTestId('toggle-links');
- const findLinksBody = () => wrapper.findByTestId('links-body');
+ const findWidgetWrapper = () => wrapper.findComponent(WidgetWrapper);
const findEmptyState = () => wrapper.findByTestId('links-empty');
const findToggleFormDropdown = () => wrapper.findByTestId('toggle-form');
const findToggleAddFormButton = () => wrapper.findByTestId('toggle-add-form');
@@ -142,31 +111,14 @@ describe('WorkItemLinks', () => {
const findAddLinksForm = () => wrapper.findByTestId('add-links-form');
const findChildrenCount = () => wrapper.findByTestId('children-count');
- beforeEach(async () => {
- await createComponent();
- });
-
afterEach(() => {
- wrapper.destroy();
mockApollo = null;
setWindowLocation('');
});
- it('is expanded by default', () => {
- expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
- expect(findLinksBody().exists()).toBe(true);
- });
-
- it('collapses on click toggle button', async () => {
- findToggleButton().vm.$emit('click');
- await nextTick();
-
- expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
- expect(findLinksBody().exists()).toBe(false);
- });
-
describe('add link form', () => {
it('displays add work item form on click add dropdown then add existing button and hides form on cancel', async () => {
+ await createComponent();
findToggleFormDropdown().vm.$emit('click');
findToggleAddFormButton().vm.$emit('click');
await nextTick();
@@ -181,6 +133,7 @@ describe('WorkItemLinks', () => {
});
it('displays create work item form on click add dropdown then create button and hides form on cancel', async () => {
+ await createComponent();
findToggleFormDropdown().vm.$emit('click');
findToggleCreateFormButton().vm.$emit('click');
await nextTick();
@@ -193,6 +146,24 @@ describe('WorkItemLinks', () => {
expect(findAddLinksForm().exists()).toBe(false);
});
+
+ it('adds work item child from the form', async () => {
+ const workItem = {
+ ...workItemQueryResponse.data.workItem,
+ id: 'gid://gitlab/WorkItem/11',
+ };
+ await createComponent();
+ findToggleFormDropdown().vm.$emit('click');
+ findToggleCreateFormButton().vm.$emit('click');
+ await nextTick();
+
+ expect(findWorkItemLinkChildItems()).toHaveLength(4);
+
+ findAddLinksForm().vm.$emit('addWorkItemChild', workItem);
+ await waitForPromises();
+
+ expect(findWorkItemLinkChildItems()).toHaveLength(5);
+ });
});
describe('when no child links', () => {
@@ -207,8 +178,8 @@ describe('WorkItemLinks', () => {
});
});
- it('renders all hierarchy widget children', () => {
- expect(findLinksBody().exists()).toBe(true);
+ it('renders all hierarchy widget children', async () => {
+ await createComponent();
expect(findWorkItemLinkChildItems()).toHaveLength(4);
});
@@ -219,15 +190,13 @@ describe('WorkItemLinks', () => {
fetchHandler: jest.fn().mockRejectedValue(new Error(errorMessage)),
});
- await nextTick();
-
- expect(findAlert().exists()).toBe(true);
- expect(findAlert().text()).toBe(errorMessage);
+ expect(findWidgetWrapper().props('error')).toBe(errorMessage);
});
- it('displays number if children', () => {
- expect(findChildrenCount().exists()).toBe(true);
+ it('displays number of children', async () => {
+ await createComponent();
+ expect(findChildrenCount().exists()).toBe(true);
expect(findChildrenCount().text()).toContain('4');
});
@@ -294,7 +263,9 @@ describe('WorkItemLinks', () => {
describe('when parent item is confidential', () => {
it('passes correct confidentiality status to form', async () => {
await createComponent({
- issueDetailsQueryHandler: jest.fn().mockResolvedValue(issueDetailsResponse(true)),
+ issueDetailsQueryHandler: jest
+ .fn()
+ .mockResolvedValue(getIssueDetailsResponse({ confidential: true })),
});
findToggleFormDropdown().vm.$emit('click');
findToggleAddFormButton().vm.$emit('click');
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 156f06a0d5e..0236fe2e60d 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
@@ -23,8 +23,6 @@ describe('WorkItemTree', () => {
let getWorkItemQueryHandler;
let wrapper;
- const findToggleButton = () => wrapper.findByTestId('toggle-tree');
- const findTreeBody = () => wrapper.findByTestId('tree-body');
const findEmptyState = () => wrapper.findByTestId('tree-empty');
const findToggleFormSplitButton = () => wrapper.findComponent(OkrActionsSplitButton);
const findForm = () => wrapper.findComponent(WorkItemLinksForm);
@@ -64,36 +62,25 @@ describe('WorkItemTree', () => {
projectPath: 'test/project',
},
});
+
+ wrapper.vm.$refs.wrapper.show = jest.fn();
};
- beforeEach(() => {
+ it('displays Add button', () => {
createComponent();
- });
-
- afterEach(() => {
- wrapper.destroy();
- });
- it('is expanded by default and displays Add button', () => {
- expect(findToggleButton().props('icon')).toBe('chevron-lg-up');
- expect(findTreeBody().exists()).toBe(true);
expect(findToggleFormSplitButton().exists()).toBe(true);
});
- it('collapses on click toggle button', async () => {
- findToggleButton().vm.$emit('click');
- await nextTick();
-
- expect(findToggleButton().props('icon')).toBe('chevron-lg-down');
- expect(findTreeBody().exists()).toBe(false);
- });
-
it('displays empty state if there are no children', () => {
createComponent({ children: [] });
+
expect(findEmptyState().exists()).toBe(true);
});
it('renders all hierarchy widget children', () => {
+ createComponent();
+
const workItemLinkChildren = findWorkItemLinkChildItems();
expect(workItemLinkChildren).toHaveLength(4);
expect(workItemLinkChildren.at(0).props().childItem.confidential).toBe(
@@ -102,6 +89,8 @@ describe('WorkItemTree', () => {
});
it('does not display form by default', () => {
+ createComponent();
+
expect(findForm().exists()).toBe(false);
});
@@ -114,6 +103,8 @@ describe('WorkItemTree', () => {
`(
'when selecting $option from split button, renders the form passing $formType and $childType',
async ({ event, formType, childType }) => {
+ createComponent();
+
findToggleFormSplitButton().vm.$emit(event);
await nextTick();
@@ -128,13 +119,16 @@ describe('WorkItemTree', () => {
);
it('remove event on child triggers `removeChild` event', () => {
+ createComponent();
const firstChild = findWorkItemLinkChildItems().at(0);
+
firstChild.vm.$emit('removeChild', 'gid://gitlab/WorkItem/2');
expect(wrapper.emitted('removeChild')).toEqual([['gid://gitlab/WorkItem/2']]);
});
it('emits `show-modal` on `click` event', () => {
+ createComponent();
const firstChild = findWorkItemLinkChildItems().at(0);
const event = {
childItem: 'gid://gitlab/WorkItem/2',
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 23dd2b6bacb..3db848a0ad2 100644
--- a/spec/frontend/work_items/components/work_item_notes_spec.js
+++ b/spec/frontend/work_items/components/work_item_notes_spec.js
@@ -1,22 +1,26 @@
-import { GlSkeletonLoader } from '@gitlab/ui';
+import { GlSkeletonLoader, GlModal } from '@gitlab/ui';
import { shallowMount } from '@vue/test-utils';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import createMockApollo from 'helpers/mock_apollo_helper';
+import { stubComponent } from 'helpers/stub_component';
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 WorkItemDiscussion from '~/work_items/components/notes/work_item_discussion.vue';
+import WorkItemAddNote from '~/work_items/components/notes/work_item_add_note.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 workItemNotesQuery from '~/work_items/graphql/notes/work_item_notes.query.graphql';
+import workItemNotesByIidQuery from '~/work_items/graphql/notes/work_item_notes_by_iid.query.graphql';
+import deleteWorkItemNoteMutation from '~/work_items/graphql/notes/delete_work_item_notes.mutation.graphql';
import { DEFAULT_PAGE_SIZE_NOTES, WIDGET_TYPE_NOTES } from '~/work_items/constants';
-import { DESC } from '~/notes/constants';
+import { ASC, DESC } from '~/notes/constants';
import {
mockWorkItemNotesResponse,
workItemQueryResponse,
mockWorkItemNotesByIidResponse,
mockMoreWorkItemNotesResponse,
+ mockWorkItemNotesResponseWithComments,
} from '../mock_data';
const mockWorkItemId = workItemQueryResponse.data.workItem.id;
@@ -32,34 +36,56 @@ const mockMoreNotesWidgetResponse = mockMoreWorkItemNotesResponse.data.workItem.
(widget) => widget.type === WIDGET_TYPE_NOTES,
);
+const mockWorkItemNotesWidgetResponseWithComments = mockWorkItemNotesResponseWithComments.data.workItem.widgets.find(
+ (widget) => widget.type === WIDGET_TYPE_NOTES,
+);
+
const firstSystemNodeId = mockNotesWidgetResponse.discussions.nodes[0].notes.nodes[0].id;
+const mockDiscussions = mockWorkItemNotesWidgetResponseWithComments.discussions.nodes;
+
describe('WorkItemNotes component', () => {
let wrapper;
Vue.use(VueApollo);
+ const showModal = jest.fn();
+
const findAllSystemNotes = () => wrapper.findAllComponents(SystemNote);
+ const findAllListItems = () => wrapper.findAll('ul.timeline > *');
const findActivityLabel = () => wrapper.find('label');
- const findWorkItemCommentForm = () => wrapper.findComponent(WorkItemCommentForm);
+ const findWorkItemAddNote = () => wrapper.findComponent(WorkItemAddNote);
const findSkeletonLoader = () => wrapper.findComponent(GlSkeletonLoader);
const findSortingFilter = () => wrapper.findComponent(ActivityFilter);
const findSystemNoteAtIndex = (index) => findAllSystemNotes().at(index);
+ const findAllWorkItemCommentNotes = () => wrapper.findAllComponents(WorkItemDiscussion);
+ const findWorkItemCommentNoteAtIndex = (index) => findAllWorkItemCommentNotes().at(index);
+ const findDeleteNoteModal = () => wrapper.findComponent(GlModal);
+
const workItemNotesQueryHandler = jest.fn().mockResolvedValue(mockWorkItemNotesResponse);
const workItemNotesByIidQueryHandler = jest
.fn()
.mockResolvedValue(mockWorkItemNotesByIidResponse);
const workItemMoreNotesQueryHandler = jest.fn().mockResolvedValue(mockMoreWorkItemNotesResponse);
+ const workItemNotesWithCommentsQueryHandler = jest
+ .fn()
+ .mockResolvedValue(mockWorkItemNotesResponseWithComments);
+ const deleteWorkItemNoteMutationSuccessHandler = jest.fn().mockResolvedValue({
+ data: { destroyNote: { note: null, __typename: 'DestroyNote' } },
+ });
+ const errorHandler = jest.fn().mockRejectedValue('Houston, we have a problem');
const createComponent = ({
workItemId = mockWorkItemId,
fetchByIid = false,
defaultWorkItemNotesQueryHandler = workItemNotesQueryHandler,
+ deleteWINoteMutationHandler = deleteWorkItemNoteMutationSuccessHandler,
} = {}) => {
wrapper = shallowMount(WorkItemNotes, {
apolloProvider: createMockApollo([
[workItemNotesQuery, defaultWorkItemNotesQueryHandler],
[workItemNotesByIidQuery, workItemNotesByIidQueryHandler],
+ [deleteWorkItemNoteMutation, deleteWINoteMutationHandler],
]),
propsData: {
workItemId,
@@ -75,6 +101,9 @@ describe('WorkItemNotes component', () => {
useIidInWorkItemsPath: fetchByIid,
},
},
+ stubs: {
+ GlModal: stubComponent(GlModal, { methods: { show: showModal } }),
+ },
});
};
@@ -87,10 +116,14 @@ describe('WorkItemNotes component', () => {
});
it('passes correct props to comment form component', async () => {
- createComponent({ workItemId: mockWorkItemId, fetchByIid: false });
+ createComponent({
+ workItemId: mockWorkItemId,
+ fetchByIid: false,
+ defaultWorkItemNotesQueryHandler: workItemNotesByIidQueryHandler,
+ });
await waitForPromises();
- expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(false);
+ expect(findWorkItemAddNote().props('fetchByIid')).toEqual(false);
});
describe('when notes are loading', () => {
@@ -121,13 +154,14 @@ describe('WorkItemNotes component', () => {
});
it('renders the notes list to the length of the response', () => {
+ expect(workItemNotesByIidQueryHandler).toHaveBeenCalled();
expect(findAllSystemNotes()).toHaveLength(
mockNotesByIidWidgetResponse.discussions.nodes.length,
);
});
it('passes correct props to comment form component', () => {
- expect(findWorkItemCommentForm().props('fetchByIid')).toEqual(true);
+ expect(findWorkItemAddNote().props('fetchByIid')).toEqual(true);
});
});
@@ -180,5 +214,124 @@ describe('WorkItemNotes component', () => {
expect(findSystemNoteAtIndex(0).props('note').id).not.toEqual(firstSystemNodeId);
});
+
+ it('puts form at start of list in when sorting by newest first', async () => {
+ await findSortingFilter().vm.$emit('changeSortOrder', DESC);
+
+ expect(findAllListItems().at(0).is(WorkItemAddNote)).toEqual(true);
+ });
+
+ it('puts form at end of list in when sorting by oldest first', async () => {
+ await findSortingFilter().vm.$emit('changeSortOrder', ASC);
+
+ expect(findAllListItems().at(-1).is(WorkItemAddNote)).toEqual(true);
+ });
+ });
+
+ describe('Activity comments', () => {
+ beforeEach(async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ await waitForPromises();
+ });
+
+ it('should not have any system notes', () => {
+ expect(workItemNotesWithCommentsQueryHandler).toHaveBeenCalled();
+ expect(findAllSystemNotes()).toHaveLength(0);
+ });
+
+ it('should have work item notes', () => {
+ expect(workItemNotesWithCommentsQueryHandler).toHaveBeenCalled();
+ expect(findAllWorkItemCommentNotes()).toHaveLength(mockDiscussions.length);
+ });
+
+ it('should pass all the correct props to work item comment note', () => {
+ const commentIndex = 0;
+ const firstCommentNote = findWorkItemCommentNoteAtIndex(commentIndex);
+
+ expect(firstCommentNote.props('discussion')).toEqual(
+ mockDiscussions[commentIndex].notes.nodes,
+ );
+ });
+ });
+
+ it('should open delete modal confirmation when child discussion emits `deleteNote` event', async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ await waitForPromises();
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: '1', isLastNote: false });
+ expect(showModal).toHaveBeenCalled();
+ });
+
+ describe('when modal is open', () => {
+ beforeEach(() => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ });
+ return waitForPromises();
+ });
+
+ it('sends the mutation with correct variables', () => {
+ const noteId = 'some-test-id';
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', { id: noteId });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ expect(deleteWorkItemNoteMutationSuccessHandler).toHaveBeenCalledWith({
+ input: {
+ id: noteId,
+ },
+ });
+ });
+
+ it('successfully removes the note from the discussion', async () => {
+ expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(2);
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', {
+ id: mockDiscussions[0].notes.nodes[0].id,
+ });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ await waitForPromises();
+ expect(findWorkItemCommentNoteAtIndex(0).props('discussion')).toHaveLength(1);
+ });
+
+ it('successfully removes the discussion from work item if discussion only had one note', async () => {
+ const secondDiscussion = findWorkItemCommentNoteAtIndex(1);
+
+ expect(findAllWorkItemCommentNotes()).toHaveLength(2);
+ expect(secondDiscussion.props('discussion')).toHaveLength(1);
+
+ secondDiscussion.vm.$emit('deleteNote', {
+ id: mockDiscussions[1].notes.nodes[0].id,
+ discussion: { id: mockDiscussions[1].id },
+ });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ await waitForPromises();
+ expect(findAllWorkItemCommentNotes()).toHaveLength(1);
+ });
+ });
+
+ it('emits `error` event if delete note mutation is rejected', async () => {
+ createComponent({
+ defaultWorkItemNotesQueryHandler: workItemNotesWithCommentsQueryHandler,
+ deleteWINoteMutationHandler: errorHandler,
+ });
+ await waitForPromises();
+
+ findWorkItemCommentNoteAtIndex(0).vm.$emit('deleteNote', {
+ id: mockDiscussions[0].notes.nodes[0].id,
+ });
+ findDeleteNoteModal().vm.$emit('primary');
+
+ await waitForPromises();
+
+ expect(wrapper.emitted('error')).toEqual([
+ ['Something went wrong when deleting a comment. Please try again'],
+ ]);
});
});
diff --git a/spec/frontend/work_items/mock_data.js b/spec/frontend/work_items/mock_data.js
index 67b477b6eb0..d4832fe376d 100644
--- a/spec/frontend/work_items/mock_data.js
+++ b/spec/frontend/work_items/mock_data.js
@@ -57,7 +57,16 @@ export const workItemQueryResponse = {
description: 'description',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
+ author: {
+ avatarUrl: 'http://127.0.0.1:3000/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',
+ },
project: {
__typename: 'Project',
id: '1',
@@ -113,6 +122,7 @@ export const workItemQueryResponse = {
nodes: [
{
id: 'gid://gitlab/WorkItem/444',
+ iid: '4',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
confidential: false,
@@ -152,7 +162,11 @@ export const updateWorkItemMutationResponse = {
description: 'description',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: '2022-08-08T12:41:54Z',
closedAt: null,
+ author: {
+ ...mockAssignees[0],
+ },
project: {
__typename: 'Project',
id: '1',
@@ -176,6 +190,7 @@ export const updateWorkItemMutationResponse = {
nodes: [
{
id: 'gid://gitlab/WorkItem/444',
+ iid: '4',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
confidential: false,
@@ -200,6 +215,14 @@ export const updateWorkItemMutationResponse = {
nodes: [mockAssignees[0]],
},
},
+ {
+ __typename: 'WorkItemWidgetLabels',
+ type: 'LABELS',
+ allowsScopedLabels: false,
+ labels: {
+ nodes: mockLabels,
+ },
+ },
],
},
},
@@ -264,7 +287,6 @@ export const workItemResponseFactory = ({
allowsMultipleAssignees = true,
assigneesWidgetPresent = true,
datesWidgetPresent = true,
- labelsWidgetPresent = true,
weightWidgetPresent = true,
progressWidgetPresent = true,
milestoneWidgetPresent = true,
@@ -273,12 +295,17 @@ export const workItemResponseFactory = ({
notesWidgetPresent = true,
confidential = false,
canInviteMembers = false,
+ labelsWidgetPresent = true,
+ labels = mockLabels,
allowsScopedLabels = false,
lastEditedAt = null,
lastEditedBy = null,
withCheckboxes = false,
parent = mockParent.parent,
workItemType = taskType,
+ author = mockAssignees[0],
+ createdAt = '2022-08-03T12:41:54Z',
+ updatedAt = '2022-08-08T12:32:54Z',
} = {}) => ({
data: {
workItem: {
@@ -289,8 +316,10 @@ export const workItemResponseFactory = ({
state: 'OPEN',
description: 'description',
confidential,
- createdAt: '2022-08-03T12:41:54Z',
+ createdAt,
+ updatedAt,
closedAt: null,
+ author,
project: {
__typename: 'Project',
id: '1',
@@ -330,7 +359,7 @@ export const workItemResponseFactory = ({
type: 'LABELS',
allowsScopedLabels,
labels: {
- nodes: mockLabels,
+ nodes: labels,
},
}
: { type: 'MOCK TYPE' },
@@ -409,6 +438,7 @@ export const workItemResponseFactory = ({
nodes: [
{
id: 'gid://gitlab/WorkItem/444',
+ iid: '5',
createdAt: '2022-08-03T12:41:54Z',
closedAt: null,
confidential: false,
@@ -441,6 +471,28 @@ export const workItemResponseFactory = ({
},
});
+export const getIssueDetailsResponse = ({ confidential = false } = {}) => ({
+ data: {
+ workspace: {
+ id: 'gid://gitlab/Project/1',
+ issuable: {
+ id: 'gid://gitlab/Issue/4',
+ confidential,
+ iteration: {
+ id: 'gid://gitlab/Iteration/1124',
+ __typename: 'Iteration',
+ },
+ milestone: {
+ id: 'gid://gitlab/Milestone/28',
+ __typename: 'Milestone',
+ },
+ __typename: 'Issue',
+ },
+ __typename: 'Project',
+ },
+ },
+});
+
export const projectWorkItemTypesQueryResponse = {
data: {
workspace: {
@@ -470,7 +522,11 @@ export const createWorkItemMutationResponse = {
description: 'description',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
+ author: {
+ ...mockAssignees[0],
+ },
project: {
__typename: 'Project',
id: '1',
@@ -494,6 +550,16 @@ export const createWorkItemMutationResponse = {
},
};
+export const createWorkItemMutationErrorResponse = {
+ data: {
+ workItemCreate: {
+ __typename: 'WorkItemCreatePayload',
+ workItem: null,
+ errors: ['an error'],
+ },
+ },
+};
+
export const createWorkItemFromTaskMutationResponse = {
data: {
workItemCreateFromTask: {
@@ -1045,11 +1111,15 @@ export const workItemObjectiveWithChild = {
deleteWorkItem: true,
updateWorkItem: true,
},
+ author: {
+ ...mockAssignees[0],
+ },
title: 'Objective',
description: 'Objective description',
state: 'OPEN',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
widgets: [
{
@@ -1190,7 +1260,11 @@ export const changeWorkItemParentMutationResponse = {
title: 'Foo',
confidential: false,
createdAt: '2022-08-03T12:41:54Z',
+ updatedAt: null,
closedAt: null,
+ author: {
+ ...mockAssignees[0],
+ },
project: {
__typename: 'Project',
id: '1',
@@ -1557,7 +1631,7 @@ export const projectWorkItemResponse = {
export const mockWorkItemNotesResponse = {
data: {
workItem: {
- id: 'gid://gitlab/WorkItem/600',
+ id: 'gid://gitlab/WorkItem/1',
iid: '60',
widgets: [
{
@@ -1596,20 +1670,30 @@ export const mockWorkItemNotesResponse = {
},
nodes: [
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
notes: {
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1629,20 +1713,30 @@ export const mockWorkItemNotesResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
notes: {
nodes: [
{
id: 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ body: 'changed milestone to %v4.0',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723565678',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1662,19 +1756,29 @@ export const mockWorkItemNotesResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
notes: {
nodes: [
{
id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
+ body: 'changed weight to **89**',
bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1753,20 +1857,31 @@ export const mockWorkItemNotesByIidResponse = {
},
nodes: [
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
notes: {
nodes: [
{
id: 'gid://gitlab/Note/2428',
+ body: 'added 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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723561234',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1786,21 +1901,32 @@ export const mockWorkItemNotesByIidResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
notes: {
nodes: [
{
id:
'gid://gitlab/MilestoneNote/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ body: 'changed milestone to %v4.0',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723568765',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1820,21 +1946,33 @@ export const mockWorkItemNotesByIidResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/addbc177f7664699a135130ab05ffb78c57e4db3',
+ id: 'gid://gitlab/Discussion/addbc177f7664699a135130ab05ffb78c57e4db3',
notes: {
nodes: [
{
id:
'gid://gitlab/IterationNote/addbc177f7664699a135130ab05ffb78c57e4db3',
+ body:
+ 'changed iteration to Et autem debitis nam suscipit eos ut. Jul 13, 2022 - Jul 19, 2022',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id:
+ 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1910,20 +2048,30 @@ export const mockMoreWorkItemNotesResponse = {
},
nodes: [
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
notes: {
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1112356a59e',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1943,20 +2091,30 @@ export const mockMoreWorkItemNotesResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
+ id: 'gid://gitlab/Discussion/7b08b89a728a5ceb7de8334246837ba1d07270dc',
notes: {
nodes: [
{
id: 'gid://gitlab/MilestoneNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83823',
+ body: 'changed milestone to %v4.0',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da1272356a59e',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -1976,19 +2134,29 @@ export const mockMoreWorkItemNotesResponse = {
__typename: 'Discussion',
},
{
- id:
- 'gid://gitlab/IndividualNoteDiscussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
notes: {
nodes: [
{
id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ body: 'changed weight to **89**',
bodyHtml: '<p dir="auto">changed weight to <strong>89</strong></p>',
systemNoteIconName: 'weight',
createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: true,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -2022,6 +2190,55 @@ export const createWorkItemNoteResponse = {
data: {
createNote: {
errors: [],
+ note: {
+ id: 'gid://gitlab/Note/569',
+ discussion: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/Note/569',
+ body: 'Main comment',
+ bodyHtml: '<p data-sourcepos="1:1-1:9" dir="auto">Main comment</p>',
+ system: false,
+ internal: false,
+ systemNoteIconName: null,
+ createdAt: '2023-01-25T04:49:46Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
+ discussion: {
+ id: 'gid://gitlab/Discussion/c872ba2d7d3eb780d2255138d67ca8b04f65b122',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ body: 'Latest 22',
+ bodyHtml: '<p data-sourcepos="1:1-1:9" dir="auto">Latest 22</p>',
+ __typename: 'Note',
+ },
__typename: 'CreateNotePayload',
},
},
@@ -2029,14 +2246,25 @@ export const createWorkItemNoteResponse = {
export const mockWorkItemCommentNote = {
id: 'gid://gitlab/Note/158',
+ body: 'How are you ? what do you think about this ?',
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',
+ lastEditedAt: null,
+ lastEditedBy: null,
system: false,
internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723569876',
+ },
userPermissions: {
adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
__typename: 'NotePermissions',
},
author: {
@@ -2048,3 +2276,174 @@ export const mockWorkItemCommentNote = {
__typename: 'UserCore',
},
};
+
+export const mockWorkItemNotesResponseWithComments = {
+ 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: false,
+ hasPreviousPage: false,
+ startCursor: null,
+ endCursor: null,
+ __typename: 'PageInfo',
+ },
+ nodes: [
+ {
+ id: 'gid://gitlab/Discussion/8bbc4890b6ff0f2cde93a5a0947cd2b8a13d3b6e',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/DiscussionNote/174',
+ body: 'Separate thread',
+ bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Separate thread</p>',
+ system: false,
+ internal: false,
+ systemNoteIconName: null,
+ createdAt: '2023-01-12T07:47:40Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
+ discussion: {
+ id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ __typename: 'Note',
+ },
+ {
+ id: 'gid://gitlab/DiscussionNote/235',
+ body: 'Thread comment',
+ bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Thread comment</p>',
+ system: false,
+ internal: false,
+ systemNoteIconName: null,
+ createdAt: '2023-01-18T09:09:54Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
+ discussion: {
+ id: 'gid://gitlab/Discussion/2bb1162fd0d39297d1a68fdd7d4083d3780af0f3',
+ __typename: 'Discussion',
+ },
+ author: {
+ id: 'gid://gitlab/User/1',
+ avatarUrl:
+ 'https://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80&d=identicon',
+ name: 'Administrator',
+ username: 'root',
+ webUrl: 'http://127.0.0.1:3000/root',
+ __typename: 'UserCore',
+ },
+ userPermissions: {
+ adminNote: true,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __typename: 'NotePermissions',
+ },
+ __typename: 'Note',
+ },
+ ],
+ __typename: 'NoteConnection',
+ },
+ __typename: 'Discussion',
+ },
+ {
+ id: 'gid://gitlab/Discussion/0f2f195ec0d1ef95ee9d5b10446b8e96a7d83864',
+ notes: {
+ nodes: [
+ {
+ id: 'gid://gitlab/WeightNote/0f2f195ec0d1ef95ee9d5b10446b8e96a9883864',
+ body: 'Main thread 2',
+ bodyHtml: '<p data-sourcepos="1:1-1:15" dir="auto">Main thread 2</p>',
+ systemNoteIconName: 'weight',
+ createdAt: '2022-11-25T07:16:20Z',
+ lastEditedAt: null,
+ lastEditedBy: null,
+ system: false,
+ internal: false,
+ discussion: {
+ id: 'gid://gitlab/Discussion/9c17769ca29798eddaed539d010da12723560987',
+ },
+ userPermissions: {
+ adminNote: false,
+ awardEmoji: true,
+ readNote: true,
+ createNote: true,
+ resolveNote: true,
+ repositionNote: true,
+ __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',
+ },
+ },
+};
diff --git a/spec/frontend/work_items/utils_spec.js b/spec/frontend/work_items/utils_spec.js
new file mode 100644
index 00000000000..aa24b80cf08
--- /dev/null
+++ b/spec/frontend/work_items/utils_spec.js
@@ -0,0 +1,27 @@
+import { autocompleteDataSources, markdownPreviewPath } from '~/work_items/utils';
+
+describe('autocompleteDataSources', () => {
+ beforeEach(() => {
+ gon.relative_url_root = '/foobar';
+ });
+
+ it('returns corrrect data sources', () => {
+ expect(autocompleteDataSources('project/group', '2')).toMatchObject({
+ commands: '/foobar/project/group/-/autocomplete_sources/commands?type=WorkItem&type_id=2',
+ labels: '/foobar/project/group/-/autocomplete_sources/labels?type=WorkItem&type_id=2',
+ members: '/foobar/project/group/-/autocomplete_sources/members?type=WorkItem&type_id=2',
+ });
+ });
+});
+
+describe('markdownPreviewPath', () => {
+ beforeEach(() => {
+ gon.relative_url_root = '/foobar';
+ });
+
+ it('returns corrrect data sources', () => {
+ expect(markdownPreviewPath('project/group', '2')).toEqual(
+ '/foobar/project/group/preview_markdown?target_type=WorkItem&target_id=2',
+ );
+ });
+});
diff --git a/spec/frontend/zen_mode_spec.js b/spec/frontend/zen_mode_spec.js
index a88910b2613..85f1dbdc305 100644
--- a/spec/frontend/zen_mode_spec.js
+++ b/spec/frontend/zen_mode_spec.js
@@ -6,6 +6,7 @@ import Mousetrap from 'mousetrap';
import { loadHTMLFixture, resetHTMLFixture } from 'helpers/fixtures';
import GLForm from '~/gl_form';
import * as utils from '~/lib/utils/common_utils';
+import { HTTP_STATUS_OK } from '~/lib/utils/http_status';
import ZenMode from '~/zen_mode';
describe('ZenMode', () => {
@@ -32,7 +33,7 @@ describe('ZenMode', () => {
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onGet().reply(200);
+ mock.onGet().reply(HTTP_STATUS_OK);
loadHTMLFixture(fixtureName);